This is the fifth part of a series (that began here).
In the last post, we added the concept of score. The car now can collect cups while avoiding trees; however, we don’t have any concept of what happens when there are no cups left.
In this post, we’ll add levels to the game, so that when you’ve collected all the cups, you move up. We’ll also introduce a time limit to make it progressively harder (as it currently stands, it’s not much of a challenge to collect the cups because you can take all day).
The source for this post is here. Again, not everything is in the post, so please refer to the repository.
Levels
Because we are creating levels, we’ll need to track the level that we’re on, so a new state property is in order:
this.state = {
playerX: 100,
playerY: 100,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
playerMomentum: 0,
playerRotation: 0,
playerVelocityX: 0,
playerVelocityY: 0,
playerLives: 3,
playerCrashed: false,
gameLoopActive: false,
message: "",
score: 0,
level: 1,
cupCount: 1,
remainingTime: 0
};
If you’ve followed this through from this first post, you may be asking yourself: “Is he ever going to refactor and clean this up!?”
To which I confidently respond:
“Probably!”
Anyway, you’ll notice that we have the level, the score, the time and the cup count. Advancing through the levels is conceptually just a number; here’s the code that completes a level:
completedLevel() {
if (this.state.level >= 10) {
this.updateMessage("Congratulations, you've completed the game");
}
this.startLevel(this.state.level + 1);
}
startLevel is a slight refactor, which essentially sets the cup count and level to the new value - we’ll come back to that shortly.
You can only complete a level by collecting enough cups, so the trigger should be in the cup collection:
collectedCup(key) {
this.setState({
score: this.state.score + 1
});
this.cups = this.cups.filter(cup => cup.key != key);
this.updateMessage("Collected cup");
if (this.cups.length == 0) {
this.completedLevel();
}
}
As soon as we’re down to 0 cups, we call completedLevel.
Time
Now it’s time to have a look at the startLevel code:
startLevel(level) {
this.setState({
level: level,
cupCount: level \* 2
});
this.obstacles = this.buildObstacles();
this.cups = this.placeCups();
this.resetCarPosition();
this.totalLevelTimeMS = (this.TOPLEVEL - (this.state.level - 1)) \* 60 \* 1000
let startLevelTimeMS = (new Date()).getTime();
this.endLevelTimeMS = startLevelTimeMS + this.totalLevelTimeMS;
}
We’re working out when the user is out of time, and storing that in endLevelTime. Note that none of these are in state variables - the only state variable is in updated in the game loop:
let remaining = (this.endLevelTimeMS - (new Date()).getTime()) / 1000;
if (remaining <= 0) {
this.updateMessage("Out of time!");
this.playerDies();
}
this.setState({
remainingTime: Math.round(remaining)
});
This is at the end of the game loop: we’re updating the remainingTime state variable, but first, we calculate it and, if it’s zero, the player dies (loses a life).
We need to tweak the code for the player dying, because otherwise the timer will never get reset:
playerDies() {
this.setState({
playerLives: this.state.playerLives - 1,
gameLoopActive: false
});
if (this.state.playerLives <= 0) {
this.initiateNewGame();
} else {
this.startLevel(this.state.level);
}
this.repositionPlayer();
this.setState({
playerCrashed: false,
gameLoopActive: true
});
}
The last part is to make the time look a bit better with another of my patented icons. GameStatus.jsx should now return the following:
return (
<div className="flex-container" style={flexStyle}>
<label style={labelStyle}>
Lives Remaining: {props.Lives}
</label>
<label style={labelStyle}>
Score: {props.Score}
</label>
<label style={labelStyle}>
Level: {props.Level}
</label>
<div style={containerStyle}>
<img src={clockImg} style={imgStyle} />
<div style={textDivStyle}>{props.RemainingTime}</div>
</div>
<label style={labelStyle}>
{props.Message}
</label>
</div>
);
There are some new styles here so that the time appears over the clock icon:
const containerStyle = {
position: 'relative',
textAlign: 'center',
color: 'red'
}
const textDivStyle = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 1,
fontWeight: 'bold'
}
const imgStyle = {
width: '100%',
zIndex: 0
}
In the next part, we’ll implement a high score table.