Final Submission. I have read and agreed with Statement of Academic Integrity.

Dependencies:   mbed

main.cpp

Committer:
Nicholas75179
Date:
2020-05-22
Revision:
4:1806215c5cd8
Parent:
1:3727d33a171b

File content as of revision 4:1806215c5cd8:

/*
ELEC2645 Embedded Systems Project
School of Electronic & Electrical Engineering
University of Leeds
Name: Nicholas Wu
Username: el18nw
Student ID Number: 201275179
Date: 22/5/2020
*/

#include "mbed.h"
#include "Gamepad.h"
#include "N5110.h"
#include "Bitmap.h"
#include "Spaceship.h"

// IN-GAME DEBUGGING FLAGS
bool DEBUG_menu = 0;
bool sound_on = 1;
// During development a mixture of serial port and in-game debugging methods
// I found in-game debugging to be more interesting and accessible
// Thus I had left these two debugging options here as examples

// OBJECTS
Gamepad pad;
N5110 lcd;
Bitmap AGALAG(title, 10, 48); // name, y and x width
Bitmap SHIP0(player_ship0, 9, 12);
Bitmap SHIP1(player_ship1, 9, 12);
Spaceship player;
Spaceship enemy[2];
// Declaring enemies as an array makes adding more enemies simple
// for this platform 2 allows for a playable framerate

// FUNCTIONS & RESPECTIVE VARIABLES
void dramatic_print(string, int, int, int);
    int temptime = clock();
    char g_buffer[14] = "0";
void title_sequence();
    float g_x = 0, g_y = 0;
void create_stars();
    bool g_star_map[224][224];
bool check_stars(int, int);
void print_background(float, float, float);
void menu();
void help();
void ship_select();
    int player_ship_type = 0;
    bool difficulty = 0;
    bool god_mode = 0;
void print_menu(int, int);
void level_system();
    int points = 0;
void engine_setup(bool);
void engine(bool, float);
void render_objects(int);
void draw_enemy(int, int, int);
void effects(bool, int, float);
void sound(bool);
bool explode(int, int);
void print_tracker(int i);

int main() {
    lcd.init();
    lcd.inverseMode();
    lcd.setContrast(0.48);
    pad.init();
    lcd.backLightOn();
    create_stars();
    title_sequence();
    while (1) menu();
}

void menu() {
    int menu = 0, selection = 0;
    
    while (1) {
        lcd.clear();
        print_background(1, 0, 0);
        print_menu(menu, selection);
        AGALAG.render(lcd, 18, 5);
        
        if (pad.B_held()) selection++;
        if (pad.X_held()) selection--;
        while (pad.B_held() || pad.X_held());
        if (selection < 0) selection = 3;
        else if (selection > 3) selection = 0;
        if (DEBUG_menu) {
            sprintf(g_buffer, " %1i,%1i", menu, selection);
            lcd.printString(g_buffer, 0, 5);
        }
        lcd.refresh();
        
        if (pad.A_held() || pad.Y_held()) {
            if (menu == 3) {
                if (selection == 0) sound_on = !sound_on;
                if (selection == 1) points += 100;;
                if (selection == 2) DEBUG_menu = !DEBUG_menu;
                if (selection == 3) menu = 0;
            }else if (menu==1) {
                if (selection == 0) ship_select();
                if (selection == 1) god_mode =! god_mode;
                if (selection == 2) difficulty =! difficulty;
                if (selection == 3) menu = 0;
            }else if (menu == 0) {
                if (selection == 0) level_system();
                if (selection == 1) menu = 1;
                if (selection == 2) help();
                if (selection == 3) menu = 3;
            }
            //this while statement captures the frame so no more than
            //one increment could happen with each button press
            while (pad.A_held() || pad.Y_held());
        }
    }
}

