Hardware components | ||||||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
| |||||
Hand tools and fabrication machines | ||||||
| ||||||
Snake OS is a highly polished, arcade-style game designed specifically for the Renesas HMI-Board. Unlike a traditional Snake game, this is an engineered piece of software running on the RT-Thread Real-Time Operating System using the LVGL (Light and Versatile Graphics Library) engine.
It transforms a standard microcontroller evaluation board into a fully interactive gaming console. It features a "Campaign Mode" where the arena evolves as you play, changing from a simple open field to a chaotic, obstacle-filled room with warping walls ("Portal Mode").
2. Why did I decide to make it?I developed Snake OS to solve a common problem in embedded engineering: Demonstrations are usually boring.
Showcase Performance: I wanted to push the Renesas chip to its limit, proving it can handle 50 FPS animations, particle physics, and game logic simultaneously without lagging.
- Showcase Performance: I wanted to push the Renesas chip to its limit, proving it can handle 50 FPS animations, particle physics, and game logic simultaneously without lagging.
Mastering HMI: I wanted to demonstrate proficiency in LVGL, showing that embedded GUIs can be just as fluid and responsive as smartphone apps.
- Mastering HMI: I wanted to demonstrate proficiency in LVGL, showing that embedded GUIs can be just as fluid and responsive as smartphone apps.
System Architecture: To prove that a Real-Time Operating System (RTOS) can handle user inputs (touch), logic processing, and display rendering in perfect sync.
- System Architecture: To prove that a Real-Time Operating System (RTOS) can handle user inputs (touch), logic processing, and display rendering in perfect sync.
The system is built on three distinct software layers:
A. The Game Engine (The Logic)The game runs on a Finite State Machine (FSM). This ensures the processor knows exactly what to do at any moment, whether it is calculating the snake's next move or waiting for the user to press "Resume" in the menu.
- Campaign System: The engine tracks the score. At specific milestones (10, 20, 30 points), it triggers a
load_level()function that physically alters the map array, adding walls or changing game rules (like turning on Portal Mode).
AI Autopilot: If the touch screen detects no input for 10 seconds, a Pathfinding Algorithm takes over. It scans the grid for the apple and calculates a safe path, allowing the board to play itself (perfect for kiosk displays).
- AI Autopilot: If the touch screen detects no input for 10 seconds, a Pathfinding Algorithm takes over. It scans the grid for the apple and calculates a safe path, allowing the board to play itself (perfect for kiosk displays).
To ensure the game feels "buttery smooth, " I decoupled the game logic from the drawing logic.
Logic: The snake moves every 50ms to 100ms (depending on difficulty).
- Logic: The snake moves every 50ms to 100ms (depending on difficulty).
Rendering: The screen updates every 20ms (50 FPS). This allows us to render smooth particle explosions and UI animations even if the snake itself is moving slowly.
- Rendering: The screen updates every 20ms (50 FPS). This allows us to render smooth particle explosions and UI animations even if the snake itself is moving slowly.
Instead of drawing simple pixels, the project uses advanced LVGL features:
Gradients: The snake body is rendered using lv_color_mix to create a fading color gradient from head to tail.
- Gradients: The snake body is rendered using
lv_color_mixto create a fading color gradient from head to tail.
Layers: The UI sits on a top layer (Z-index), ensuring the "Score" and "Pause" buttons never get covered by the snake.
- Layers: The UI sits on a top layer (Z-index), ensuring the "Score" and "Pause" buttons never get covered by the snake.
Animations: The Splash Screen uses lv_anim to create a smooth loading bar sequence on boot.
- Animations: The Splash Screen uses
lv_animto create a smooth loading bar sequence on boot.
SNAKE OS
C/C++1. What is it?
Snake OS (HMI Edition) is a specialized embedded software application that turns a Renesas HMI-Board into a dedicated handheld gaming console.
Unlike a typical PC game, this is a "Bare Metal" / RTOS application. This means the code runs directly on the microcontroller hardware, managing every pixel on the screen and every touch input in real-time without an operating system like Windows or Android getting in the way.
It serves as a Technical Demonstrator to show that:
RT-Thread (Real-Time OS) can manage complex logic states seamlessly.
LVGL (Graphics Library) can render high-framerate animations (50 FPS) on embedded chips.
Hardware Interaction (Touch & Display) can be highly responsive.
2. How do you use it?
Here is the user manual for interacting with the system:
Step 1: Power On & Boot
Action: Plug the Renesas board into a USB power source.
Result: The system boots up. You will see the Splash Screen animation with the "SNAKE OS" logo and a loading bar. This initializes the hardware drivers in the background.
Step 2: The Main Menu
Action: Once the loading finishes, you are presented with the Main Menu.
Options:
PLAY GAME: Starts a new session.
SETTINGS: Takes you to a toggle switch where you can manually turn "Portal Walls" ON or OFF (for Levels 1-3).
Step 3: Playing the Game
Controls: Use the large D-Pad (Arrow Keys) at the bottom of the screen to steer the snake.
Objective:
Eat the Red Apples to grow and gain points.
Avoid hitting the Gray Obstacles or your own tail.
If Portal Mode is active (Cyan Border), you can pass through walls to teleport to the other side.
Bonus: If you see a Golden Apple, eat it quickly! It gives you +5 points and removes a wall, but it disappears after 5 seconds.
Step 4: Pausing & Quitting
Pause: Tap the Pause Icon (||) in the top-right corner. The game freezes instantly.
Resume: Tap "RESUME" to see a "3... 2... 1..." countdown before the snake moves again.
Quit: Tap "QUIT GAME" to return to the main menu (and see a "You Gave Up" message).
Step 5: Game Over & Restart
Action: If you crash, the "GAME OVER" screen appears showing your Score and High Score (saved to memory).
Result: Tap "RESTART" to play again instantly.
Autopilot (Attract Mode)
Action: Stop touching the screen for 10 seconds.
Result: The AI takes over and plays the game automatically to attract new players. Touch the screen anywhere to regain control.
#include "ui.h"
#include "game.h"
#include <stdio.h>
/* ---------------------------------------------------------- */
/* OBJECTS & VARIABLES */
/* ---------------------------------------------------------- */
static lv_obj_t *scr_splash;
static lv_obj_t *scr_menu;
static lv_obj_t *scr_settings;
static lv_obj_t *scr_game;
static lv_obj_t *obj_board;
static lv_obj_t *obj_control_panel;
static lv_obj_t *label_score;
static lv_obj_t *label_high_score;
static lv_obj_t *label_level;
static lv_obj_t *label_demo;
static lv_obj_t *modal_game_over;
static lv_obj_t *modal_pause;
static lv_obj_t *modal_quit_msg;
static lv_obj_t *label_countdown;
static lv_obj_t *sw_portal_settings;
static lv_timer_t *timer_game = NULL;
static lv_timer_t *timer_countdown = NULL;
/* 50 FPS Rendering for smooth visuals */
#define RENDER_PERIOD_MS 20
static uint32_t time_accumulator = 0;
/* --- Styling --- */
#define SNAKE_COLOR_HEAD lv_color_hex(0x4CAF50)
#define SNAKE_COLOR_TAIL lv_color_hex(0xCCFF90)
#define FOOD_COLOR lv_color_hex(0xFF5252)
#define GOLDEN_FOOD_COLOR lv_color_hex(0xFFD700)
#define OBSTACLE_COLOR lv_color_hex(0x9E9E9E)
#define BOARD_BG_COLOR lv_color_hex(0x202020)
#define CONTROL_BG_COLOR lv_color_hex(0x303030)
#define PORTAL_BORDER_COLOR lv_color_hex(0x00E5FF)
#define NORMAL_BORDER_COLOR lv_color_hex(0x555555)
/* --- Forward Declarations --- */
static void game_tick_cb(lv_timer_t * t);
static void countdown_timer_cb(lv_timer_t * t);
static void board_draw_event_cb(lv_event_t * e);
static void btn_dir_event_cb(lv_event_t * e);
static void pause_btn_event_cb(lv_event_t * e);
static void pause_modal_event_cb(lv_event_t * e);
static void quit_msg_event_cb(lv_event_t * e);
static void restart_btn_event_cb(lv_event_t * e);
static void menu_event_cb(lv_event_t * e);
static void settings_event_cb(lv_event_t * e);
static void show_game_over(void);
static void show_quit_message(void);
static void start_resume_countdown(void);
static void create_dpad(lv_obj_t * parent);
static void create_menu_screen(void);
static void create_settings_screen(void);
static void create_game_screen(void);
static void splash_anim_ready_cb(lv_anim_t * a);
static void cleanup_game_resources(void);
/* ---------------------------------------------------------- */
/* 1. INITIALIZATION (SPLASH SCREEN) */
/* ---------------------------------------------------------- */
void ui_init(void) {
scr_splash = lv_obj_create(NULL);
lv_obj_set_style_bg_color(scr_splash, lv_color_black(), 0);
lv_scr_load(scr_splash);
lv_obj_t * logo_cont = lv_obj_create(scr_splash);
lv_obj_set_size(logo_cont, 180, 60);
lv_obj_align(logo_cont, LV_ALIGN_CENTER, 0, -60);
lv_obj_set_style_bg_color(logo_cont, lv_color_hex(0x4CAF50), 0);
lv_obj_set_style_radius(logo_cont, 10, 0);
lv_obj_set_style_border_width(logo_cont, 2, 0);
lv_obj_set_style_border_color(logo_cont, lv_color_white(), 0);
lv_obj_clear_flag(logo_cont, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t * label_logo = lv_label_create(logo_cont);
lv_label_set_text(label_logo, "SNAKE OS");
lv_obj_set_style_text_font(label_logo, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(label_logo, lv_color_white(), 0);
lv_obj_center(label_logo);
lv_obj_t * eye = lv_obj_create(logo_cont);
lv_obj_set_size(eye, 8, 8);
lv_obj_set_style_radius(eye, 4, 0);
lv_obj_set_style_bg_color(eye, lv_color_black(), 0);
lv_obj_align(eye, LV_ALIGN_RIGHT_MID, -10, -5);
lv_obj_t * label_devs = lv_label_create(scr_splash);
lv_label_set_text(label_devs, "Developed By:\nJoseph Vergara\nJP Maluya");
lv_obj_set_style_text_align(label_devs, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(label_devs, lv_color_white(), 0);
lv_obj_align(label_devs, LV_ALIGN_CENTER, 0, 20);
lv_obj_t * bar_loading = lv_bar_create(scr_splash);
lv_obj_set_size(bar_loading, 200, 10);
lv_obj_align(bar_loading, LV_ALIGN_BOTTOM_MID, 0, -40);
lv_obj_set_style_bg_color(bar_loading, lv_color_hex(0x333333), LV_PART_MAIN);
lv_obj_set_style_bg_color(bar_loading, lv_color_hex(0xFFD700), LV_PART_INDICATOR);
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, bar_loading);
lv_anim_set_values(&a, 0, 100);
lv_anim_set_time(&a, 3000);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_bar_set_value);
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
lv_anim_set_ready_cb(&a, splash_anim_ready_cb);
lv_anim_start(&a);
}
static void splash_anim_ready_cb(lv_anim_t * a) {
create_menu_screen();
lv_scr_load_anim(scr_menu, LV_SCR_LOAD_ANIM_FADE_ON, 500, 0, true);
}
static void cleanup_game_resources(void) {
if (timer_game != NULL) {
lv_timer_del(timer_game);
timer_game = NULL;
}
if (timer_countdown != NULL) {
lv_timer_del(timer_countdown);
timer_countdown = NULL;
}
}
/* ---------------------------------------------------------- */
/* 2. MENU SCREEN */
/* ---------------------------------------------------------- */
static void create_menu_screen(void) {
game.state = GAME_STATE_MENU;
scr_menu = lv_obj_create(NULL);
lv_obj_set_style_bg_color(scr_menu, BOARD_BG_COLOR, 0);
lv_obj_t * lbl_title = lv_label_create(scr_menu);
lv_label_set_text(lbl_title, "SNAKE OS");
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl_title, lv_color_hex(0x4CAF50), 0);
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 30);
lv_obj_t * btn_play = lv_btn_create(scr_menu);
lv_obj_set_size(btn_play, 160, 50);
lv_obj_align(btn_play, LV_ALIGN_CENTER, 0, -20);
lv_obj_add_event_cb(btn_play, menu_event_cb, LV_EVENT_CLICKED, (void*)1);
lv_obj_t * lbl_play = lv_label_create(btn_play);
lv_label_set_text(lbl_play, "PLAY GAME");
lv_obj_center(lbl_play);
lv_obj_t * btn_settings = lv_btn_create(scr_menu);
lv_obj_set_size(btn_settings, 160, 50);
lv_obj_align(btn_settings, LV_ALIGN_CENTER, 0, 40);
lv_obj_set_style_bg_color(btn_settings, lv_color_hex(0x555555), 0);
lv_obj_add_event_cb(btn_settings, menu_event_cb, LV_EVENT_CLICKED, (void*)2);
lv_obj_t * lbl_set = lv_label_create(btn_settings);
lv_label_set_text(lbl_set, "SETTINGS");
lv_obj_center(lbl_set);
}
static void menu_event_cb(lv_event_t * e) {
int code = (int)(intptr_t)lv_event_get_user_data(e);
if (code == 1) {
create_game_screen();
lv_scr_load_anim(scr_game, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, true);
} else if (code == 2) {
create_settings_screen();
lv_scr_load_anim(scr_settings, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, true);
}
}
/* ---------------------------------------------------------- */
/* 3. SETTINGS SCREEN */
/* ---------------------------------------------------------- */
static void create_settings_screen(void) {
game.state = GAME_STATE_SETTINGS;
scr_settings = lv_obj_create(NULL);
lv_obj_set_style_bg_color(scr_settings, BOARD_BG_COLOR, 0);
lv_obj_t * lbl = lv_label_create(scr_settings);
lv_label_set_text(lbl, "SETTINGS");
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 20);
lv_obj_t * panel = lv_obj_create(scr_settings);
lv_obj_set_size(panel, 240, 60);
lv_obj_align(panel, LV_ALIGN_CENTER, 0, -30);
lv_obj_set_style_bg_color(panel, CONTROL_BG_COLOR, 0);
lv_obj_set_style_border_width(panel, 0, 0);
lv_obj_t * lbl_sw = lv_label_create(panel);
lv_label_set_text(lbl_sw, "Portal Walls");
lv_obj_set_style_text_color(lbl_sw, lv_color_white(), 0);
lv_obj_align(lbl_sw, LV_ALIGN_LEFT_MID, 0, 0);
sw_portal_settings = lv_switch_create(panel);
if (game.user_portal_setting) lv_obj_add_state(sw_portal_settings, LV_STATE_CHECKED);
lv_obj_align(sw_portal_settings, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(sw_portal_settings, settings_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_t * btn_back = lv_btn_create(scr_settings);
lv_obj_set_size(btn_back, 100, 40);
lv_obj_align(btn_back, LV_ALIGN_BOTTOM_MID, 0, -30);
lv_obj_add_event_cb(btn_back, settings_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t * lbl_back = lv_label_create(btn_back);
lv_label_set_text(lbl_back, "BACK");
lv_obj_center(lbl_back);
}
static void settings_event_cb(lv_event_t * e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * obj = lv_event_get_target(e);
if (code == LV_EVENT_VALUE_CHANGED) {
bool active = lv_obj_has_state(obj, LV_STATE_CHECKED);
game_set_portal_mode(active);
}
else if (code == LV_EVENT_CLICKED) {
create_menu_screen();
lv_scr_load_anim(scr_menu, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 300, 0, true);
}
}
/* ---------------------------------------------------------- */
/* 4. GAME SCREEN */
/* ---------------------------------------------------------- */
static void create_game_screen(void) {
cleanup_game_resources();
game_reset();
game.state = GAME_STATE_IDLE;
time_accumulator = 0;
scr_game = lv_obj_create(NULL);
lv_obj_set_style_bg_color(scr_game, lv_color_black(), 0);
/* Board Area */
obj_board = lv_obj_create(scr_game);
lv_obj_set_size(obj_board, 300, 272);
lv_obj_align(obj_board, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_bg_color(obj_board, BOARD_BG_COLOR, 0);
lv_obj_set_style_border_width(obj_board, 2, 0);
if(game.portal_mode) lv_obj_set_style_border_color(obj_board, PORTAL_BORDER_COLOR, 0);
else lv_obj_set_style_border_color(obj_board, NORMAL_BORDER_COLOR, 0);
lv_obj_set_scrollbar_mode(obj_board, LV_SCROLLBAR_MODE_OFF);
lv_obj_clear_flag(obj_board, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(obj_board, board_draw_event_cb, LV_EVENT_DRAW_MAIN, NULL);
/* Auto Pilot Label */
label_demo = lv_label_create(scr_game);
lv_label_set_text(label_demo, "AUTO PILOT");
lv_obj_set_style_text_font(label_demo, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(label_demo, lv_color_hex(0xFFD700), 0);
lv_obj_set_style_bg_color(label_demo, lv_color_black(), 0);
lv_obj_set_style_bg_opa(label_demo, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(label_demo, 8, 0);
lv_obj_set_style_radius(label_demo, 4, 0);
lv_obj_set_style_border_width(label_demo, 1, 0);
lv_obj_set_style_border_color(label_demo, lv_color_hex(0xFFD700), 0);
lv_obj_align_to(label_demo, obj_board, LV_ALIGN_TOP_MID, 0, 15);
lv_obj_add_flag(label_demo, LV_OBJ_FLAG_HIDDEN);
/* Control Panel */
obj_control_panel = lv_obj_create(scr_game);
lv_obj_set_size(obj_control_panel, 180, 272);
lv_obj_align(obj_control_panel, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(obj_control_panel, CONTROL_BG_COLOR, 0);
lv_obj_set_style_border_width(obj_control_panel, 0, 0);
label_score = lv_label_create(obj_control_panel);
lv_label_set_text(label_score, "Score: 0");
lv_obj_set_style_text_font(label_score, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(label_score, lv_color_white(), 0);
lv_obj_align(label_score, LV_ALIGN_TOP_LEFT, 10, 10);
label_high_score = lv_label_create(obj_control_panel);
lv_label_set_text(label_high_score, "Best: 0");
lv_obj_set_style_text_font(label_high_score, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(label_high_score, lv_color_hex(0xFFD700), 0);
lv_obj_align(label_high_score, LV_ALIGN_TOP_LEFT, 10, 30);
label_level = lv_label_create(obj_control_panel);
lv_label_set_text(label_level, "Level 1");
lv_obj_set_style_text_font(label_level, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(label_level, lv_color_hex(0x00E5FF), 0);
lv_obj_align(label_level, LV_ALIGN_TOP_LEFT, 10, 50);
/* Pause Button (Top Right) */
lv_obj_t * btn_pause = lv_btn_create(obj_control_panel);
lv_obj_set_size(btn_pause, 40, 40);
lv_obj_align(btn_pause, LV_ALIGN_TOP_RIGHT, -10, 10);
lv_obj_set_style_bg_color(btn_pause, lv_color_hex(0x555555), 0);
lv_obj_add_event_cb(btn_pause, pause_btn_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t * lbl_pause = lv_label_create(btn_pause);
lv_label_set_text(lbl_pause, LV_SYMBOL_PAUSE);
lv_obj_center(lbl_pause);
create_dpad(obj_control_panel);
timer_game = lv_timer_create(game_tick_cb, RENDER_PERIOD_MS, NULL);
}
static void create_dpad(lv_obj_t * parent) {
const int btn_size = 48;
const int center_offset_y = 30;
lv_obj_t * btn_up = lv_btn_create(parent);
lv_obj_set_size(btn_up, btn_size, btn_size);
lv_obj_align(btn_up, LV_ALIGN_CENTER, 0, center_offset_y - btn_size);
lv_obj_set_style_bg_color(btn_up, lv_palette_main(LV_PALETTE_BLUE), 0);
lv_obj_t * lbl = lv_label_create(btn_up);
lv_label_set_text(lbl, LV_SYMBOL_UP);
lv_obj_center(lbl);
lv_obj_add_event_cb(btn_up, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_UP);
lv_obj_t * btn_down = lv_btn_create(parent);
lv_obj_set_size(btn_down, btn_size, btn_size);
lv_obj_align(btn_down, LV_ALIGN_CENTER, 0, center_offset_y + btn_size);
lv_obj_set_style_bg_color(btn_down, lv_palette_main(LV_PALETTE_BLUE), 0);
lbl = lv_label_create(btn_down);
lv_label_set_text(lbl, LV_SYMBOL_DOWN);
lv_obj_center(lbl);
lv_obj_add_event_cb(btn_down, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_DOWN);
lv_obj_t * btn_left = lv_btn_create(parent);
lv_obj_set_size(btn_left, btn_size, btn_size);
lv_obj_align(btn_left, LV_ALIGN_CENTER, -btn_size, center_offset_y);
lv_obj_set_style_bg_color(btn_left, lv_palette_main(LV_PALETTE_BLUE), 0);
lbl = lv_label_create(btn_left);
lv_label_set_text(lbl, LV_SYMBOL_LEFT);
lv_obj_center(lbl);
lv_obj_add_event_cb(btn_left, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_LEFT);
lv_obj_t * btn_right = lv_btn_create(parent);
lv_obj_set_size(btn_right, btn_size, btn_size);
lv_obj_align(btn_right, LV_ALIGN_CENTER, btn_size, center_offset_y);
lv_obj_set_style_bg_color(btn_right, lv_palette_main(LV_PALETTE_BLUE), 0);
lbl = lv_label_create(btn_right);
lv_label_set_text(lbl, LV_SYMBOL_RIGHT);
lv_obj_center(lbl);
lv_obj_add_event_cb(btn_right, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_RIGHT);
}
/* --- PAUSE SYSTEM --- */
static void pause_btn_event_cb(lv_event_t * e) {
if (game.state != GAME_STATE_PLAYING && game.state != GAME_STATE_PAUSED) return;
game_toggle_pause();
if (game.state == GAME_STATE_PAUSED) {
modal_pause = lv_obj_create(scr_game);
lv_obj_set_size(modal_pause, 200, 160);
lv_obj_center(modal_pause);
lv_obj_set_style_bg_color(modal_pause, lv_color_hex(0x444444), 0);
lv_obj_set_style_border_color(modal_pause, lv_color_white(), 0);
lv_obj_t * lbl = lv_label_create(modal_pause);
lv_label_set_text(lbl, "PAUSED");
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 10);
lv_obj_t * btn_res = lv_btn_create(modal_pause);
lv_obj_set_size(btn_res, 140, 40);
lv_obj_align(btn_res, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(btn_res, pause_modal_event_cb, LV_EVENT_CLICKED, (void*)1);
lv_obj_t * lbl_res = lv_label_create(btn_res);
lv_label_set_text(lbl_res, "RESUME");
lv_obj_center(lbl_res);
lv_obj_t * btn_quit = lv_btn_create(modal_pause);
lv_obj_set_size(btn_quit, 140, 40);
lv_obj_align(btn_quit, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_set_style_bg_color(btn_quit, lv_color_hex(0xFF5252), 0);
lv_obj_add_event_cb(btn_quit, pause_modal_event_cb, LV_EVENT_CLICKED, (void*)2);
lv_obj_t * lbl_quit = lv_label_create(btn_quit);
lv_label_set_text(lbl_quit, "QUIT GAME");
lv_obj_center(lbl_quit);
} else {
if(modal_pause) {
lv_obj_del(modal_pause);
modal_pause = NULL;
}
}
}
static void pause_modal_event_cb(lv_event_t * e) {
int code = (int)(intptr_t)lv_event_get_user_data(e);
lv_obj_del(modal_pause);
modal_pause = NULL;
if (code == 1) { /* Resume */
start_resume_countdown();
} else if (code == 2) { /* Quit */
show_quit_message();
}
}
static void start_resume_countdown(void) {
game.state = GAME_STATE_RESUME_COUNTDOWN;
game.countdown_val = 3;
label_countdown = lv_label_create(scr_game);
lv_obj_set_style_text_font(label_countdown, &lv_font_montserrat_14, 0);
lv_obj_set_style_transform_zoom(label_countdown, 1024, 0);
lv_obj_set_style_text_color(label_countdown, lv_color_hex(0xFFD700), 0);
lv_label_set_text(label_countdown, "3");
lv_obj_align_to(label_countdown, obj_board, LV_ALIGN_CENTER, 0, 0);
timer_countdown = lv_timer_create(countdown_timer_cb, 1000, NULL);
}
static void countdown_timer_cb(lv_timer_t * t) {
game.countdown_val--;
if (game.countdown_val > 0) {
lv_label_set_text_fmt(label_countdown, "%d", game.countdown_val);
} else {
lv_obj_del(label_countdown);
lv_timer_del(timer_countdown);
timer_countdown = NULL;
game.state = GAME_STATE_PLAYING;
time_accumulator = 0;
}
}
static void show_quit_message(void) {
modal_quit_msg = lv_obj_create(scr_game);
lv_obj_set_size(modal_quit_msg, 220, 120);
lv_obj_center(modal_quit_msg);
lv_obj_set_style_bg_color(modal_quit_msg, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(modal_quit_msg, 2, 0);
lv_obj_set_style_border_color(modal_quit_msg, lv_color_hex(0xFF5252), 0);
lv_obj_t * lbl = lv_label_create(modal_quit_msg);
lv_label_set_text(lbl, "YOU GAVE UP!\n(Weak...)");
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(0xFF5252), 0);
lv_obj_center(lbl);
lv_obj_t * btn = lv_btn_create(modal_quit_msg);
lv_obj_set_size(btn, 220, 120);
lv_obj_center(btn);
lv_obj_set_style_bg_opa(btn, LV_OPA_TRANSP, 0);
lv_obj_add_event_cb(btn, quit_msg_event_cb, LV_EVENT_CLICKED, NULL);
}
static void quit_msg_event_cb(lv_event_t * e) {
cleanup_game_resources();
create_menu_screen();
lv_scr_load_anim(scr_menu, LV_SCR_LOAD_ANIM_FADE_ON, 500, 0, true);
}
static void game_tick_cb(lv_timer_t * t) {
if (game.state == GAME_STATE_IDLE || game.state == GAME_STATE_OVER) {
game_process_idle();
}
game_update_particles();
if (game.state == GAME_STATE_AI_PLAYING && modal_game_over) {
lv_obj_del(modal_game_over);
modal_game_over = NULL;
}
if (game.state == GAME_STATE_AI_PLAYING) {
lv_obj_clear_flag(label_demo, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(label_demo);
} else if (label_demo) {
lv_obj_add_flag(label_demo, LV_OBJ_FLAG_HIDDEN);
}
if (obj_board) {
if(game.portal_mode) lv_obj_set_style_border_color(obj_board, PORTAL_BORDER_COLOR, 0);
else lv_obj_set_style_border_color(obj_board, NORMAL_BORDER_COLOR, 0);
}
if (game.state == GAME_STATE_PLAYING || game.state == GAME_STATE_AI_PLAYING) {
time_accumulator += RENDER_PERIOD_MS;
int catch_up_limit = 0;
while (time_accumulator >= game.speed_ms && catch_up_limit < 2) {
game_update();
time_accumulator -= game.speed_ms;
catch_up_limit++;
}
if (time_accumulator > 300) time_accumulator = 0;
lv_label_set_text_fmt(label_score, "Score: %d", (int)game.score);
lv_label_set_text_fmt(label_high_score, "Best: %d", (int)game.high_score);
lv_label_set_text_fmt(label_level, "Level %d", (int)game.level);
lv_obj_invalidate(obj_board);
if (game.state == GAME_STATE_OVER) {
show_game_over();
}
}
}
static void btn_dir_event_cb(lv_event_t * e) {
if (game.state == GAME_STATE_MENU || game.state == GAME_STATE_SETTINGS ||
game.state == GAME_STATE_PAUSED || game.state == GAME_STATE_RESUME_COUNTDOWN) return;
if (game.state == GAME_STATE_IDLE || game.state == GAME_STATE_OVER || game.state == GAME_STATE_AI_PLAYING) {
if(game.state == GAME_STATE_OVER) game_reset();
if(game.state == GAME_STATE_AI_PLAYING) game_reset();
if (modal_game_over) {
lv_obj_del(modal_game_over);
modal_game_over = NULL;
}
game.state = GAME_STATE_PLAYING;
}
direction_t dir = (direction_t)(intptr_t)lv_event_get_user_data(e);
game_set_direction(dir);
}
static void restart_btn_event_cb(lv_event_t * e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
game_reset();
game.state = GAME_STATE_PLAYING;
if (modal_game_over) {
lv_obj_del(modal_game_over);
modal_game_over = NULL;
}
lv_label_set_text(label_score, "Score: 0");
}
}
static void board_draw_event_cb(lv_event_t * e) {
lv_draw_ctx_t * draw_ctx = lv_event_get_draw_ctx(e);
lv_draw_rect_dsc_t rect_dsc;
lv_draw_rect_dsc_init(&rect_dsc);
lv_obj_t * obj = lv_event_get_target(e);
lv_area_t obj_coords;
lv_obj_get_coords(obj, &obj_coords);
int offset_x = obj_coords.x1;
int offset_y = obj_coords.y1 + GRID_OFFSET_Y;
rect_dsc.bg_color = FOOD_COLOR;
rect_dsc.radius = CELL_SIZE / 2;
lv_area_t area;
area.x1 = offset_x + game.food.x * CELL_SIZE;
area.y1 = offset_y + game.food.y * CELL_SIZE;
area.x2 = area.x1 + CELL_SIZE - 2;
area.y2 = area.y1 + CELL_SIZE - 2;
lv_draw_rect(draw_ctx, &rect_dsc, &area);
if (game.golden_active) {
rect_dsc.bg_color = GOLDEN_FOOD_COLOR;
area.x1 = offset_x + game.golden_food.x * CELL_SIZE;
area.y1 = offset_y + game.golden_food.y * CELL_SIZE;
area.x2 = area.x1 + CELL_SIZE - 2;
area.y2 = area.y1 + CELL_SIZE - 2;
lv_draw_rect(draw_ctx, &rect_dsc, &area);
}
rect_dsc.bg_color = OBSTACLE_COLOR;
rect_dsc.radius = 2;
for (int i = 0; i < game.obstacle_count; i++) {
area.x1 = offset_x + game.obstacles[i].x * CELL_SIZE;
area.y1 = offset_y + game.obstacles[i].y * CELL_SIZE;
area.x2 = area.x1 + CELL_SIZE - 2;
area.y2 = area.y1 + CELL_SIZE - 2;
lv_draw_rect(draw_ctx, &rect_dsc, &area);
}
rect_dsc.radius = 4;
for (int i = 0; i < game.length; i++) {
uint8_t ratio = 0;
if (game.length > 1) ratio = (i * 255) / (game.length - 1);
rect_dsc.bg_color = lv_color_mix(SNAKE_COLOR_TAIL, SNAKE_COLOR_HEAD, ratio);
area.x1 = offset_x + game.body[i].x * CELL_SIZE;
area.y1 = offset_y + game.body[i].y * CELL_SIZE;
area.x2 = area.x1 + CELL_SIZE - 2;
area.y2 = area.y1 + CELL_SIZE - 2;
lv_draw_rect(draw_ctx, &rect_dsc, &area);
if (i == 0) {
lv_draw_rect_dsc_t eye_dsc;
lv_draw_rect_dsc_init(&eye_dsc);
eye_dsc.bg_color = lv_color_white();
eye_dsc.radius = 10;
lv_draw_rect_dsc_t pupil_dsc;
lv_draw_rect_dsc_init(&pupil_dsc);
pupil_dsc.bg_color = lv_color_black();
pupil_dsc.radius = 10;
int hx = area.x1; int hy = area.y1;
int eye_size = 6; int pupil_size = 2;
lv_area_t eye1, eye2, p1, p2;
int e1_x, e1_y, e2_x, e2_y, p_off_x, p_off_y;
switch(game.current_dir) {
case DIR_UP: e1_x = hx+2; e1_y = hy+2; e2_x = hx+12; e2_y = hy+2; p_off_x = 2; p_off_y = 0; break;
case DIR_DOWN: e1_x = hx+2; e1_y = hy+10; e2_x = hx+12; e2_y = hy+10; p_off_x = 2; p_off_y = 4; break;
case DIR_LEFT: e1_x = hx+2; e1_y = hy+2; e2_x = hx+2; e2_y = hy+12; p_off_x = 0; p_off_y = 2; break;
case DIR_RIGHT: default: e1_x = hx+10; e1_y = hy+2; e2_x = hx+10; e2_y = hy+12; p_off_x = 4; p_off_y = 2; break;
}
eye1.x1=e1_x; eye1.y1=e1_y; eye1.x2=e1_x+eye_size; eye1.y2=e1_y+eye_size;
eye2.x1=e2_x; eye2.y1=e2_y; eye2.x2=e2_x+eye_size; eye2.y2=e2_y+eye_size;
p1.x1=e1_x+(eye_size/2)-(pupil_size/2)+(p_off_x-2); p1.y1=e1_y+(eye_size/2)-(pupil_size/2)+(p_off_y-2);
p1.x2=p1.x1+pupil_size; p1.y2=p1.y1+pupil_size;
p2.x1=e2_x+(eye_size/2)-(pupil_size/2)+(p_off_x-2); p2.y1=e2_y+(eye_size/2)-(pupil_size/2)+(p_off_y-2);
p2.x2=p2.x1+pupil_size; p2.y2=p2.y1+pupil_size;
lv_draw_rect(draw_ctx, &eye_dsc, &eye1);
lv_draw_rect(draw_ctx, &eye_dsc, &eye2);
lv_draw_rect(draw_ctx, &pupil_dsc, &p1);
lv_draw_rect(draw_ctx, &pupil_dsc, &p2);
}
}
for (int i = 0; i < MAX_PARTICLES; i++) {
if (game.particles[i].active) {
rect_dsc.bg_color = lv_color_hex(game.particles[i].color);
rect_dsc.radius = 0;
int px = offset_x + game.particles[i].x;
int py = offset_y + game.particles[i].y;
int size = 4;
area.x1 = px; area.y1 = py; area.x2 = px + size; area.y2 = py + size;
lv_draw_rect(draw_ctx, &rect_dsc, &area);
}
}
}
static void show_game_over(void) {
if (modal_game_over) return;
modal_game_over = lv_obj_create(scr_game);
lv_obj_set_size(modal_game_over, 220, 180);
lv_obj_center(modal_game_over);
lv_obj_set_style_bg_color(modal_game_over, lv_color_hex(0x444444), 0);
lv_obj_set_style_border_color(modal_game_over, lv_color_white(), 0);
lv_obj_t * lbl = lv_label_create(modal_game_over);
lv_label_set_text(lbl, "GAME OVER");
lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(0xFF5252), 0);
lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 10);
lv_obj_t * score_lbl = lv_label_create(modal_game_over);
lv_label_set_text_fmt(score_lbl, "Score: %d\nBest: %d", (int)game.score, (int)game.high_score);
lv_obj_set_style_text_align(score_lbl, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_style_text_color(score_lbl, lv_color_white(), 0);
lv_obj_align(score_lbl, LV_ALIGN_TOP_MID, 0, 50);
lv_obj_t * btn = lv_btn_create(modal_game_over);
lv_obj_set_size(btn, 120, 50);
lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(btn, restart_btn_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t * btn_lbl = lv_label_create(btn);
lv_label_set_text(btn_lbl, "RESTART");
lv_obj_center(btn_lbl);
}








Comments