George Sykes ELEC2645 project

Dependencies:   mbed

https://os.mbed.com/media/uploads/el18gs/pixil-frame-0.png

GHOST HUNTER

In a world of ghostly horrors there is much money to be made in underground ghost fighting rings. You've managed to get hold of a Ghostbuster, a special piece of equipment that allows you to catch, train and fight ghosts.

Instructions

Below you will find the instructions for the game. Please note that due to COVID-19 a large part of the game (fighting ghosts) could not be added as it would have required access to a second gamepad which i could not acquire.

Welcome screen

When first started you will be presented with a welcome screen

  • Pot 1 to adjust the contrast on the screen
  • Press A to continue.

Main menu

You have three options, catch ghosts (add ghosts to your inventory), inventory (sell ghosts) or settings(adjust the games settings).

  • Press X and B to move the selection up and down respectively
  • Press A to enter the selected submenu

Catch Ghost

Will now be presented with two challenges. In the first you need to find a ghost, in the second you catch it. Theses stages will start automatically.

Find ghost

Rotate the gamepad on its roll and pitch axis until all the LED's turn on. The ones on the left indicate roll and the right pitch.

  • Rotate the gamepad on it roll and pitch to light up the LED's

Catch ghost

Return the gamepad to a comfortable position and use the joystick to move the crosshairs onto the ghost sprite. When ready press the A button to catch the ghost. You will be told what kind of ghost you have captured and it will be added to your inventory.

  • Press A to catch the ghost
  • Move the joystick to move the crosshairs

Inventory

The inventory allows you to view your ghosts and sell them.

  • Use Pot 1 to scroll through the ghosts
  • Pot 2 to scroll up and down the details of the individual ghosts
  • Press X to prepare to sell a ghost and press again to confirm, if you don't press again the sale screen will disappear after 5 seconds
  • Press Start to return to the main menu

Settings

This menu allows you to adjust some of the settings of the game.

  • Press X to go up one option
  • Press B to go down one option
  • Press A to enter the selected submenu
  • Press Start to return to the main menu

Contrast

Set the contrast of the LCD screen, the contrast will adjust on this screen so you can see the effect (contrast is bounded between 0.4 and 0.6).

  • Pot 1 to increase or decrease the contrast
  • Press A to set the contrast

Button Delay

Set the minimum time between button presses; if this is too low the game will detect two button presses when there was only one, too high and the buttons will seem unresponsive. So as to ensure these issues do not occur while changing the setting button X temporarily operates on the new delay but none of the others will until A is pressed.

  • Pot 1 to increase or decrease the delay
  • Press X to test the new delay, this will toggle the small circle to be filled in or unfilled
  • Press A to save the setting

gameEngine/gameEngine.cpp

Committer:
el18gs
Date:
2020-05-26
Revision:
17:3ebcf7bba112
Parent:
15:598baed15751

File content as of revision 17:3ebcf7bba112:

#include "gameEngine.h"

gameEngine::gameEngine(SDFileSystem &sd, Gamepad &pad, N5110 &lcd)
{
#ifdef DEBUG
    printf("Initialising game\n");
#endif

#ifdef DEBUG
    printf("Intialising gamepad\n");
#endif
    pad.init();

#ifdef DEBUG
    printf("Mounting SD card\n");
#endif
    sd.mount(); // Mount the SD card

#ifdef DEBUG
    printf("Initialising LCD\n");
#endif
    lcd.init(); // Initialise the screen
    lcd.backLightOn(); // Turn the LCD backlight on
}

void gameEngine::welcome(SDFileSystem &sd, N5110 &lcd, Gamepad &pad, volatile int &g_buttonA_flag)
{
#ifdef DEBUG
    printf("Entered welcome screen\n");
#endif
    int** welcome = import_sprite("/sd/assets/welcome.sprite", sd); // importing welcome sprite
    lcd.clear(); // clear the LCD and display the sprite
    for(int i = 0; i < 48; i++) {
        for(int j = 0; j < 84; j++) {
            lcd.setPixel(j,i, welcome[i][j]);
        }
    }

    while(!g_buttonA_flag) {
        // User can set contrast before continuing, contrast should be bounded between 4 and 6
        lcd.setContrast(0.4 + ((double) pad.read_pot1()/5));
        lcd.refresh();
        wait_ms(10);
    }
    g_buttonA_flag = 0; // Once the flags been detected set it back to 0
}


