AMAN DIPATUAN
Published © LGPL

Space Dodger: High-Performance LVGL Game on HMI-Board

Learn how to optimize embedded graphics with this open-source "Space Dodger" game. Built on RT-Thread Studio, it demonstrates collision dete

BeginnerFull instructions provided1 hour4
Space Dodger: High-Performance LVGL Game on HMI-Board

Things used in this project

Hardware components

IoT Enabler
Renesas IoT Enabler
×1

Software apps and online services

RT-Thread IoT OS
RT-Thread IoT OS

Story

Read more

Schematics

Flowchart

Code

game.c

C/C++
#include "lvgl.h"
#include <stdlib.h>
#include <time.h>
#include <stdio.h>

/* --- SCREEN SETTINGS --- */
#define SCREEN_WIDTH   480
#define SCREEN_HEIGHT  272
#define PLAYER_SIZE    45
#define ENEMY_SIZE     30
#define MOVE_STEP      40

/* --- DIFFICULTY SETTINGS --- */
#define MAX_ENEMIES    3
#define ENEMY_SPACING  120
#define MAX_LEADERBOARD 3
#define LANE_WIDTH     40

/* --- GLOBAL OBJECTS --- */
static lv_obj_t *player = NULL;
static lv_obj_t *enemies[MAX_ENEMIES];
static lv_obj_t *score_label = NULL;
static lv_obj_t *game_over_panel = NULL;
static lv_obj_t *start_menu_panel = NULL;
static lv_obj_t *left_btn = NULL;
static lv_obj_t *right_btn = NULL;

/* --- TIMERS --- */
static lv_timer_t *game_timer_handle = NULL;
static lv_timer_t *level_timer_handle = NULL;

/* --- GAME VARIABLES --- */
static int player_x = (SCREEN_WIDTH / 2) - (PLAYER_SIZE / 2);
static int enemies_x[MAX_ENEMIES];
static float enemies_y[MAX_ENEMIES]; // Changed to float for smooth varying speeds
static float enemies_individual_speed[MAX_ENEMIES]; // Individual speed variance

// History buffer to prevent clumping
static int spawn_history[2] = {-1, -1};

static int base_speed = 4;
static int seconds_elapsed = 0;
static int score = 0;
static int prev_score = -1;
static int high_scores[MAX_LEADERBOARD] = {0, 0, 0};

/* --- SHAPES --- */
static lv_point_t star_points[] = {
    {22, 0}, {45, 45}, {22, 35}, {0, 45}, {22, 0}
};

/* --- STYLES --- */
static lv_style_t style_box;
static lv_style_t style_star;
static lv_style_t style_text_large;

/* --- FORWARD DECLARATIONS --- */
void start_gameplay_setup(void);
void show_game_over(void);
void game_loop(lv_timer_t *timer);
void level_timer(lv_timer_t *t);
void update_leaderboard(int new_score);

/* --- IMPROVED RANDOMIZER --- */
static int get_new_enemy_x(void) {
    // 1. Calculate how many lanes fit
    int total_lanes = (SCREEN_WIDTH - ENEMY_SIZE) / LANE_WIDTH;

    // 2. Calculate offset to PERFECTLY CENTER the grid
    int grid_width = total_lanes * LANE_WIDTH;
    int x_offset = (SCREEN_WIDTH - grid_width) / 2;

    int new_lane;
    bool is_duplicate;

    // 3. Smart Retry Loop
    // Keep picking a lane until it is NOT in our recent history
    do {
        new_lane = rand() % total_lanes;
        is_duplicate = false;
        if(new_lane == spawn_history[0] || new_lane == spawn_history[1]) {
            is_duplicate = true;
        }
    } while (is_duplicate);

    // Update History (Shift old values)
    spawn_history[1] = spawn_history[0];
    spawn_history[0] = new_lane;

    // Return centered pixel coordinate + small jitter
    return x_offset + (new_lane * LANE_WIDTH) + (rand() % 5);
}

/* --- LEADERBOARD --- */
void update_leaderboard(int new_score) {
    for(int i = 0; i < MAX_LEADERBOARD; i++) {
        if(new_score > high_scores[i]) {
            for(int j = MAX_LEADERBOARD - 1; j > i; j--) {
                high_scores[j] = high_scores[j-1];
            }
            high_scores[i] = new_score;
            break;
        }
    }
}