void print_menu(int menu, int selection) {
    //selected menu item is denoted as such: >example<
    if (menu == 0) {
        if (selection == 0) lcd.printString(">PLAY!<", 21, 2);
        else lcd.printString("PLAY!", 27, 2);
        if (selection == 1) lcd.printString(">OPTIONS<", 15, 3);
        else lcd.printString("OPTIONS", 21, 3);
        if (selection == 2) lcd.printString(">CONTROLS<", 12, 4);
        else lcd.printString("CONTROLS", 18, 4);
        if (selection == 3) lcd.printString(">DEBUG<", 21, 5);
        else lcd.printString("DEBUG", 27, 5);
    }
    if (menu == 1) {
        if (selection == 0) lcd.printString(">SHIP=0<", 18, 2);
        else lcd.printString("SHIP=0", 24, 2);
        lcd.printChar(player_ship_type+49, 54, 2);
        if (selection == 1) lcd.printString(">GOD MODE=0<", 6, 3);
        else lcd.printString("GOD MODE=0", 12, 3);
        lcd.printChar(48+god_mode, 66, 3);
        if (selection == 2) lcd.printString(">EASY MODE<", 9, 4);
        else lcd.printString("EASY MODE", 15, 4);
        if (difficulty) lcd.printString("HARD", 15, 4);
        if (selection == 3) lcd.printString(">BACK<", 24, 5);
        else lcd.printString("BACK", 30, 5);
    }
    if (menu==3) {
        if (selection == 0) lcd.printString(">SOUND=0<", 15, 2);
        else lcd.printString("SOUND=0", 21, 2);
        if (sound_on) lcd.printString("1", 57, 2);
        if (selection == 1) lcd.printString(">POINTS=   0<", 3, 3);
        else lcd.printString("POINTS=   0", 9, 3);
            sprintf(g_buffer, "%4i", points);
            lcd.printString(g_buffer, 52, 3);
        if (selection == 2) lcd.printString(">MENU DB=0<", 9, 4);
        else lcd.printString("MENU DB=0", 15, 4);
        if (DEBUG_menu) lcd.printString("1", 63, 4);
        if (selection == 3) lcd.printString(">BACK<", 24, 5);
        else lcd.printString("BACK", 30, 5);
    }
}

void ship_select() {
    player_ship_type++;
    player_ship_type %= 2;
    //selects between one of two
    //ship types available
}

void level_system() {
    while (!pad.start_held()) {
        create_stars();
        engine_setup(int(points/400)); 
        //passing the result of this division saves calculation during gameplay
        if (enemy[0].HP <= 0 && enemy[1].HP <= 0 && player.HP <= 0) {
            lcd.printString("<ACCEPTABLE>", 6, 4);
        }
        else if (player.HP <= 0) lcd.printString("<GAME OVER>", 9, 4);
        else if (enemy[0].HP <= 0 && enemy[1].HP <= 0) {
            lcd.printString("<NICE>", 24, 4);
            points += 100;
            if (difficulty) points += 100;
            if (points>400) points += 100;
        } else lcd.printString("<TOO SLOW>", 12, 4);
        //above checks victory conditions, extra points for difficulty
        sprintf(g_buffer, "SCORE:%4i", points);
        if (enemy[0].HP > 0 || enemy[1].HP > 0) points = 0;
        lcd.printString(g_buffer, 12, 5);
        lcd.refresh();
        wait_ms(200);
        if (points <= 200) lcd.printString("<EXIT: START>", 6, 0);
        lcd.printString("<PLAY: ABXY>", 9, 1);
        lcd.refresh();
        while (!pad.A_held() && !pad.B_held() && !pad.Y_held()
            && !pad.X_held() && !pad.start_held());
    }
    pad.leds(0);
}

void engine_setup (bool phase_two) {
    float HP_divider;
    //init_ship sets parameters - position, direction,
    //turn rate, acceleration, speed and hitpoints
    if (player_ship_type == 0) {
        player.init_ship(1, 0, 0, 0.05, 0.05, 0.7, 12);
        HP_divider = 0.5; //HP divider is used to display health on the LEDs
    } else {
        player.init_ship(1, 0, 0, 0.15, 0.15, 0.5, 20);
        HP_divider = 0.3;
    } //ship type 2: slower speed and fire rate,
      //faster turn rate, higher damage and more HP
    enemy[0].init_ship(40, -20+rand()%40, -1+rand()%2,
        0.06+0.00005*points, 0.08+0.0001*points, 0.7+0.001*points, 10);
        //additional maths in the enemy initialisation
        //to increase difficulty over time
    if (difficulty) for (int i = 0; i <= 1; i++) {
        enemy[i].init_ship(50+rand()%30, 10-20*i, 2-4*i,
        0.06+0.00005*points, 0.1+0.0001*points, 1+0.001*points, 10);
    } else if (phase_two) for (int i = 0; i <= 1; i++) {
        enemy[i].init_ship(50+rand()%30, 10-20*i, -1+2*i,
        0.05+0.00005*points, 0.05+0.0001*points, 0.4+0.001*points, 10);
    }
    engine(phase_two, HP_divider);
}

