#include #include #include #include #include #include #include #include #define TAG "Arkanoid" #define FLIPPER_LCD_WIDTH 128 #define FLIPPER_LCD_HEIGHT 64 #define MAX_SPEED 3 typedef enum { EventTypeTick, EventTypeKey } EventType; typedef struct { //Brick Bounds used in collision detection int leftBrick; int rightBrick; int topBrick; int bottomBrick; bool isHit[4][13]; //Array of if bricks are hit or not } BrickState; typedef struct { int dx; //Initial movement of ball int dy; //Initial movement of ball int xb; //Balls starting possition int yb; //Balls starting possition bool released; //If the ball has been released by the player //Ball Bounds used in collision detection int leftBall; int rightBall; int topBall; int bottomBall; } BallState; typedef struct { FuriMutex* mutex; BallState ball_state; BrickState brick_state; NotificationApp* notify; unsigned int COLUMNS; //Columns of bricks unsigned int ROWS; //Rows of bricks bool initialDraw; //If the inital draw has happened int xPaddle; //X position of paddle char text[16]; //General string buffer bool bounced; //Used to fix double bounce glitch int lives; //Amount of lives int level; //Current level unsigned int score; //Score for the game unsigned int brickCount; //Amount of bricks hit int tick; //Tick counter bool gameStarted; // Did the game start? int speed; // Ball speed } ArkanoidState; typedef struct { EventType type; InputEvent input; } GameEvent; static const NotificationSequence sequence_short_sound = { &message_note_c5, &message_delay_50, &message_sound_off, NULL, }; // generate number in range [min,max) int rand_range(int min, int max) { return min + rand() % (max - min); } void move_ball(Canvas* canvas, ArkanoidState* st) { st->tick++; int current_speed = abs(st->speed - 1 - MAX_SPEED); if(st->tick % current_speed != 0 && st->tick % (current_speed + 1) != 0) { return; } if(st->ball_state.released) { //Move ball if(abs(st->ball_state.dx) == 2) { st->ball_state.xb += st->ball_state.dx / 2; // 2x speed is really 1.5 speed if((st->tick / current_speed) % 2 == 0) st->ball_state.xb += st->ball_state.dx / 2; } else { st->ball_state.xb += st->ball_state.dx; } st->ball_state.yb = st->ball_state.yb + st->ball_state.dy; //Set bounds st->ball_state.leftBall = st->ball_state.xb; st->ball_state.rightBall = st->ball_state.xb + 2; st->ball_state.topBall = st->ball_state.yb; st->ball_state.bottomBall = st->ball_state.yb + 2; //Bounce off top edge if(st->ball_state.yb <= 0) { st->ball_state.yb = 2; st->ball_state.dy = -st->ball_state.dy; } //Lose a life if bottom edge hit if(st->ball_state.yb >= FLIPPER_LCD_HEIGHT) { canvas_draw_frame(canvas, st->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1); st->xPaddle = 54; st->ball_state.yb = 60; st->ball_state.released = false; st->lives--; st->gameStarted = false; if(rand_range(0, 2) == 0) { st->ball_state.dx = 1; } else { st->ball_state.dx = -1; } } //Bounce off left side if(st->ball_state.xb <= 0) { st->ball_state.xb = 2; st->ball_state.dx = -st->ball_state.dx; } //Bounce off right side if(st->ball_state.xb >= FLIPPER_LCD_WIDTH - 2) { st->ball_state.xb = FLIPPER_LCD_WIDTH - 4; st->ball_state.dx = -st->ball_state.dx; // arduboy.tunes.tone(523, 250); } //Bounce off paddle if(st->ball_state.xb + 1 >= st->xPaddle && st->ball_state.xb <= st->xPaddle + 12 && st->ball_state.yb + 2 >= FLIPPER_LCD_HEIGHT - 1 && st->ball_state.yb <= FLIPPER_LCD_HEIGHT) { st->ball_state.dy = -st->ball_state.dy; st->ball_state.dx = ((st->ball_state.xb - (st->xPaddle + 6)) / 3); //Applies spin on the ball // prevent straight bounce, but not prevent roguuemaster from stealing if(st->ball_state.dx == 0) { st->ball_state.dx = (rand_range(0, 2) == 1) ? 1 : -1; } } //Bounce off Bricks for(unsigned int row = 0; row < st->ROWS; row++) { for(unsigned int column = 0; column < st->COLUMNS; column++) { if(!st->brick_state.isHit[row][column]) { //Sets Brick bounds st->brick_state.leftBrick = 10 * column; st->brick_state.rightBrick = 10 * column + 10; st->brick_state.topBrick = 6 * row + 1; st->brick_state.bottomBrick = 6 * row + 7; //If A collison has occured if(st->ball_state.topBall <= st->brick_state.bottomBrick && st->ball_state.bottomBall >= st->brick_state.topBrick && st->ball_state.leftBall <= st->brick_state.rightBrick && st->ball_state.rightBall >= st->brick_state.leftBrick) { st->score += st->level; // Blink led when we hit some brick notification_message(st->notify, &sequence_short_sound); //notification_message(st->notify, &sequence_blink_white_100); st->brickCount++; st->brick_state.isHit[row][column] = true; canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4); //Vertical collision if(st->ball_state.bottomBall > st->brick_state.bottomBrick || st->ball_state.topBall < st->brick_state.topBrick) { //Only bounce once each ball move if(!st->bounced) { st->ball_state.dy = -st->ball_state.dy; st->ball_state.yb += st->ball_state.dy; st->bounced = true; } } //Hoizontal collision if(st->ball_state.leftBall < st->brick_state.leftBrick || st->ball_state.rightBall > st->brick_state.rightBrick) { //Only bounce once brick each ball move if(!st->bounced) { st->ball_state.dx = -st->ball_state.dx; st->ball_state.xb += st->ball_state.dx; st->bounced = true; } } } } } } //Reset Bounce st->bounced = false; } else { //Ball follows paddle st->ball_state.xb = st->xPaddle + 5; } } void draw_lives(Canvas* canvas, ArkanoidState* arkanoid_state) { if(arkanoid_state->lives == 3) { canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 11); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 11); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 12); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 12); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 15); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 15); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 16); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 16); } else if(arkanoid_state->lives == 2) { canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 11); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 11); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 12); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 12); } else { canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7); canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8); canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8); } } void draw_score(Canvas* canvas, ArkanoidState* arkanoid_state) { snprintf(arkanoid_state->text, sizeof(arkanoid_state->text), "%u", arkanoid_state->score); canvas_draw_str_aligned( canvas, FLIPPER_LCD_WIDTH - 2, FLIPPER_LCD_HEIGHT - 6, AlignRight, AlignBottom, arkanoid_state->text); } void draw_ball(Canvas* canvas, ArkanoidState* ast) { canvas_draw_dot(canvas, ast->ball_state.xb, ast->ball_state.yb); canvas_draw_dot(canvas, ast->ball_state.xb + 1, ast->ball_state.yb); canvas_draw_dot(canvas, ast->ball_state.xb, ast->ball_state.yb + 1); canvas_draw_dot(canvas, ast->ball_state.xb + 1, ast->ball_state.yb + 1); move_ball(canvas, ast); } void draw_paddle(Canvas* canvas, ArkanoidState* arkanoid_state) { canvas_draw_frame(canvas, arkanoid_state->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1); } void reset_level(Canvas* canvas, ArkanoidState* arkanoid_state) { //Undraw paddle canvas_draw_frame(canvas, arkanoid_state->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1); //Undraw ball canvas_draw_dot(canvas, arkanoid_state->ball_state.xb, arkanoid_state->ball_state.yb); canvas_draw_dot(canvas, arkanoid_state->ball_state.xb + 1, arkanoid_state->ball_state.yb); canvas_draw_dot(canvas, arkanoid_state->ball_state.xb, arkanoid_state->ball_state.yb + 1); canvas_draw_dot(canvas, arkanoid_state->ball_state.xb + 1, arkanoid_state->ball_state.yb + 1); //Alter various variables to reset the game arkanoid_state->xPaddle = 54; arkanoid_state->ball_state.yb = 60; arkanoid_state->brickCount = 0; arkanoid_state->ball_state.released = false; arkanoid_state->gameStarted = false; // Reset all brick hit states for(unsigned int row = 0; row < arkanoid_state->ROWS; row++) { for(unsigned int column = 0; column < arkanoid_state->COLUMNS; column++) { arkanoid_state->brick_state.isHit[row][column] = false; } } } static void arkanoid_state_init(ArkanoidState* arkanoid_state) { // Init notification arkanoid_state->notify = furi_record_open(RECORD_NOTIFICATION); // Set the initial game state arkanoid_state->COLUMNS = 13; arkanoid_state->ROWS = 4; arkanoid_state->ball_state.dx = -1; arkanoid_state->ball_state.dy = -1; arkanoid_state->speed = 2; arkanoid_state->bounced = false; arkanoid_state->lives = 3; arkanoid_state->level = 1; arkanoid_state->score = 0; arkanoid_state->COLUMNS = 13; arkanoid_state->COLUMNS = 13; // Reset initial state arkanoid_state->initialDraw = false; arkanoid_state->gameStarted = false; } static void arkanoid_draw_callback(Canvas* const canvas, void* ctx) { furi_assert(ctx); ArkanoidState* arkanoid_state = ctx; furi_mutex_acquire(arkanoid_state->mutex, FuriWaitForever); //Initial level draw if(!arkanoid_state->initialDraw) { arkanoid_state->initialDraw = true; // Set default font for text canvas_set_font(canvas, FontSecondary); //Draws the new level reset_level(canvas, arkanoid_state); } //Draws new bricks and resets their values for(unsigned int row = 0; row < arkanoid_state->ROWS; row++) { for(unsigned int column = 0; column < arkanoid_state->COLUMNS; column++) { if(!arkanoid_state->brick_state.isHit[row][column]) { canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4); } } } if(arkanoid_state->lives > 0) { draw_paddle(canvas, arkanoid_state); draw_ball(canvas, arkanoid_state); draw_score(canvas, arkanoid_state); draw_lives(canvas, arkanoid_state); if(arkanoid_state->brickCount == arkanoid_state->ROWS * arkanoid_state->COLUMNS) { arkanoid_state->level++; reset_level(canvas, arkanoid_state); } } else { reset_level(canvas, arkanoid_state); arkanoid_state->initialDraw = false; arkanoid_state->lives = 3; arkanoid_state->score = 0; } furi_mutex_release(arkanoid_state->mutex); } static void arkanoid_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) { furi_assert(event_queue); GameEvent event = {.type = EventTypeKey, .input = *input_event}; furi_message_queue_put(event_queue, &event, FuriWaitForever); } static void arkanoid_update_timer_callback(FuriMessageQueue* event_queue) { furi_assert(event_queue); GameEvent event = {.type = EventTypeTick}; furi_message_queue_put(event_queue, &event, 0); } int32_t arkanoid_game_app(void* p) { UNUSED(p); int32_t return_code = 0; FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(GameEvent)); ArkanoidState* arkanoid_state = malloc(sizeof(ArkanoidState)); arkanoid_state_init(arkanoid_state); arkanoid_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); if(!arkanoid_state->mutex) { FURI_LOG_E(TAG, "Cannot create mutex\r\n"); return_code = 255; goto free_and_exit; } // Set system callbacks ViewPort* view_port = view_port_alloc(); view_port_draw_callback_set(view_port, arkanoid_draw_callback, arkanoid_state); view_port_input_callback_set(view_port, arkanoid_input_callback, event_queue); FuriTimer* timer = furi_timer_alloc(arkanoid_update_timer_callback, FuriTimerTypePeriodic, event_queue); furi_timer_start(timer, furi_kernel_get_tick_frequency() / 22); // Open GUI and register view_port Gui* gui = furi_record_open(RECORD_GUI); gui_add_view_port(gui, view_port, GuiLayerFullscreen); // Call dolphin deed on game start dolphin_deed(DolphinDeedPluginGameStart); GameEvent event; for(bool processing = true; processing;) { FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100); furi_mutex_acquire(arkanoid_state->mutex, FuriWaitForever); if(event_status == FuriStatusOk) { // Key events if(event.type == EventTypeKey) { if(event.input.type == InputTypePress || event.input.type == InputTypeLong || event.input.type == InputTypeRepeat) { switch(event.input.key) { case InputKeyBack: processing = false; break; case InputKeyRight: if(arkanoid_state->xPaddle < FLIPPER_LCD_WIDTH - 12) { arkanoid_state->xPaddle += 8; } break; case InputKeyLeft: if(arkanoid_state->xPaddle > 0) { arkanoid_state->xPaddle -= 8; } break; case InputKeyUp: if(arkanoid_state->speed < MAX_SPEED) { arkanoid_state->speed++; } break; case InputKeyDown: if(arkanoid_state->speed > 1) { arkanoid_state->speed--; } break; case InputKeyOk: if(arkanoid_state->gameStarted == false) { //Release ball if FIRE pressed arkanoid_state->ball_state.released = true; //Apply random direction to ball on release if(rand_range(0, 2) == 0) { arkanoid_state->ball_state.dx = 1; } else { arkanoid_state->ball_state.dx = -1; } //Makes sure the ball heads upwards arkanoid_state->ball_state.dy = -1; //start the game flag arkanoid_state->gameStarted = true; } break; default: break; } } } } view_port_update(view_port); furi_mutex_release(arkanoid_state->mutex); } furi_timer_free(timer); view_port_enabled_set(view_port, false); gui_remove_view_port(gui, view_port); furi_record_close(RECORD_GUI); furi_record_close(RECORD_NOTIFICATION); view_port_free(view_port); furi_mutex_free(arkanoid_state->mutex); free_and_exit: free(arkanoid_state); furi_message_queue_free(event_queue); return return_code; }