int gameEngine::game_menu(N5110 &lcd, volatile int &g_buttonA_flag, volatile int &g_buttonB_flag, volatile int &g_buttonX_flag)
{
#ifdef DEBUG
    printf("entered game menu\n");
#endif
    g_buttonA_flag = 0, g_buttonB_flag = 0, g_buttonX_flag = 0;
    int current_state = 0; // Set the current selected option to Catch Ghosts

    while(1) {
        lcd.clear();
        lcd.printString("Catch Ghosts",0,0);
        lcd.printString("Inventory",0,2);
        lcd.printString("Settings",0,4);

        if(g_buttonA_flag) {
            g_buttonA_flag = 0;
            return current_state;
        }

        if(g_buttonX_flag) {
            current_state = mainMenuFsm[current_state].nextState[0];
        } else if(g_buttonB_flag) {
            current_state = mainMenuFsm[current_state].nextState[1];
        }
        g_buttonA_flag = 0, g_buttonB_flag = 0, g_buttonX_flag = 0;

        switch (mainMenuFsm[current_state].output) {
            case 0:
                lcd.drawCircle(79, 3, 3, FILL_BLACK);
                break;
            case 2:
                lcd.drawCircle(79, 20, 3, FILL_BLACK);
                break;
            case 4:
                lcd.drawCircle(79, 35, 3, FILL_BLACK);
                break;
        }
        lcd.refresh();
    }
}

void gameEngine::catch_ghosts(Inventory &inventory, FX0S8700CQ &accel, SDFileSystem &sd, N5110 &lcd, Gamepad &pad, volatile int &g_buttonA_flag, BusOut &left_LED, BusOut &right_LED)
{
    ghostCatchTrial(accel, lcd, sd, pad,left_LED, right_LED, g_buttonA_flag);

    Ghost caught_ghost(rand()%100, rand()%20, "/ghosts/", sd);
    caught_ghost.save(sd);

    lcd.clear();
    lcd.printString("You caught:", 0, 0);
    lcd.printString(caught_ghost.get_type_string().c_str(), 0, 1);
    lcd.printString("Press A to", 0, 2);
    lcd.printString("continue", 0, 3);
    lcd.refresh();

    g_buttonA_flag = 0;
    while(!g_buttonA_flag) {
    }
    g_buttonA_flag = 0;

    inventory.regen(sd);
}

void gameEngine::ghostCatchTrial(FX0S8700CQ &accel, N5110 &lcd, SDFileSystem &sd, Gamepad &pad, BusOut &left_LED, BusOut &right_LED, volatile int &g_buttonA_flag)
{
    // Display instructions
    lcd.clear();
    lcd.printString("Align the",18,0);
    lcd.printString("Gamepad until",4,1);
    lcd.printString("all LEDs",20,2);
    lcd.printString("turn on",22,3);
    lcd.refresh();

    // Detect whn the pad is in the right orientation
    angleDetectionTechnicalSub(accel, left_LED, right_LED);

    // Import the ghost and generate its starting location
    Vector2D ideal = {rand() % 85,rand() % 49};
    int** ghost = import_sprite("/sd/assets/ghost.sprite", sd);

    // Loop until the ghost has been caught
    while(1) {
        if (drawCrosshairs(ideal, ghost, lcd, pad, g_buttonA_flag)) {
            return;
        } else {
            randomMove(ideal);
        }
    }
}