void engine(bool phase_two, float HP_divider) {
    int end_delay = 40, time_limit = clock() + 6000;
    // end_delay lets the animation continue for about two seconds after death
    // time_limit keeps the game fun by forcing players to engage with the game
    
    while (end_delay > 0 && clock() < time_limit) {
        Vector2D coord=pad.get_mapped_coord();
        lcd.clear();
        
        for (int i = 0; i <= (difficulty || phase_two); i++) {
            if (enemy[i].HP > 0) {
                enemy[i].AI_controls(player.pos_x, player.pos_y, player.orientation);
            }
            if (enemy[i].check_hitbox(int(player.pos_x), int(player.pos_y), 4)) {
                player.HP--;
            }
            if (player.check_hitbox(int(enemy[i].pos_x), int(enemy[i].pos_y), 5)) {
                if (player_ship_type == 0)enemy[i].HP--;
                else enemy[i].HP -= 4; //cannon on ship1 does more damage
            }
            enemy[i].update();
        }//change the for loop and object declaration to add more enemies
        
        if (player_ship_type == 0) SHIP0.render(lcd, 60, 20);
        else SHIP1.render(lcd, 60, 20);
        
        render_objects(phase_two || difficulty);
        effects(phase_two, time_limit, HP_divider);
        // effects control every other aspect of the gamepad output
        
        if (player.HP > 0) {
            player.controls(player_ship_type, pad.Y_held(),
            pad.A_held(), pad.X_held(), pad.B_held(), coord.x, coord.y);
        }
        if ((enemy[0].HP<=0 && enemy[1].HP <= 0) || player.HP <= 0) end_delay--;
        if (god_mode) player.HP = 24;
        
        lcd.refresh();
        // I opted not to limit the frame rate of the game because
        // it will never render fast enough to be less fun
        // as if it was running at full speed
    }
}

void render_objects(int extra_enemies) {
    int temp1, temp2;
    float sine = sin(player.orientation), cosine = cos(player.orientation);
    
    for (int i = -16; i < 69; i++) {
        for (int ii = -24; ii < 32; ii++) {
            temp1 = int(player.pos_x + (i * cosine - ii * sine));
            temp2 = int(player.pos_y + (ii * cosine + i * sine));
            
            if (temp1 == int(enemy[0].pos_x) 
                && temp2 == int(enemy[0].pos_y)) {
                draw_enemy(i, ii, 0);
                #ifdef DEBUG_render
                    printf("Found enemy0: %3.1f,%3.1f\n", 
                    enemy[0].pos_x, enemy[0].pos_y);
                #endif
                
            } else if (extra_enemies && temp1 == int(enemy[1].pos_x) 
                && temp2 == int(enemy[1].pos_y)) {
                draw_enemy(i, ii, 1);
                #ifdef DEBUG_render
                    printf("Found enemy1: %3.1f,%3.1f\n", 
                    enemy[1].pos_x, enemy[1].pos_y);
                #endif
            }
            else if (check_stars(temp1, temp2) 
                || player.check_bullets(temp1, temp2)
                || enemy[0].check_bullets(temp1, temp2) 
                || enemy[1].check_bullets(temp1, temp2)){
                lcd.setPixel(68-i, 24-ii, 1);
            }
        }// above finds the location of particles to render
    }
}

void draw_enemy(int i, int j, int num) {
    float angle = player.orientation - enemy[num].orientation - 1.5708f;
    //this calculation finds what direction
    //to draw the enemy with respect to the player
    
    if (enemy[num].HP > 0) {
        lcd.drawRect(63 - i, 32 - j, 12, 4, FILL_TRANSPARENT); //health bar
        lcd.drawRect(64 - i, 33 - j, enemy[num].HP, 2, FILL_BLACK);
        
        if (j < 20) {//limits the game to drawing within bound of the screen
            lcd.drawCircle(68 - i, 24 - j, 5, FILL_BLACK);
            lcd.drawCircle(68 - i + 3 * sin(angle),
                24 - j + 3 * cos(angle), 2, FILL_WHITE);
        }
    } else if (explode (68 - i, 24 - j) && j < 20) {
        lcd.drawCircle(68 - i, 24 - j, 5, FILL_BLACK);
        lcd.drawCircle(68 - i + 3 * sin(angle),
            24 - j + 3 * cos(angle), 2, FILL_WHITE);
    }
}

