Toggle Menu
Home Blog Projects Newsletter

Space defender - part 5 - Game States

Space defender game & code displayed

In the previous post we've made use of the HUD and now our game is basically done, but it's missing a few things. We need to add a game over screen with a way to restart the game. We also want to add a screen when you just started the game, instead of just starting the game right away.

Game States

We're going to add a few game states to our game. We're going to have a gameState enum that will hold the different states of the game. We'll also have a variable that holds the current state of the game.

Add the following code right at the beginning of your main.ts file:

enum gameStates {
    "PLAYING",
    "PAUSED",
    "GAME_OVER"
}
let gameState = gameStates.PAUSED;

This code creates an enum called gameStates that holds the different states of the game. We then create a variable called gameState that holds the current state of the game. We set the initial state to PAUSED.

Before we do anything with these states, we need a reset function. This function will be called right before we start the game. Add the following code to the bottom of your main.ts file:

function resetGame() {
    lives = 3;
    level = 1;
    score = 0;
    setHudValue("gameLives", lives);
    setHudValue("gameLevel", level);
    setHudValue("gameScore", score);
    Player.x = app.screen.width / 2 - Player.width / 2;
    Player.y = app.screen.height - 50;
    bullets.forEach(bullet => app.stage.removeChild(bullet));
    enemies.forEach(enemy => app.stage.removeChild(enemy));
    bullets.length = 0;
    enemies.length = 0;
    setEnemySpawnInterval();
    spawnEnemy();
}

This will set all the values to their initial starting values, clear the bullets and enemies from the stage and the arrays, reset the player's position, and set the enemy spawn interval. Then start the game by spawning an enemy.

Now that that's set, let's add a way to start the game.

Starting the game

We're going to add a new KeyHandler for that. Add the following code right after where we defined the KeyHandler for the left and right arrow keys:

KeyHandler(
    "Enter",
    () => {
        if(gameState !== gameStates.PLAYING) {
            if(gameState === gameStates.GAME_OVER) {
                resetGame();
            }
            gameState = gameStates.PLAYING;
            togglePauseText();
        }
    }
);

This code will start the game when the player presses the Enter key. If the game is not in the PLAYING state, it will set the game state to PLAYING. If the game is in the GAME_OVER state, it will reset the game and then set the game state to PLAYING.

But the player wouldn't know this, so we need some text to tell the player what to do. Add the following code right after where we defined the KeyHandlers

// A simple text style, 24px white text
const textsStyle = {
    fontSize: 24,
    fill: 0xFFFFFF
};

let startGameText = new PIXI.Text({
    text: 'Press enter to start the game',
    style: textsStyle
});

startGameText.y = 250;

function togglePauseText() {
    if(gameState === gameStates.PAUSED || gameState === gameStates.GAME_OVER) {
        // Since the text can change, we need to reposition it.
        startGameText.x = app.screen.width / 2 - startGameText.width / 2;
        app.stage.addChild(startGameText);
    } else {
        app.stage.removeChild(startGameText);
    }
}
togglePauseText();

This code creates a new text object that says "Press enter to start the game" and positions it in the middle of the screen. It then creates a function called togglePauseText that will show or hide the text based on the current game state. It then calls this function to show the text when the game is paused or over.

Now, we would also like to pause the game.

Pausing the game

Add the following code right after where we defined the KeyHandler for the left and right arrow keys:

KeyHandler(
    "Escape",
    () => {
        if(gameState !== gameStates.PAUSED) {
            gameState = gameStates.PAUSED;
            startGameText.text = 'Press enter to resume the game';
            togglePauseText();
        }
    }
);

In this code we'll set the gameState to PAUSED when the player presses the Escape key. We'll also reuse the startGameText object and set the text to "Press enter to resume the game".

Using the game states

Great, now that we can toggle between PLAYING and PAUSED states, let's actually use these states, and have the game act accordingly.

If we're not in the PLAYING state, we don't want to set the playerSpeed when the player presses the left or right arrow keys. Add the following code into the KeyHandlers where we set the playerSpeedX to 500 or -500 for the left and right arrow keys:

if (gameState !== gameStates.PLAYING) {
    return;
}

Also add the above code into the handler for the spacebar KeyHandler, so that the player can't shoot when the game is paused or over.

Next, update the if statement in the spawnEnemy function to look like this:

if(!document.hasFocus() || gameState !== gameStates.PLAYING) {
    return;
}

And finally, the whole game loop doesn't need to fire if we're paused, add the following code right at the beginning of the game loop:

if(gameState !== gameStates.PLAYING) {
    return;
}

Game Over

Now that we have a way to start and pause the game, let's add a game over screen.

Where now we have a console.log("Game Over"), we want to set the game state to GAME_OVER and show a game over text. Replace the console.log with the following code:

gameState = gameStates.GAME_OVER;
startGameText.text = 'Press enter to restart the game';
scoreText.text = `Score: ${score}`;
scoreText.x = app.screen.width / 2 - scoreText.width / 2;
app.stage.addChild(gameOverText);
togglePauseText();
app.stage.addChild(scoreText);

This code is using two more text objects that we haven't created that yet, lets do that.

Right after where we created the startGameText object, add the following code:

let gameOverText = new PIXI.Text({
    text: 'GAME OVER',
    style: textsStyle
});

gameOverText.x = app.screen.width / 2 - gameOverText.width / 2;
gameOverText.y = 200;

let scoreText = new PIXI.Text({
    text: 'Score: 0',
    style: textsStyle
});

scoreText.y = 300;

Now, we should be able to play the game, pause it, and restart it when we lose.

Conclusion

And that's it! You can now play the game, pause it, and restart it when you lose.

I am aware that this last part might have been a bit overwhelming, but I hope you managed to follow along and understand everything, don't forget you can always check out the full source code. If you have any questions or feedback, feel free to reach out to me on X


And this is the final part of this small series, I hope you enjoyed it and learned something new. A small thing I'd like to point out while leaving you with this, is that this is a very basic game, and there are many ways to improve it. You could add more enemies, power-ups, different levels, and so on. I encourage you to experiment and try new things, that's how you learn and grow as a developer.

Also, the way this game is structured is not the best way to structure a game, but for the purpose of this tutorial, I wanted to keep it simple and in one file. In a future tutorial, I might show you how to structure a game in a more scalable way.

Thank you for reading all the way through this tutorial!

Recent articles

Make a simple game using PixiJS and TypeScriptPixiJS setup with Vite and TypeScriptHow Your Coding Skills Can Make a Real Difference in the WorldA guide to writing clean and readable codeAccomplish more with the "Cult of Done"Host your web app for free on Cloudflare PagesFinally got around to making a website for myself