/* --- GAMEPLAY LOOP --- */
void game_loop(lv_timer_t *timer)
{
    (void)timer;
    if(!player) return;

    bool collision_detected = false;

    for(int i = 0; i < MAX_ENEMIES; i++) {
        if(enemies[i] == NULL) continue;

        // Move with individual speed variance
        enemies_y[i] += enemies_individual_speed[i];

        // Respawn logic
        if (enemies_y[i] > SCREEN_HEIGHT) {
            enemies_y[i] = -40;
            enemies_x[i] = get_new_enemy_x();

            // Randomize individual speed slightly (Base speed + random 0.0 to 1.5)
            enemies_individual_speed[i] = base_speed + ((rand() % 15) / 10.0f);

            score++;
        }

        lv_obj_set_pos(enemies[i], enemies_x[i], (int)enemies_y[i]);

        // Collision
        if (abs(player_x - enemies_x[i]) < (PLAYER_SIZE/2 + ENEMY_SIZE/2 - 8) &&
            abs((SCREEN_HEIGHT - PLAYER_SIZE - 10) - (int)enemies_y[i]) < (PLAYER_SIZE/2 + ENEMY_SIZE/2 - 8))
        {
            collision_detected = true;
        }
    }

    lv_obj_set_pos(player, player_x, SCREEN_HEIGHT - PLAYER_SIZE - 10);

    if (score != prev_score) {
        char buf[32];
        sprintf(buf, "Score: %d", score);
        lv_label_set_text(score_label, buf);
        prev_score = score;
    }

    if (collision_detected) {
        show_game_over();
    }
}

void level_timer(lv_timer_t *t) {
    (void)t;
    seconds_elapsed++;
    if (seconds_elapsed > 0 && seconds_elapsed % 10 == 0) {
        if (base_speed < 15) base_speed += 1; // Increase base speed
    }
}

/* --- CONTROLS --- */
static void move_left(lv_event_t *e) {
    (void)e;
    player_x -= MOVE_STEP;
    if (player_x < 0) player_x = 0;
}

static void move_right(lv_event_t *e) {
    (void)e;
    player_x += MOVE_STEP;
    if (player_x > (SCREEN_WIDTH - PLAYER_SIZE)) {
        player_x = (SCREEN_WIDTH - PLAYER_SIZE);
    }
}

/* --- MENUS --- */
static void start_btn_handler(lv_event_t * e)
{
    if(start_menu_panel) {
        lv_obj_del(start_menu_panel);
        start_menu_panel = NULL;
    }
    start_gameplay_setup();
}

static void restart_event_handler(lv_event_t * e)
{
    if(game_over_panel) {
        lv_obj_del(game_over_panel);
        game_over_panel = NULL;
    }

    score = 0;
    prev_score = -1;
    base_speed = 4;
    seconds_elapsed = 0;
    player_x = (SCREEN_WIDTH / 2) - (PLAYER_SIZE / 2);

    spawn_history[0] = -1;
    spawn_history[1] = -1;

    for(int i = 0; i < MAX_ENEMIES; i++) {
        enemies_x[i] = get_new_enemy_x();
        enemies_y[i] = -50 - (i * ENEMY_SPACING);
        enemies_individual_speed[i] = base_speed;
    }

    lv_label_set_text(score_label, "Score: 0");
    lv_timer_resume(game_timer_handle);
    lv_timer_resume(level_timer_handle);
}