void effects(bool phase_two, int time_limit, float HP_divider) {
    if (enemy[0].HP > 0) print_tracker(0);
    if ((difficulty || phase_two) && (enemy[1].HP > 0)) print_tracker(1);
    if (pad.X_held() && clock() % 18 > 8) {
        for (int i = 0; i < 3; i++)lcd.setPixel(69 + i, 24 , 1);
        if (clock() % 9 > 3) {
            lcd.setPixel(72, 24, 1);
            lcd.setPixel(73, 24, 1);
        }
    }
    pad.leds(0); //LEDs display player health proportionally
    if (player.HP > 0) {
        for (int health = 0; health <= (HP_divider*player.HP); health++) {
            pad.led(health, 1);
        }
    }
    else explode(68, 24);
    if (sound_on) sound(difficulty || phase_two);
    sprintf(g_buffer, "%2i", (time_limit - clock()) / 100);
    lcd.printString(g_buffer, 12, 5);
}

void print_tracker(int num) {
    float diff_x = enemy[num].pos_x-player.pos_x;
    if (abs(diff_x) < 0.0001f) diff_x = 0.0001f;
    
    float angle_to_enemy = atan((enemy[num].pos_y - player.pos_y) / diff_x);
    if ((diff_x) > 0) angle_to_enemy += 3.1416f;
    
    #ifdef DEBUG_tracker
            sprintf(g_buffer, "%1.3f", player.orientation);
            lcd.printString(g_buffer, 60, 0);
            sprintf(g_buffer, "%1.3f", angle_to_enemy - player.orientation);
            lcd.printString(g_buffer, 60, 1);
    #endif
    
    float sine = sin(angle_to_enemy - player.orientation);
    float cosine = cos(angle_to_enemy - player.orientation);
    
    lcd.setPixel(67 + 8 * cosine, 24 + 6 * sine, 1);
    lcd.setPixel(67 + 9 * cosine, 24 + 7 * sine, 1);
    lcd.setPixel(67 + 10 * cosine, 24 + 8 * sine, 1);
    lcd.setPixel(67 + 11 * cosine, 24 + 9 * sine, 1);
    lcd.setPixel(67 + 12 * cosine, 24 + 10 * sine, 1);
    lcd.setPixel(67 + 13 * cosine, 24 + 11 * sine, 1);
    lcd.drawCircle(67 + 9 * cosine, 24 + 7 * sine, 1, FILL_BLACK);
    lcd.drawCircle(67 + 10 * cosine, 24 + 8 * sine, 1, FILL_BLACK);
}

void sound(bool extra_enemy) {
    //buzzers can only produce one tone at a time
    //this function prioritises some sounds over others
    if (player.HP <= 0) {
        player.explosion_FX--;
    } 
    if (enemy[0].HP <= 0) {
        enemy[0].explosion_FX--;
    }
    if (extra_enemy && enemy[1].HP <= 0) {
        enemy[1].explosion_FX--;
    } 
    //"explosion" is initialised as 8, and is decremented each frame
    //below if statement makes the explosion produce noise over 7 frames
    if ((player.explosion_FX < 8 && player.explosion_FX > 0)
        || (enemy[0].explosion_FX < 8 && enemy[0].explosion_FX > 0)
        || (extra_enemy && enemy[1].explosion_FX < 8 && enemy[1].explosion_FX > 0)) {
        pad.tone(1000, 0.1);
    }
    else if (player.gun_FX > 0) pad.tone(1600 - player.gun_FX * 200, 0.1);
    //the player's weapon takes priority for sound, after explosions
    else if (enemy[0].gun_FX > 0) pad.tone(5000, 0.1);
    else if (enemy[1].gun_FX > 0) pad.tone(5000, 0.1);
    
    enemy[0].gun_FX--;
    enemy[1].gun_FX--;
    player.gun_FX--;
    //decrementing the FX variable makes sure the sound is not delayed
    //if the function is busy producing a different sound
}

