In this post I want to make the game time limited. By doing this, I make it more of a challenge to beat the high score. I'm going to set the time limit for a level to 30 seconds - I think that's a good amount. Also, I don't want to just dump the player straight into the action unprepared, so and I'm going to add a couple of bits of "ceremony" before and after the gameplay to make it a nicer experience.
There are a few main things that need to be added to make this happen:
Let's start with defining these states:
The animation is pretty basic (it can easily be polished later) but it shows how I can re-use the Animator object and very little extra code to create a nice intro animation. It's also very easy for me to then use the end of that animation to trigger moving to the next state.
There are a few main things that need to be added to make this happen:
- Add "sub" states to the Game state: intro, game and outro
- Intro state needs to show a countdown
- Game state will run the game as normal, but with a visible "timer bar" to show how long is left
- Outro state will show a summary of the game - score, high score, etc.
Let's start with defining these states:
States
First of all I will define the different states. I'll use an enum for this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
GAME_STATES = makeEnum({ | |
intro: 0, | |
game: 1, | |
outro: 2, | |
exiting: 3, | |
}); |
The current state will be held in a variable inside the main Game state object:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enter() { | |
// Initalise state manager | |
this.state = GAME_STATES.intro; | |
this.stateTimer = 0; | |
// Rest of enter() code... | |
} |
..and then I'll run different update and rendering code based on the current state. Here's the update() function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
update() { | |
this.stateTimer++; | |
// State specific updates | |
switch (this.state) { | |
case GAME_STATES.intro: | |
// Update animation and exit this state when it has finished | |
this.introAnimator.update(); | |
if (this.introAnimator.getCurrentAnimationName() == null) { | |
// Next game state | |
this.state = GAME_STATES.game; | |
this.stateTimer = 0; | |
} | |
break; | |
case GAME_STATES.game: | |
// Update and collied | |
objectManager.update(); | |
objectManager.collide('player', 'bullet'); | |
objectManager.collide('player', 'scoreBullet'); | |
// Exit when state has run for long enough | |
if (this.stateTimer == config.framesPerSecond * config.level.gameLengthInSeconds) { | |
// Did we get a high score? | |
this.wasHighScore = currentScore > highScore; | |
highScore = Math.max(currentScore, highScore) | |
// Next game state | |
this.state = GAME_STATES.outro; | |
this.stateTimer = 0; | |
} | |
break; | |
case GAME_STATES.outro: | |
// Exit when state has run for long enough | |
if (this.stateTimer == config.framesPerSecond * config.level.outroLengthInSeconds) { | |
// Next game state | |
this.state = GAME_STATES.exiting; | |
this.stateTimer = 0; | |
} | |
break; | |
default: | |
logManager.error('Unexpected state: ' + this.state); | |
} | |
} |
Timer Bar
All three states show the timer bar at the bottom of the playfield. This shows as completely full during the intro state, then it drops down to empty during the game state. The code for this is in the render() function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Draw time bar | |
const width = 400; | |
const x = (config.canvasSize - width) / 2; | |
const height = 20; | |
const bottomBorder = 10; | |
const y = config.canvasSize - bottomBorder - height; | |
let alpha = 0.0; | |
switch (this.state) { | |
case GAME_STATES.intro: | |
alpha = 0.0; | |
break; | |
case GAME_STATES.game: | |
alpha = this.stateTimer / (config.framesPerSecond * config.level.gameLengthInSeconds); | |
break; | |
case GAME_STATES.outro: | |
alpha = 1.0; | |
break; | |
} | |
this.drawHorizontalBar(x, y, width, height, 2.0, 1.0 - alpha); |
Intro State
In the Intro state I draw the game objects, but don't update them. This way the player can see how the level looks but can't control it. I show a countdown over the top of the objects using an Animator object.The animation is pretty basic (it can easily be polished later) but it shows how I can re-use the Animator object and very little extra code to create a nice intro animation. It's also very easy for me to then use the end of that animation to trigger moving to the next state.
Game State
The Game state is pretty much the same as before - update game objects and render them. There's a timer that kicks us into the next state after 30 seconds of gameplay.
Outro State
Finally we come to the Outro state. This state shows a little ceremony where I display the score that the player achieved during the level and the current high score. If the player beat the current high score this time then I flash "NEW HIGH SCORE" on the screen too. The code is inside the render() function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Draw level summary | |
resourceManager.getResource('font').print(config.canvasSize / 2, config.canvasSize / 2, 'Hits: ' + hits + '\nScore: ' + currentScore + '\nHiScore: ' + highScore, FONT_XALIGN.middle, FONT_YALIGN.middle); | |
if (this.wasHighScore && this.stateTimer & 20) { | |
resourceManager.getResource('font').print(config.canvasSize / 2, config.canvasSize / 2 - 60, 'NEW HIGH SCORE', FONT_XALIGN.middle, FONT_YALIGN.middle); | |
} |
After a few seconds, the Outro state exits and we end up back at the title screen.
Wrapping Up
I added a time limit to the level and wrapped up the gameplay between an intro and an outro. I also changed a few other bits that weren't covered here:- Refactored the states so that each state is in it's own file
- Made the player flash white when they are hit. During this flash the player is also unable to score any points. You can see the code for this in player.js
- Tweaked the Zapper enemy that I added in the last post to make it a little more nasty
Next Time
I'm still heading towards getting level editing working in the game, but there are a couple of house-keeping activities that I want to get out of the way before that. I'll be working on these over the next couple of posts.
Comments
Post a Comment