Pages

Saturday, September 6, 2014

[Minix][Tutorial 9] Creating a state machine and a main menu

In this tutorial we are going to create the main menu for our game!

Adding a path builder

First, get menu-background-0x114.bmp, export it as a .bmp with GIMP like I have explained in the previous tutorial and paste it in your res/images folder. You can delete the test.bmp from the previous tutorial.



Think of every time we are going to load an image. Isn't it boring to hard code the entire image path? What if in the future we decide to change the images folder location? Man, what a mess to have to change every single hard coded image path! Wouldn't it be better to have a function that given the image name, would build the entire image path? Yes it would! So, let's do it!

Add the getImagePath function to Utilities, just like shown below.



Now edit line 24 in FlappyNix.c, we should now be able to load images like this. Pretty useful, right?



Compile, install and run the program. This is how it looks like now:



Implementing a state machine

We've got to a point where we will start dealing with states. Every program/game has different states: main menu state, play state, pause state, game over state, etc. In order to manage each state correctly the state machine was invented. It is responsible for state initialization, update, drawing and deletion. So, let's make a few changes and implement our own state machine.

Creating the main menu state

First, let's create the main menu state structure.

Create MainMenuState.c and MainMenuState.h and do not forget to declare it in the Makefile.



To simplify button representation, we will need a Rectangle class. Declare it in the Makefile as well. Here is how it looks, it is pretty simple:



Now let's implement the main menu. Here is MainMenuState.h:



Long story short: there is an integer to tell if the state is complete/done; a background image; two buttons - play and exit - each with an integer that tells if the mouse is hovering them and a Rectangle representing their boundaries; an integer representing the action performed when the state became done - either play or exit could have been pressed.

Below is the implementation of the main menu. Paste it to MainMenuState.c:
#include "MainMenuState.h"

#include "Graphics.h"
#include "Keyboard.h"
#include "Mouse.h"
#include "Utilities.h"

MainMenuState* newMainMenuState() {
    MainMenuState* state = (MainMenuState*) malloc(sizeof(MainMenuState));

    state->done = 0;
    state->background = loadBitmap(getImagePath("menu-background"));

    // these numbers are just meant to create the buttons boundaries
    double w = .075, hi = .44, hf = hi + .12;
    int x1 = getHorResolution() / 2 - getHorResolution() * w;
    int x2 = getHorResolution() / 2 + getHorResolution() * w;
    int y1 = getVerResolution() * hi;
    int y2 = getVerResolution() * hf;
    state->playButton = newRectangle(x1, y1, x2, y2);
    state->mouseOnPlay = 0;

    hi = .64, hf = hi + .12;
    y1 = getVerResolution() * hi;
    y2 = getVerResolution() * hf;
    state->exitButton = newRectangle(x1, y1, x2, y2);
    state->mouseOnExit = 0;

    return state;
}

int updateMainMenuState(MainMenuState* state, unsigned long scancode) {
    int draw = 0;

    // if ESC has been pressed, quit
    if (scancode == KEY_DOWN(KEY_ESC)) {
        state->action = 1;
        state->done = 1;
    }

    // if mouse is inside the play button rectangle (boundaries)
    if (mouseInsideRect(state->playButton))
        state->mouseOnPlay = 1;
    else
        state->mouseOnPlay = 0;

    // if mouse is inside the exit button rectangle (boundaries)
    if (mouseInsideRect(state->exitButton)) {
        state->mouseOnExit = 1;

        // and left mouse button has been released
        if (getMouse()->leftButtonReleased) {
            state->action = 1;
            state->done = 1;
        }
    } else
        state->mouseOnExit = 0;

    return draw;
}

void drawMainMenuState(MainMenuState* state) {
    drawBitmap(state->background, 0, 0, ALIGN_LEFT);

    if (state->mouseOnPlay)
        drawRect(state->playButton, YELLOW);
    else if (state->mouseOnExit)
        drawRect(state->exitButton, YELLOW);
}

void deleteMainMenuState(MainMenuState* state) {
    deleteBitmap(state->background);
    deleteRectangle(state->playButton);
    deleteRectangle(state->exitButton);

    free(state);
}

Contained in the above code snippet are function calls like: mouseInsideRect(state->exitButton) and drawRect(state->playButton, YELLOW)

The first is implemented in Mouse.c:



The second is implemented in Graphics.c:



Ok, now that we have implemented the main menu state, we need to make a lot of changes to FlappyNix and transform it in a state machine.

Modifying FlappyNix to implement a state machine


This is how FlappyNix.h looks like now:



We have deleted the test image, as well as our previous yellow rectangle coordinate. Make sure you get rid of anything directly associated to those things from FlappyNix.c as well.

We now have a void* state; which is a generic pointer to the current state of our program. What is a generic pointer? If you have just read the information on that link you might now know that, since there are different states - game over, main menu, etc - but they all fit under the "state" category, we can represent an abstract state with a void pointer/generic pointer. That state can either become the main menu state or the game over state, get it?

Since there is no way to know the type of that state - because it is just a void pointer - I have added an enumerator that indicates which state is the current one. Every time the state changes, we will update this currentState indicator - this will enable us to call the right methods on the right state and not make any stupid mistakes like calling drawGameOverState() when the active state is MainMenuState. I hope this was not too confusing and you understood everything.

Since I have made a lot of changes to FlappyNix.c I am going to paste the whole source file here and try to explain it after.
#include <minix/drivers.h>
#include "FlappyNix.h"