bool explode(int i, int j) {
    //a clock() with a modulo helps make the explosion more exciting
    //by forcing it to switch on and off over time
    if (clock() % 9 > 4) {
        for (int inc = 0; inc < 6; inc++) {
            float b = rand() % 63 / 10;
            int a = rand() % 6;
            int tempx = i - a * sin(b), tempy = j - a * cos(b);
            
            if (tempy > 3 && i < 81) lcd.drawCircle(tempx, tempy, 2, FILL_BLACK);
        }//6 randomly placed white packets to simulate explosion
        return 0;
    }//boolean returns are used for switching a rendering function in draw_enemy();
    return 1;
}

void print_background(float screen_x, float screen_y, float screen_orientation) {
    int temp1, temp2;
    float sine = sin(screen_orientation), cosine = cos(screen_orientation);
    
    for (int i = -16; i < 69; i++ ) {
        for (int ii = -24; ii < 25; ii++) {
            temp1 = int(screen_x + (i * cosine - ii * sine));
            temp2 = int(screen_y + (ii * cosine + i * sine)) + 1;
            
            if (check_stars(temp1, temp2)) lcd.setPixel(68 - i, 24 - ii, 1);
        }
    }
}

bool check_stars(int temp1, int temp2) {
    while (temp1 < 0) temp1 += 224;
    temp1 = temp1 % 224; //this function tesselates the star map
    while (temp2 < 0) temp2 += 224;
    temp2 = temp2 % 224;
    
    if (g_star_map[temp1][temp2])return 1;
    else return 0;
}

void create_stars() {//the backhround is a boolean array
    for (int i = 0; i < 224; i++) {
        for (int ii = 0; ii < 224; ii++) g_star_map[i][ii] = 0;
    }//this clears the background for drawing a new one
    for (int i = 0; i < 224; i++) {
        for (int ii = 0; ii < 224; ii++) {
            if (rand() % 1000 > 985) g_star_map[i][ii] = 1;
        }//1.5% of the sky is stars!
    }
}

void title_sequence() {
    temptime = clock();
    
    while ((clock() - temptime) < 40) {
        lcd.clear();
        print_background(1, 0, 0);
        lcd.refresh();
    }
    sprintf(g_buffer, "Space Combat");
    
    while (!pad.A_held() && !pad.B_held() && !pad.Y_held()
        && !pad.X_held() && !pad.start_held()) {
        AGALAG.render(lcd, 18, 12);
        if ((clock() - temptime) < 240) dramatic_print(g_buffer, 7, 3, 20);
        lcd.refresh();
    }
    temptime = clock();
    
    while (clock() - temptime < 140 && !pad.start_held()) {
        lcd.clear();
        print_background(1, 0, 0);
        AGALAG.render(lcd, 18, 11 - (clock() - temptime) / 20);
        lcd.refresh();
    }
}

void dramatic_print(string str, int offset, int line_num, int delay_time) {
        int i = ((clock() - temptime) / delay_time) % 14;
        
        for (int ii = 0; ii <= i; ii++) {
            lcd.printChar(str[ii], 6 * ii + offset, line_num);
        }
}

void help() {
    while (pad.A_held() || pad.Y_held());
    pad.leds(1.0);
    
    while (!pad.A_held() && !pad.B_held() && !pad.Y_held()
        && !pad.X_held() && !pad.start_held()) {
        Vector2D coord = pad.get_mapped_coord();
        lcd.clear();
        print_background(1, 0, 0);
        lcd.printString("<HP=LED>", 19, 0);
        lcd.printString("<SHOOT>", 0, 1);
        lcd.printString("<MOVE>", 45, 1);
        lcd.printString(">BACK<", 24, 5);
        lcd.drawCircle(63, 36, 3, FILL_TRANSPARENT);
        lcd.drawCircle(63, 20, 3, FILL_TRANSPARENT);
        lcd.drawCircle(53, 28, 3, FILL_TRANSPARENT);
        lcd.drawCircle(73, 28, 3, FILL_TRANSPARENT);
        lcd.drawCircle(20 + int(10 * coord.x), 
            28 - int(10 * coord.y), 3, FILL_BLACK);
            
        if (clock() % 4 > 1) lcd.drawCircle(20, 28, 12, FILL_TRANSPARENT);
        lcd.refresh();
    }
    pad.leds(0);
}