Final Submission. I have read and agreed with Statement of Academic Integrity.
Dependencies: mbed
Diff: main.cpp
- Revision:
- 0:fde420d18f42
- Child:
- 1:3727d33a171b
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main.cpp Fri May 22 09:42:51 2020 +0000 @@ -0,0 +1,508 @@ +//************************// +// AGALAG: SPACE COMBAT // +// Version 2.6 // +// MAY 2020 // +// Nicholas W. // +//************************// + +#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); +} \ No newline at end of file