|
|
@@ -54,7 +54,7 @@ typedef struct AsteroidsApp {
|
|
|
|
|
|
/* Game state. */
|
|
|
int running; /* Once false exists the app. */
|
|
|
- int gameover; /* Gameover status. */
|
|
|
+ bool gameover; /* Gameover status. */
|
|
|
uint32_t ticks; /* Game ticks. Increments at each refresh. */
|
|
|
uint32_t score; /* Game score. */
|
|
|
uint32_t lives; /* Number of lives in the current game. */
|
|
|
@@ -81,6 +81,13 @@ typedef struct AsteroidsApp {
|
|
|
bool fire; /* Short press detected: fire a bullet. */
|
|
|
} AsteroidsApp;
|
|
|
|
|
|
+/* ============================== Prototyeps ================================ */
|
|
|
+
|
|
|
+// Only functions called before their definition are here.
|
|
|
+
|
|
|
+void restart_game_after_gameover(AsteroidsApp *app);
|
|
|
+uint32_t key_pressed_time(AsteroidsApp *app, InputKey key);
|
|
|
+
|
|
|
/* ============================ 2D drawing ================================== */
|
|
|
|
|
|
/* This structure represents a polygon of at most POLY_MAX points.
|
|
|
@@ -215,7 +222,7 @@ void render_callback(Canvas *const canvas, void *ctx) {
|
|
|
|
|
|
/* Clear screen. */
|
|
|
canvas_set_color(canvas, ColorWhite);
|
|
|
- canvas_draw_box(canvas, 0, 0, 127, 63);
|
|
|
+ canvas_draw_box(canvas, 0, 0, SCREEN_XRES-1, SCREEN_YRES-1);
|
|
|
|
|
|
/* Draw score. */
|
|
|
canvas_set_color(canvas, ColorBlack);
|
|
|
@@ -235,6 +242,15 @@ void render_callback(Canvas *const canvas, void *ctx) {
|
|
|
|
|
|
for (int j = 0; j < app->asteroids_num; j++)
|
|
|
draw_asteroid(canvas,&app->asteroids[j]);
|
|
|
+
|
|
|
+ /* Game over text. */
|
|
|
+ if (app->gameover) {
|
|
|
+ canvas_set_color(canvas, ColorBlack);
|
|
|
+ canvas_set_font(canvas, FontPrimary);
|
|
|
+ canvas_draw_str(canvas, 28, 35, "GAME OVER");
|
|
|
+ canvas_set_font(canvas, FontSecondary);
|
|
|
+ canvas_draw_str(canvas, 25, 50, "Press OK to restart");
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* ============================ Game logic ================================== */
|
|
|
@@ -256,9 +272,9 @@ float distance(float x1, float y1, float x2, float y2) {
|
|
|
* spheres (this is why this function only takes the radius). This
|
|
|
* is, after all, kinda accurate for asteroids, for bullets, and
|
|
|
* even for the ship "core" itself. */
|
|
|
-bool detect_collision(float x1, float y1, float r1,
|
|
|
- float x2, float y2, float r2,
|
|
|
- float factor)
|
|
|
+bool objects_are_colliding(float x1, float y1, float r1,
|
|
|
+ float x2, float y2, float r2,
|
|
|
+ float factor)
|
|
|
{
|
|
|
/* The objects are colliding if the distance between object 1 and 2
|
|
|
* is smaller than the sum of the two radiuses r1 and r2.
|
|
|
@@ -372,14 +388,24 @@ void asteroid_was_hit(AsteroidsApp *app, int id) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/* Set gameover state. When in game-over mode, the game displays a gameover
|
|
|
+ * text with a background of many asteroids floating around. */
|
|
|
+void game_over(AsteroidsApp *app) {
|
|
|
+ restart_game_after_gameover(app);
|
|
|
+ app->gameover = true;
|
|
|
+ int asteroids = 8;
|
|
|
+ while(asteroids-- && add_asteroid(app) != NULL);
|
|
|
+}
|
|
|
+
|
|
|
/* Function called when a collision between the asteroid and the
|
|
|
* ship is detected. */
|
|
|
void ship_was_hit(AsteroidsApp *app) {
|
|
|
app->ship_hit = SHIP_HIT_ANIMATION_LEN;
|
|
|
- if (app->lives)
|
|
|
+ if (app->lives) {
|
|
|
app->lives--;
|
|
|
- else
|
|
|
- app->gameover = true;
|
|
|
+ } else {
|
|
|
+ game_over(app);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/* Restart game after the ship is hit. Will reset the ship position, bullets
|
|
|
@@ -398,55 +424,16 @@ void restart_game(AsteroidsApp *app) {
|
|
|
/* Called after gameover to restart the game. This function
|
|
|
* also calls restart_game(). */
|
|
|
void restart_game_after_gameover(AsteroidsApp *app) {
|
|
|
- app->gameover = 0;
|
|
|
+ app->gameover = false;
|
|
|
app->ticks = 0;
|
|
|
app->score = 0;
|
|
|
+ app->ship_hit = 0;
|
|
|
app->lives = GAME_START_LIVES;
|
|
|
restart_game(app);
|
|
|
}
|
|
|
|
|
|
-/* This is the main game execution function, called 10 times for
|
|
|
- * second (with the Flipper screen latency, an higher FPS does not
|
|
|
- * make sense). In this function we update the position of objects based
|
|
|
- * on velocity. Detect collisions. Update the score and so forth.
|
|
|
- *
|
|
|
- * Each time this function is called, app->tick is incremented. */
|
|
|
-void game_tick(void *ctx) {
|
|
|
- AsteroidsApp *app = ctx;
|
|
|
-
|
|
|
- /* This is a special condition: ship was hit, we frozen the game
|
|
|
- * as long as ship_hit isn't zero again, and show an animation of
|
|
|
- * a rotating ship. */
|
|
|
- if (app->ship_hit) {
|
|
|
- app->ship.rot += 0.5;
|
|
|
- app->ship_hit--;
|
|
|
- view_port_update(app->view_port);
|
|
|
- if (app->ship_hit == 0) {
|
|
|
- restart_game(app);
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- /* Handle keypresses. */
|
|
|
- if (app->pressed[InputKeyLeft]) app->ship.rot -= .35;
|
|
|
- if (app->pressed[InputKeyRight]) app->ship.rot += .35;
|
|
|
- if (app->pressed[InputKeyOk]) {
|
|
|
- app->ship.vx -= 0.5*(float)sin(app->ship.rot);
|
|
|
- app->ship.vy += 0.5*(float)cos(app->ship.rot);
|
|
|
- }
|
|
|
-
|
|
|
- /* Fire a bullet if needed. app->fire is set in
|
|
|
- * asteroids_update_keypress_state() since depends on exact
|
|
|
- * pressure timing. */
|
|
|
- if (app->fire) {
|
|
|
- ship_fire_bullet(app);
|
|
|
- app->fire = false;
|
|
|
- }
|
|
|
-
|
|
|
- /* Update ship position according to its velocity. */
|
|
|
- update_pos_by_velocity(&app->ship.x,&app->ship.y,app->ship.vx,app->ship.vy);
|
|
|
-
|
|
|
- /* Update bullets position. */
|
|
|
+/* Move bullets. */
|
|
|
+void update_bullets_position(AsteroidsApp *app) {
|
|
|
for (int j = 0; j < app->bullets_num; j++) {
|
|
|
update_pos_by_velocity(&app->bullets[j].x,&app->bullets[j].y,
|
|
|
app->bullets[j].vx,app->bullets[j].vy);
|
|
|
@@ -456,8 +443,10 @@ void game_tick(void *ctx) {
|
|
|
fill it with the top bullet to take the array dense. */
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- /* Update asteroids position. */
|
|
|
+/* Move asteroids. */
|
|
|
+void update_asteroids_position(AsteroidsApp *app) {
|
|
|
for (int j = 0; j < app->asteroids_num; j++) {
|
|
|
update_pos_by_velocity(&app->asteroids[j].x,&app->asteroids[j].y,
|
|
|
app->asteroids[j].vx,app->asteroids[j].vy);
|
|
|
@@ -465,14 +454,17 @@ void game_tick(void *ctx) {
|
|
|
if (app->asteroids[j].rot < 0) app->asteroids[j].rot = 2*PI;
|
|
|
else if (app->asteroids[j].rot > 2*PI) app->asteroids[j].rot = 0;
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
+/* Collision detection and game state update based on collisions. */
|
|
|
+void detect_collisions(AsteroidsApp *app) {
|
|
|
/* Detect collision between bullet and asteroid. */
|
|
|
for (int j = 0; j < app->bullets_num; j++) {
|
|
|
Bullet *b = &app->bullets[j];
|
|
|
for (int i = 0; i < app->asteroids_num; i++) {
|
|
|
Asteroid *a = &app->asteroids[i];
|
|
|
- if (detect_collision(a->x, a->y, a->size,
|
|
|
- b->x, b->y, 1, 1))
|
|
|
+ if (objects_are_colliding(a->x, a->y, a->size,
|
|
|
+ b->x, b->y, 1, 1))
|
|
|
{
|
|
|
asteroid_was_hit(app,i);
|
|
|
remove_bullet(app,j);
|
|
|
@@ -489,13 +481,69 @@ void game_tick(void *ctx) {
|
|
|
/* Detect collision between ship and asteroid. */
|
|
|
for (int j = 0; j < app->asteroids_num; j++) {
|
|
|
Asteroid *a = &app->asteroids[j];
|
|
|
- if (detect_collision(a->x, a->y, a->size,
|
|
|
- app->ship.x, app->ship.y, 4, 1))
|
|
|
+ if (objects_are_colliding(a->x, a->y, a->size,
|
|
|
+ app->ship.x, app->ship.y, 4, 1))
|
|
|
{
|
|
|
ship_was_hit(app);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
+}
|
|
|
+
|
|
|
+/* This is the main game execution function, called 10 times for
|
|
|
+ * second (with the Flipper screen latency, an higher FPS does not
|
|
|
+ * make sense). In this function we update the position of objects based
|
|
|
+ * on velocity. Detect collisions. Update the score and so forth.
|
|
|
+ *
|
|
|
+ * Each time this function is called, app->tick is incremented. */
|
|
|
+void game_tick(void *ctx) {
|
|
|
+ AsteroidsApp *app = ctx;
|
|
|
+
|
|
|
+ /* There are two special screens:
|
|
|
+ *
|
|
|
+ * 1. Ship was hit, we frozen the game as long as ship_hit isn't zero
|
|
|
+ * again, and show an animation of a rotating ship. */
|
|
|
+ if (app->ship_hit) {
|
|
|
+ app->ship.rot += 0.5;
|
|
|
+ app->ship_hit--;
|
|
|
+ view_port_update(app->view_port);
|
|
|
+ if (app->ship_hit == 0) {
|
|
|
+ restart_game(app);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ } else if (app->gameover) {
|
|
|
+ /* 2. Game over. We need to update only background asteroids. In this
|
|
|
+ * state the game just displays a GAME OVER text with the floating
|
|
|
+ * asteroids in backgroud. */
|
|
|
+ if (key_pressed_time(app,InputKeyOk) > 100) {
|
|
|
+ restart_game_after_gameover(app);
|
|
|
+ }
|
|
|
+ update_asteroids_position(app);
|
|
|
+ view_port_update(app->view_port);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Handle keypresses. */
|
|
|
+ if (app->pressed[InputKeyLeft]) app->ship.rot -= .35;
|
|
|
+ if (app->pressed[InputKeyRight]) app->ship.rot += .35;
|
|
|
+ if (app->pressed[InputKeyOk]) {
|
|
|
+ app->ship.vx -= 0.5*(float)sin(app->ship.rot);
|
|
|
+ app->ship.vy += 0.5*(float)cos(app->ship.rot);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Fire a bullet if needed. app->fire is set in
|
|
|
+ * asteroids_update_keypress_state() since depends on exact
|
|
|
+ * pressure timing. */
|
|
|
+ if (app->fire) {
|
|
|
+ ship_fire_bullet(app);
|
|
|
+ app->fire = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Update positions and detect collisions. */
|
|
|
+ update_pos_by_velocity(&app->ship.x,&app->ship.y,app->ship.vx,app->ship.vy);
|
|
|
+ update_bullets_position(app);
|
|
|
+ update_asteroids_position(app);
|
|
|
+ detect_collisions(app);
|
|
|
|
|
|
/* From time to time, create a new asteroid. The more asteroids
|
|
|
* already on the screen, the smaller probability of creating
|
|
|
@@ -534,6 +582,7 @@ AsteroidsApp* asteroids_app_alloc() {
|
|
|
|
|
|
app->running = 1; /* Turns 0 when back is pressed. */
|
|
|
restart_game_after_gameover(app);
|
|
|
+ game_over(app);
|
|
|
memset(app->pressed,0,sizeof(app->pressed));
|
|
|
return app;
|
|
|
}
|
|
|
@@ -555,12 +604,19 @@ void asteroids_app_free(AsteroidsApp *app) {
|
|
|
free(app);
|
|
|
}
|
|
|
|
|
|
+/* Return the time in milliseconds the specified key is continuously
|
|
|
+ * pressed. Or 0 if it is not pressed. */
|
|
|
+uint32_t key_pressed_time(AsteroidsApp *app, InputKey key) {
|
|
|
+ return app->pressed[key] == 0 ? 0 :
|
|
|
+ furi_get_tick() - app->pressed[key];
|
|
|
+}
|
|
|
+
|
|
|
/* Handle keys interaction. */
|
|
|
void asteroids_update_keypress_state(AsteroidsApp *app, InputEvent input) {
|
|
|
if (input.type == InputTypePress) {
|
|
|
app->pressed[input.key] = furi_get_tick();
|
|
|
} else if (input.type == InputTypeRelease) {
|
|
|
- uint32_t dur = furi_get_tick() - app->pressed[input.key];
|
|
|
+ uint32_t dur = key_pressed_time(app,input.key);
|
|
|
app->pressed[input.key] = 0;
|
|
|
if (dur < 200 && input.key == InputKeyOk) app->fire = true;
|
|
|
}
|
|
|
@@ -576,9 +632,7 @@ int32_t asteroids_app_entry(void* p) {
|
|
|
|
|
|
/* This is the main event loop: here we get the events that are pushed
|
|
|
* in the queue by input_callback(), and process them one after the
|
|
|
- * other. The timeout is 100 milliseconds, so if not input is received
|
|
|
- * before such time, we exit the queue_get() function and call
|
|
|
- * view_port_update() in order to refresh our screen content. */
|
|
|
+ * other. */
|
|
|
InputEvent input;
|
|
|
while(app->running) {
|
|
|
FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100);
|