// Sub routine that manages the technical side of the angle detection part of ghost catching
void gameEngine::angleDetectionTechnicalSub(FX0S8700CQ &accel, BusOut &left_LED, BusOut &right_LED)
{
    // Generate a random target angle and set the counter to 0
    float pitch = rand() % 46;
    if(rand() & 1) {
        pitch = pitch * -1;
    }
    float roll = rand() % 46;
    if(rand() & 1) {
        roll = roll * -1;
    }
    int counter = 0;

    while(counter < 250) { // loop forever (until intentionally broken)
        accel.read_data(); // Read data from the accelerometer and assume the pad's off
        bool focused = true;

        // Check if the pad is pointing in the right direction, light up LEDs to indicate proximity
        if((double)accel.sensor.roll/roll > 0.75) {
            left_LED = 0b000;
        } else if ((double)accel.sensor.roll/roll > 0.3) {
            left_LED = 0b001;
            focused = false;
        } else {
            left_LED = 0b011;
            focused = false;
        }
        if((double)accel.sensor.pitch/pitch > 0.75) {
            right_LED = 0b000;
        } else if ((double)accel.sensor.pitch/pitch > 0.3) {
            right_LED = 0b001;
            focused = false;
        } else {
            right_LED = 0b011;
            focused = false;
        }

        // Increment the counter
        if(focused) {
            counter++;
        }
        wait_ms(10);
    }
}

bool gameEngine::drawCrosshairs(Vector2D ideal, int** ghost, N5110 &lcd, Gamepad &pad, volatile int g_buttonA_flag)
{
    lcd.clear(); // Clear the LCD
    // Get the position of the joystick
    Vector2D actual = {cursor_transform(1, -1, 84, 0, pad.get_coord().x),
                       cursor_transform(1, -1, 48, 0, pad.get_coord().y * -1)
                      };
    
    printf("X: %f, Y: %f\n", pad.get_coord().x, pad.get_coord().y);                  
    
    // Draw the crosshairs (x0 y0 x1 y1 type)
    lcd.drawLine(actual.x, 0, actual.x, 48, 2);
    lcd.drawLine(0, actual.y, 84, actual.y, 2);

    int col = 0;
    for(int i = ideal.x - 8; i < ideal.x + 8; i++) { // Iterate Columns: x
        int row = 0;
        for(int j = ideal.y - 9; j < ideal.y + 9; j++) { // Iterate Rows: y
            lcd.setPixel(i,j, ghost[row][col]);
            row++;
        }
        col++;
    }

    if(ghostHit(ideal.x, ideal.y, actual.x, actual.y)) {
        lcd.printString("Press A!",0,0);
        if(g_buttonA_flag) {
            g_buttonA_flag = 0;
            return true;
        }
    }
    
    g_buttonA_flag = 0;
    lcd.refresh();
    return false;
}

void gameEngine::randomMove(Vector2D &ideal)
{
    int x_move = 0;
    int y_move = 0;
    if(rand() & 1) {
        x_move = 1;
    } else {
        x_move = -1;
    }
    if(rand() & 1) {
        y_move = 1;
    } else {
        y_move = -1;
    }

    ideal.x = ideal.x + x_move;
    ideal.y = ideal.y + y_move;

    if(ideal.x > 70) {
        ideal.x = 70;
    } else if(ideal.x < 13) {
        ideal.x = 13;
    }
    if(ideal.y > 30) {
        ideal.y = 30;
    } else if(ideal.y < 12) {
        ideal.y = 12;
    }
}

bool gameEngine::ghostHit( int xGhost, int yGhost, int xJoy, int yJoy)
{
    int xDifference = abs(xGhost - xJoy);
    int yDifference = abs(yGhost - yJoy);

    if(xDifference < 10 && yDifference <10) {
        return true;
    } else {
        return false;
    }
}