/* --- SETUP --- */
void start_gameplay_setup(void)
{
    score_label = lv_label_create(lv_scr_act());
    lv_label_set_text(score_label, "Score: 0");
    lv_obj_set_style_text_color(score_label, lv_color_hex(0xFFFFFF), 0);
    lv_obj_align(score_label, LV_ALIGN_TOP_MID, 0, 10);

    player = lv_obj_create(lv_scr_act());
    lv_obj_set_size(player, PLAYER_SIZE, PLAYER_SIZE);
    lv_obj_set_style_bg_opa(player, LV_OPA_0, 0);
    lv_obj_set_style_border_width(player, 0, 0);
    lv_obj_clear_flag(player, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_set_pos(player, player_x, SCREEN_HEIGHT - PLAYER_SIZE - 10);

    lv_obj_t * star_line = lv_line_create(player);
    lv_line_set_points(star_line, star_points, 5);
    lv_obj_add_style(star_line, &style_star, 0);
    lv_obj_center(star_line);

    for(int i = 0; i < MAX_ENEMIES; i++) {
        enemies[i] = lv_obj_create(lv_scr_act());
        lv_obj_add_style(enemies[i], &style_box, 0);
        lv_obj_set_size(enemies[i], ENEMY_SIZE, ENEMY_SIZE);
        lv_obj_clear_flag(enemies[i], LV_OBJ_FLAG_SCROLLABLE);

        enemies_x[i] = get_new_enemy_x();
        enemies_y[i] = -50 - (i * ENEMY_SPACING);
        enemies_individual_speed[i] = base_speed; // Start synced

        lv_obj_set_pos(enemies[i], enemies_x[i], (int)enemies_y[i]);
    }

    left_btn = lv_btn_create(lv_scr_act());
    lv_obj_set_size(left_btn, SCREEN_WIDTH / 2, SCREEN_HEIGHT);
    lv_obj_set_pos(left_btn, 0, 0);
    lv_obj_set_style_bg_opa(left_btn, LV_OPA_0, 0);
    lv_obj_add_event_cb(left_btn, move_left, LV_EVENT_CLICKED, NULL);

    right_btn = lv_btn_create(lv_scr_act());
    lv_obj_set_size(right_btn, SCREEN_WIDTH / 2, SCREEN_HEIGHT);
    lv_obj_set_pos(right_btn, SCREEN_WIDTH / 2, 0);
    lv_obj_set_style_bg_opa(right_btn, LV_OPA_0, 0);
    lv_obj_add_event_cb(right_btn, move_right, LV_EVENT_CLICKED, NULL);

    game_timer_handle = lv_timer_create(game_loop, 20, NULL);
    level_timer_handle = lv_timer_create(level_timer, 1000, NULL);
}

void show_game_over(void)
{
    lv_timer_pause(game_timer_handle);
    lv_timer_pause(level_timer_handle);
    update_leaderboard(score);

    game_over_panel = lv_obj_create(lv_scr_act());
    lv_obj_set_size(game_over_panel, 300, 240);
    lv_obj_center(game_over_panel);
    lv_obj_set_style_bg_color(game_over_panel, lv_color_hex(0x1a1a1a), 0);
    lv_obj_set_style_border_color(game_over_panel, lv_color_hex(0xFF0000), 0);
    lv_obj_set_style_border_width(game_over_panel, 3, 0);

    lv_obj_t * title = lv_label_create(game_over_panel);
    lv_label_set_text(title, "GAME OVER");
    lv_obj_add_style(title, &style_text_large, 0);
    lv_obj_set_style_text_color(title, lv_color_hex(0xFF4444), 0);
    lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 5);

    lv_obj_t * score_msg = lv_label_create(game_over_panel);
    char buf[64];
    sprintf(buf, "Your Score: %d", score);
    lv_label_set_text(score_msg, buf);
    lv_obj_set_style_text_color(score_msg, lv_color_hex(0xFFFFFF), 0);
    lv_obj_align(score_msg, LV_ALIGN_TOP_MID, 0, 45);

    lv_obj_t * lb_list = lv_label_create(game_over_panel);
    char lb_buf[128];
    sprintf(lb_buf, "1. %d\n2. %d\n3. %d", high_scores[0], high_scores[1], high_scores[2]);
    lv_label_set_text(lb_list, lb_buf);
    lv_obj_set_style_text_color(lb_list, lv_color_hex(0x00EEFF), 0);
    lv_obj_align(lb_list, LV_ALIGN_TOP_MID, 0, 80);

    lv_obj_t * btn = lv_btn_create(game_over_panel);
    lv_obj_set_size(btn, 140, 40);
    lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -5);
    lv_obj_set_style_bg_color(btn, lv_color_hex(0x00AACC), 0);
    lv_obj_add_event_cb(btn, restart_event_handler, LV_EVENT_CLICKED, NULL);

    lv_obj_t * btn_label = lv_label_create(btn);
    lv_label_set_text(btn_label, "TRY AGAIN");
    lv_obj_center(btn_label);
}

void create_start_menu(void)
{
    start_menu_panel = lv_obj_create(lv_scr_act());
    lv_obj_set_size(start_menu_panel, LV_PCT(100), LV_PCT(100));
    lv_obj_set_style_bg_color(start_menu_panel, lv_color_hex(0x101010), 0);
    lv_obj_set_style_border_width(start_menu_panel, 0, 0);
    lv_obj_clear_flag(start_menu_panel, LV_OBJ_FLAG_SCROLLABLE);

    lv_obj_t * title = lv_label_create(start_menu_panel);
    lv_label_set_text(title, "AMAN DODGER");
    lv_obj_add_style(title, &style_text_large, 0);
    lv_obj_set_style_text_color(title, lv_color_hex(0x00EEFF), 0);
    lv_obj_align(title, LV_ALIGN_CENTER, 0, -50);

    lv_obj_t * btn = lv_btn_create(start_menu_panel);
    lv_obj_set_size(btn, 160, 60);
    lv_obj_align(btn, LV_ALIGN_CENTER, 0, 40);
    lv_obj_set_style_bg_color(btn, lv_color_hex(0x00CC00), 0);
    lv_obj_add_event_cb(btn, start_btn_handler, LV_EVENT_CLICKED, NULL);

    lv_obj_t * btn_label = lv_label_create(btn);
    lv_label_set_text(btn_label, "START GAME");
    lv_obj_set_style_text_font(btn_label, &lv_font_montserrat_16, 0);
    lv_obj_center(btn_label);
}

void game_start(void)
{
    srand((unsigned)time(NULL));

    lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x101010), 0);

    lv_style_init(&style_box);
    lv_style_set_radius(&style_box, 2);
    lv_style_set_bg_color(&style_box, lv_color_hex(0xFF3333));
    lv_style_set_border_width(&style_box, 2);
    lv_style_set_border_color(&style_box, lv_color_hex(0x880000));

    lv_style_init(&style_star);
    lv_style_set_line_width(&style_star, 3);
    lv_style_set_line_color(&style_star, lv_color_hex(0x00EEFF));
    lv_style_set_line_rounded(&style_star, true);

    lv_style_init(&style_text_large);
    lv_style_set_text_font(&style_text_large, &lv_font_montserrat_16);

    create_start_menu();
}

Credits

AMAN DIPATUAN
1 project • 0 followers

Comments