#include "Graphics.h"
#include "Keyboard.h"
#include "Mouse.h"

#include "MainMenuState.h"

const int FPS = 25;
const int mouseFPSmult = 3;

void checkIfStateIsDone(FlappyNix* game);
void deleteCurrentState(FlappyNix* game);

FlappyNix* startFlappyNix() {
    FlappyNix* flappy = (FlappyNix*) malloc(sizeof(FlappyNix));

    // subscribing devices
    flappy->IRQ_SET_KB = kb_subscribe_int();
    flappy->IRQ_SET_MOUSE = subscribeMouse();
    flappy->IRQ_SET_TIMER = subscribeTimer();

    // resetting timer frequency
    timerSetSquare(0, mouseFPSmult * FPS);

    // initializing other variables
    flappy->scancode = 0;
    flappy->currentState = MAIN_MENU_STATE;
    flappy->state = newMainMenuState();

    // finish initialization
    flappy->done = 0, flappy->draw = 1;
    flappy->timer = newTimer();

    return flappy;
}

void updateFlappyNix(FlappyNix* flappy) {
    int ipc_status, r = 0;
    message msg;

    resetTimerTickedFlag(flappy->timer);

    if (driver_receive(ANY, &msg, &ipc_status) != 0)
        return;

    if (is_ipc_notify(ipc_status)) {
        switch (_ENDPOINT_P(msg.m_source)) {
        case HARDWARE:
            // KEYBOARD interruption
            if (msg.NOTIFY_ARG & flappy->IRQ_SET_KB)
                flappy->scancode = readKBCState();

            // TIMER interruption
            if (msg.NOTIFY_ARG & flappy->IRQ_SET_TIMER)
                timerHandler(flappy->timer);

            // MOUSE interruption
            if (msg.NOTIFY_ARG & flappy->IRQ_SET_MOUSE)
                updateMouse();
            break;
        default:
            break;
        }
    }

    if (flappy->timer->ticked) {
        getMouse()->draw = 1;

        if (flappy->timer->counter % mouseFPSmult == 0) {
            // update at 25 FPS
            switch (flappy->currentState) {
            case MAIN_MENU_STATE:
                updateMainMenuState(flappy->state, flappy->scancode);
                break;
            default:
                break;
            }

            flappy->scancode = 0;
            flappy->draw = 1;
        }
    }

    checkIfStateIsDone(flappy);
}

void drawFlappyNix(FlappyNix* flappy) {
    switch (flappy->currentState) {
    case MAIN_MENU_STATE:
        drawMainMenuState(flappy->state);
        break;
    default:
        break;
    }
}

void stopFlappyNix(FlappyNix* flappy) {
    deleteCurrentState(flappy);
    deleteMouse();
    deleteTimer(flappy->timer);

    // unsubscribe devices
    kb_unsubscribe_int();
    unsubscribeMouse();
    unsubscribeTimer();

    free(flappy);
}

void changeState(FlappyNix* game, State newState) {
    // deleting current state
    deleteCurrentState(game);

    // changing current state
    game->currentState = newState;

    // creating new state
    switch (game->currentState) {
    case MAIN_MENU_STATE:
        game->state = newMainMenuState();
        break;
    }

    game->draw = 1;
}

void checkIfStateIsDone(FlappyNix* game) {
    switch (game->currentState) {
    case MAIN_MENU_STATE:
        if (((MainMenuState*) (game->state))->done) {
            int action = ((MainMenuState*) (game->state))->action;

            switch (action) {
            case PLAY_CHOSEN:
                game->done = 1;
                break;
            case EXIT_CHOSEN:
                game->done = 1;
                break;
            }
        }
        break;
    default:
        break;
    }
}

void deleteCurrentState(FlappyNix* game) {
    switch (game->currentState) {
    case MAIN_MENU_STATE:
        deleteMainMenuState(game->state);
        break;
    }
}

So, I will try to explain what is going on...

The prototypes at lines 13 and 14 are there just for visibility purposes. Their actual implementation is near the bottom of the file, but they are called throughout the file inside other functions, so in order to make them visible for the whole file, I have placed their declarations at lines 13 and 14.

startFlappyNix

The previous yellow rectangle related stuff was removed.
The current state and the actual state pointer initialization has been added.

updateFlappyNix and drawFlappyNix

Not much has changed, we now have a switch dependent on the current state enumerator. Again, this enables us to call the right methods for the currently active state. I have explained this just a couple of lines above.

changeState

This method is called when we want to change from one state to another.

For example:
In the next tutorial we will be adding the play state. When we implement it and then press the play button on the main menu, this method will be called and what will happen is the following:
the active state (in this case, main menu state) will be deleted, the current state identifier will be updated and then, according to this identifier, the new state will be created. Since we pressed the play button, the play state is expected to be generated.

checkIfStateIsDone

This method is called right at the end of the update method. It checks if the active state is done/finished.
If it is, depending on the active state, a certain action is performed: the program can be terminated or there can take place a change of state. And yes, if you are wondering if this is the only place where changeState() - the method previously described - can (or should) be called, you are indeed correct.

deleteCurrentState

This one is easy: according to the current state identifier, the correct delete method for the active state is called.

Main menu preview

So that's it! Phew, this was a long post! Here is a quick preview of what your program should look like, in particular the main menu and it's buttons being hovered:

Back to index

Click here to go back to the index post.

No comments:

Post a Comment