void gameEngine::settings(N5110 &lcd, Gamepad &pad, volatile int &g_buttonA_flag, volatile int &g_buttonStart_flag, volatile int &g_buttonX_flag, volatile int &g_buttonB_flag,
                          volatile bool &g_buttonTesting,
                          volatile int &g_buttonSensitivityTest,
                          volatile int &g_buttonSensitivity)
{
#ifdef DEBUG
    printf("entered game menu\n");
#endif
    // Set the default state to 0 and zero all the interrupt flags
    int current_state = 0;
    g_buttonA_flag = 0, g_buttonStart_flag = 0, g_buttonX_flag = 0, g_buttonB_flag = 0;

    // Loop until one of the sub menus is entered
    while(1) {
        lcd.clear();                            // Clear the display and print the options
        lcd.printString("Settings",0,0);
        lcd.printString("Contrast",0,2);
        lcd.printString("Button delay",0,4);

        // Decide what to do depending on what flags are high, zero all flags after
        if(g_buttonA_flag) {                                    // If the A enter the selected sub menu
            g_buttonA_flag = 0;
            switch (settingsFsm[current_state].output) {
                case 2:
                    adjustContrast(lcd, pad, g_buttonA_flag);
                    break;
                case 4:
                    buttonDelay(lcd, pad, g_buttonA_flag, g_buttonX_flag, g_buttonTesting, g_buttonSensitivityTest, g_buttonSensitivity);
                    break;
            }
        } else if (g_buttonStart_flag) {                        // If Start return to the main menu
            g_buttonStart_flag = 0;
            return;
        } else if(g_buttonX_flag) {                             // If X go up one option
            current_state = settingsFsm[current_state].nextState[0];
        } else if(g_buttonB_flag) {                             // If B go down one option
            current_state = settingsFsm[current_state].nextState[1];
        }
        g_buttonA_flag = 0, g_buttonStart_flag = 0, g_buttonX_flag = 0, g_buttonB_flag = 0;

        // Display a circle on the correct line
        switch (settingsFsm[current_state].output) {
            case 2:
                lcd.drawCircle(79, 20, 3, FILL_BLACK);
                break;
            case 4:
                lcd.drawCircle(79, 35, 3, FILL_BLACK);
                break;
        }
        lcd.refresh();
    }
}

void gameEngine::adjustContrast(N5110 &lcd, Gamepad &pad, volatile int &g_buttonA_flag)
{
    while(1) {
        lcd.clear();
        lcd.printString("Contrast", 0, 0);
        lcd.printString("Press A to set", 0, 3);

        float conSet = 0.4 + ((double) pad.read_pot1()/5);
        float bar = cursor_transform(0.6,0.4,64,0,conSet);

        lcd.drawRect(10,10,74,10,FILL_TRANSPARENT);    // transparent, just outline
        lcd.drawRect(10,10,bar,10,FILL_BLACK);  // filled black rectangle
        lcd.setContrast(conSet);

        if(g_buttonA_flag) {
            g_buttonA_flag = 0;
            return;
        }

        lcd.refresh();
    }
}

void gameEngine::buttonDelay(N5110 &lcd,
                             Gamepad &pad,
                             volatile int &g_buttonA_flag,
                             volatile int &g_buttonX_flag,
                             volatile bool &g_buttonTesting,
                             volatile int &g_buttonSensitivityTest,
                             volatile int &g_buttonSensitivity)
{
    // Tell the X button that test mode is on, initially start with an empty circle
    g_buttonTesting = true;
    bool trialCircle = false;

    while(1) {
        lcd.clear();
        lcd.printString("Button Delay", 0, 0);
        lcd.printString("Press X to test", 0, 4);
        lcd.printString("Press A to set", 0, 5);

        // Calculate the length of the bar
        g_buttonSensitivityTest = cursor_transform(1,0,2,0,pad.read_pot1()) * 250;
        int bar = cursor_transform(500,0,64,0,g_buttonSensitivityTest);

        // example of how to draw rectangles
        //          origin x,y,width,height,type
        lcd.drawRect(10,10,74,10,FILL_TRANSPARENT);     // transparent, just outline
        lcd.drawRect(10,10,bar,10,FILL_BLACK);          // filled black rectangle

        if(trialCircle) {
            lcd.drawCircle(42, 25, 3, FILL_BLACK);
        } else {
            lcd.drawCircle(42, 25, 3, FILL_TRANSPARENT);
        }

        if(g_buttonX_flag) {
            g_buttonX_flag = 0;
            trialCircle = !trialCircle;
        } else if(g_buttonA_flag) {
            g_buttonA_flag = 0;
            g_buttonSensitivity = g_buttonSensitivityTest;
            g_buttonTesting = false;
            return;
        }

        // Refresh the screen so its up to date
        lcd.refresh();
    }
}