More data state!

In the last class, we filled in some more of our running game. The snake now can move around the grid, although it never dies, and never eats any apples. That stuff we are going to fix next class. But in the meantime, we need to actually store what it means to be "playing" and also "game over" and "in menu".

If you remember from last year in the Rock Paper Scissors game, we used a state machine to figure out what to do in what situation. We had a switch over all the states, and depending on the state, we did different things (and allowed different things). We need a similar thing for this snake game, but it's not nearly as complicated (mostly due to not dealing with the network updates). A State machine, if you recall, is a set of states, with a way to get from one state to the next one.

First, the game must start out not "playing". Almost all games you start up, you get a start menu, then you select what you want to do. Johann suggested we have a mechanism to set the difficulty, which really will say how fast the snake is running. We also might set other options using the menu.

So we need to design our state machine here. We will have 3 states, that we will store as an enum as follows:

enum Mode {
   menu,
   playing,
   gameOver
}

We'll put this inside the GameState, which already is gathering quite a bit of stuff! We not only need to declare the State type, we need to declare that we need one of those types as part of the state:

Mode state;

And we should initialize the state inside the setup function:

void setup(int width, int height) {
    rightWall = width;
    bottomWall = height;
    player.x = rightWall / 2;
    player.y = bottomWall / 2;
    player.direction = Dir.up;

    state = Mode.menu;
}

Moving the state

In state machines, many times the things that move state from one thing to the next are different depending on the state. That is, what changes the state depends on what the current state is. In our game, moving from the menu to playing the game is going to be a key press. But moving from the playing state to gameOver is not a key press but running into a wall or tail piece.

