Improved game UI and state management

This commit is contained in:
Arnaud Vergnet 2020-03-16 19:10:32 +01:00
parent 8fc5cfb25e
commit 7f33c8376d
3 changed files with 162 additions and 32 deletions

View file

@ -10,6 +10,7 @@ export default class GameLogic {
width: number;
gameRunning: boolean;
gamePaused: boolean;
gameTime: number;
score: number;
@ -27,6 +28,7 @@ export default class GameLogic {
this.height = height;
this.width = width;
this.gameRunning = false;
this.gamePaused = false;
this.gameTick = 250;
this.colors = colors;
}
@ -43,6 +45,10 @@ export default class GameLogic {
return this.gameRunning;
}
isGamePaused(): boolean {
return this.gamePaused;
}
getEmptyLine() {
let line = [];
for (let col = 0; col < this.getWidth(); col++) {
@ -161,17 +167,30 @@ export default class GameLogic {
callback(this.gameTime, this.score, this.getFinalGrid());
}
canUseInput() {
return this.gameRunning && !this.gamePaused
}
rightPressed(callback: Function) {
if (!this.canUseInput())
return;
this.tryMoveTetromino(1, 0);
callback(this.getFinalGrid());
}
leftPressed(callback: Function) {
if (!this.canUseInput())
return;
this.tryMoveTetromino(-1, 0);
callback(this.getFinalGrid());
}
rotatePressed(callback: Function) {
if (!this.canUseInput())
return;
this.tryRotateTetromino();
callback(this.getFinalGrid());
}
@ -180,20 +199,32 @@ export default class GameLogic {
let shape = Math.floor(Math.random() * 7);
this.currentObject = new Tetromino(shape, this.colors);
if (!this.isTetrominoPositionValid())
this.endGame();
this.endGame(false);
}
endGame() {
console.log('Game Over!');
togglePause() {
if (!this.gameRunning)
return;
this.gamePaused = !this.gamePaused;
if (this.gamePaused) {
clearInterval(this.gameTickInterval);
} else {
this.gameTickInterval = setInterval(this.onTick, this.gameTick);
}
}
endGame(isRestart: boolean) {
this.gameRunning = false;
this.gamePaused = false;
clearInterval(this.gameTickInterval);
this.endCallback(this.gameTime, this.score);
this.endCallback(this.gameTime, this.score, isRestart);
}
startGame(tickCallback: Function, endCallback: Function) {
if (this.gameRunning)
return;
this.endGame(true);
this.gameRunning = true;
this.gamePaused = false;
this.gameTime = 0;
this.score = 0;
this.currentGrid = this.getEmptyGrid();

View file

@ -1,10 +1,12 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {Alert, View} from 'react-native';
import {IconButton, Text, withTheme} from 'react-native-paper';
import {MaterialCommunityIcons} from "@expo/vector-icons";
import GameLogic from "./GameLogic";
import Grid from "./components/Grid";
import HeaderButton from "../../components/HeaderButton";
type Props = {
navigation: Object,
@ -12,6 +14,7 @@ type Props = {
type State = {
grid: Array<Array<Object>>,
gameRunning: boolean,
gameTime: number,
gameScore: number
}
@ -31,22 +34,49 @@ class TetrisScreen extends React.Component<Props, State> {
this.logic = new GameLogic(20, 10, this.colors);
this.state = {
grid: this.logic.getEmptyGrid(),
gameRunning: false,
gameTime: 0,
gameScore: 0,
};
this.onTick = this.onTick.bind(this);
this.onGameEnd = this.onGameEnd.bind(this);
this.updateGrid = this.updateGrid.bind(this);
const onScreenBlur = this.onScreenBlur.bind(this);
this.props.navigation.addListener('blur', onScreenBlur);
this.props.navigation.addListener('blur', this.onScreenBlur.bind(this));
this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
}
componentDidMount() {
const rightButton = this.getRightButton.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
});
this.startGame();
}
getRightButton() {
return (
<View
style={{
flexDirection: 'row',
}}>
<HeaderButton icon={'pause'} onPress={() => this.togglePause()}/>
</View>
);
}
/**
* Remove any interval on un-focus
*/
onScreenBlur() {
this.logic.endGame();
if (!this.logic.isGamePaused())
this.logic.togglePause();
}
onScreenFocus() {
if (!this.logic.isGameRunning())
this.startGame();
else if (this.logic.isGamePaused())
this.showPausePopup();
}
onTick(time: number, score: number, newGrid: Array<Array<Object>>) {
@ -63,17 +93,63 @@ class TetrisScreen extends React.Component<Props, State> {
});
}
startGame() {
if (!this.logic.isGameRunning()) {
this.logic.startGame(this.onTick, this.onGameEnd);
}
togglePause() {
this.logic.togglePause();
if (this.logic.isGamePaused())
this.showPausePopup();
}
onGameEnd(time: number, score: number) {
showPausePopup() {
Alert.alert(
'PAUSE',
'GAME PAUSED',
[
{text: 'RESTART', onPress: () => this.showRestartConfirm()},
{text: 'RESUME', onPress: () => this.togglePause()},
],
{cancelable: false},
);
}
showRestartConfirm() {
Alert.alert(
'RESTART?',
'WHOA THERE',
[
{text: 'NO', onPress: () => this.showPausePopup()},
{text: 'YES', onPress: () => this.startGame()},
],
{cancelable: false},
);
}
showGameOverConfirm() {
Alert.alert(
'GAME OVER',
'NOOB',
[
{text: 'LEAVE', onPress: () => this.props.navigation.goBack()},
{text: 'RESTART', onPress: () => this.startGame()},
],
{cancelable: false},
);
}
startGame() {
this.logic.startGame(this.onTick, this.onGameEnd);
this.setState({
gameRunning: true,
});
}
onGameEnd(time: number, score: number, isRestart: boolean) {
this.setState({
gameTime: time,
gameScore: score,
})
gameRunning: false,
});
if (!isRestart)
this.showGameOverConfirm();
}
render() {
@ -82,28 +158,49 @@ class TetrisScreen extends React.Component<Props, State> {
width: '100%',
height: '100%',
}}>
<Text style={{
textAlign: 'center',
<View style={{
flexDirection: 'row',
position: 'absolute',
top: 10,
left: 10,
}}>
Score: {this.state.gameScore}
</Text>
<Text style={{
textAlign: 'center',
<MaterialCommunityIcons
name={'timer'}
color={this.colors.subtitle}
size={20}/>
<Text style={{
marginLeft: 5,
color: this.colors.subtitle
}}>{this.state.gameTime}</Text>
</View>
<View style={{
flexDirection: 'row',
marginRight: 'auto',
marginLeft: 'auto',
}}>
time: {this.state.gameTime}
</Text>
<MaterialCommunityIcons
name={'star'}
color={this.colors.tetrisScore}
size={30}/>
<Text style={{
marginLeft: 5,
fontSize: 22,
}}>{this.state.gameScore}</Text>
</View>
<Grid
width={this.logic.getWidth()}
height={this.logic.getHeight()}
grid={this.state.grid}
/>
<View style={{
flexDirection: 'row-reverse',
flexDirection: 'row',
marginLeft: 'auto',
marginRight: 'auto',
}}>
<IconButton
icon="arrow-right"
icon="format-rotate-90"
size={40}
onPress={() => this.logic.rightPressed(this.updateGrid)}
onPress={() => this.logic.rotatePressed(this.updateGrid)}
/>
<IconButton
icon="arrow-left"
@ -111,15 +208,15 @@ class TetrisScreen extends React.Component<Props, State> {
onPress={() => this.logic.leftPressed(this.updateGrid)}
/>
<IconButton
icon="format-rotate-90"
icon="arrow-right"
size={40}
onPress={() => this.logic.rotatePressed(this.updateGrid)}
onPress={() => this.logic.rightPressed(this.updateGrid)}
/>
<IconButton
icon="power"
size={40}
onPress={() => this.startGame()}
/>
icon="arrow-down"
size={40}
onPress={() => this.logic.rightPressed(this.updateGrid)}
/>
</View>
</View>
);

View file

@ -58,6 +58,7 @@ export default class ThemeManager {
// Tetris
tetrisBackground:'#d1d1d1',
tetrisBorder:'#afafaf',
tetrisScore:'#fff307',
tetrisI : '#42f1ff',
tetrisO : '#ffdd00',
tetrisT : '#ba19ff',
@ -109,6 +110,7 @@ export default class ThemeManager {
// Tetris
tetrisBackground:'#2c2c2c',
tetrisBorder:'#1b1b1b',
tetrisScore:'#e2d707',
tetrisI : '#30b3be',
tetrisO : '#c1a700',
tetrisT : '#9114c7',