What is the big difference here? The big difference is that pressing a key is part of input processing, while hitting a wall is part of the move function. These are both acceptable phases of the game to change state. The one place where you should not be changing any state is during rendering (all the code between BeginDrawing() and EndDrawing().

I recommend that we use a switch statement for handling inputs, since the game state is going to determine which keys are examined, and even whether we want to process them with IsKeyDown or IsKeyPressed.

So let's start by defining a handleInputs() method inside the GameState struct. Instead of all the code we have inside our loop, we will just call this function, and it will handle the inputs depending on the game state:

    void handleInputs() {
        switch(state)
        {
            case Mode.playing:
                if(IsKeyDown(KeyboardKey.KEY_LEFT)) {
                    setDirection(Dir.left);
                }
                if(IsKeyDown(KeyboardKey.KEY_UP)) {
                    setDirection(Dir.up);
                }
                if(IsKeyDown(KeyboardKey.KEY_RIGHT)) {
                    setDirection(Dir.right);
                }
                if(IsKeyDown(KeyboardKey.KEY_DOWN)) {
                    setDirection(Dir.down);
                }
                break;
            case Mode.menu:
                break;
            case Mode.gameOver:
                break;
            default: break;
        }
    }

Notice that this code is the code we had back in the game loop, but we've now moved it into the state struct (this is where it belongs anyway). And don't forget that we need to call it instead!

   while(!WindowShouldClose())
    {
        // get inputs (this used to be where all that code was)
        gameState.handleInputs();

        // update game state
        ++frames;
        if(frames % 60 == 0)
        {
            gameState.move();
        }

        // draw game state
        BeginDrawing();
        ...

If you run this code, you will notice that moving the snake using the arrow keys doesn't actually work any more! Why not? Because the default game state is Mode.menu. We haven't written that part yet, so there is currently no way to get from the menu to the playing state.

Let's say the key to begin playing is 'P'. How should we handle this code? This is the part you are going to write. Here is a complete file that has all the code we have so far. You need to write the part that follows "INSERT CODE HERE". which is where you should handle the menu key presses.

Note that:

If you are having trouble, please ask on discord. We will go over this homework at the start of the next class

Starting code:

import std.stdio;
import raylib;

enum Dir
{
    left,
    up,
    right,
    down
}

struct Snake
{
    int x;
    int y;
    int apples; // apples eaten
    Dir direction;
    void draw(int squaresize)
    {
        Vector2 pos = Vector2(x * squaresize, y * squaresize);
        DrawRectangleV(pos, Vector2(squaresize, squaresize), Colors.GREEN);

        int pos1 = squaresize / 4;
        int pos2 = squaresize - pos1;
        // draw the eyes
        Vector2 eye1;
        Vector2 eye2;
        switch(direction)
        {
            case Dir.up:
                eye1 = Vector2(pos1, pos1);
                eye2 = Vector2(pos2, pos1);
                break;
            case Dir.down:
                eye1 = Vector2(pos1, pos2);
                eye2 = Vector2(pos2, pos2);
                break;
            case Dir.left:
                eye1 = Vector2(pos1, pos1);
                eye2 = Vector2(pos1, pos2);
                break;
            case Dir.right:
                eye1 = Vector2(pos2, pos1);
                eye2 = Vector2(pos2, pos2);
                break;
            default: break;
        }
        DrawCircleV(pos + eye1, squaresize / 10, Colors.BLACK);
        DrawCircleV(pos + eye2, squaresize / 10, Colors.BLACK);

    }
}

struct Apple
{
    int x;
    int y;
    string appleType;
    int mass;
    void draw(int ss)
    {
        DrawCircle(x * ss + ss/2, y * ss + ss/2, mass * 5, Colors.RED);
    }
}

struct TailPiece
{
    int x;
    int y;
    void draw(int squaresize)
    {
        DrawRectangle(x * squaresize, y * squaresize, squaresize, squaresize, Colors.GREEN);
    }
}

struct GameState {
    Snake player;
    Dir forbidden;
    TailPiece[] tail;
    Apple[] apples;
    int rightWall;
    int bottomWall;

    enum Mode {
        menu,
        playing,
        gameOver
    }

    Mode state;

    void setup(int width, int height) {
        rightWall = width;
        bottomWall = height;
        player.x = rightWall / 2;
        player.y = bottomWall / 2;
        player.direction = Dir.up;

        state = Mode.menu;
    }
    void handleInputs() {
        switch(state)
        {
            case Mode.playing:
                if(IsKeyDown(KeyboardKey.KEY_LEFT)) {
                    setDirection(Dir.left);
                }
                if(IsKeyDown(KeyboardKey.KEY_UP)) {
                    setDirection(Dir.up);
                }
                if(IsKeyDown(KeyboardKey.KEY_RIGHT)) {
                    setDirection(Dir.right);
                }
                if(IsKeyDown(KeyboardKey.KEY_DOWN)) {
                    setDirection(Dir.down);
                }
                break;
            case Mode.menu:
                if(IsKeyPressed(KeyboardKey.KEY_P))
                {
                    // INSERT CODE HERE
                }
                break;
            case Mode.gameOver:
                break;
            default: break;
        }
    }
    void move() {
        TailPiece last = TailPiece(player.x, player.y);
        foreach(ref piece; tail)
        {
            TailPiece tmp = piece;
            piece = last;
            last = tmp;
        }
        switch(player.direction)
        {
            case Dir.up:
            player.y = player.y - 1;
            forbidden = Dir.down;
            break;
            case Dir.down:
            player.y = player.y + 1;
            forbidden = Dir.up;
            break;
            case Dir.left:
            player.x = player.x - 1;
            forbidden = Dir.right;
            break;
            case Dir.right:
            player.x = player.x + 1;
            forbidden = Dir.left;
            break;
            default: break;
        }

    }

    void setDirection(Dir newDirection) {
        if(newDirection != forbidden)
        {
            player.direction = newDirection;
        }
    }
}

void main()
{
    // setup
    InitWindow(500, 500, "Snake");
    GameState gameState;
    gameState.setup(10, 10);

    SetTargetFPS(60);
    int frames;
    while(!WindowShouldClose())
    {
        // get inputs
        gameState.handleInputs();

        // update game state
        ++frames;
        if(frames % 60 == 0)
        {
            gameState.move();
        }

        // draw game state
        BeginDrawing();
        ClearBackground(Colors.WHITE);
        foreach(i; 0 .. 10)
        {
            DrawLine(50 * i, 0, 50 * i, 500, Colors.GRAY);
            DrawLine(0, 50 * i, 500, 50 * i, Colors.GRAY);
        }

        // draw snake head
        gameState.player.draw(50);
        foreach(tailpiece; gameState.tail)
        {
            tailpiece.draw(50);
        }

        foreach(apple; gameState.apples)
        {
            apple.draw(50);
        }

        EndDrawing();
    }
}

Bonus question

You may note that before we start playing using the P key, the snake moves without being controllable during the menu state. Why is that? Is there anything you can do to make the snake sit on the screen without moving?

©2021 Steven Schveighoffer