diff --git a/Makefile b/Makefile index 96a5a37..e5d2264 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,12 @@ CC := gcc LIBS := `pkg-config --libs sdl3` INC := `pkg-config --cflags sdl3` -I./include -SRC := src/main.c \ - src/engine/engine.c \ +SRC := src/engine/engine.c \ src/engine/engine_init.c \ src/engine/render.c OUT := build/main +SNAKE_SRC := src/examples/snake.c $(SRC) +SNAKE := build/snake build: $(CC) -o $(OUT) $(SRC) $(INC) $(LIBS) @@ -13,6 +14,15 @@ build: run: $(OUT) +build-snake: + $(CC) -o $(SNAKE) $(SNAKE_SRC) $(INC) $(LIBS) + +run-snake: + $(SNAKE) + +clean-snake: + rm $(SNAKE) + clean: rm $(OUT) diff --git a/src/examples/snake.c b/src/examples/snake.c new file mode 100644 index 0000000..46f6c0f --- /dev/null +++ b/src/examples/snake.c @@ -0,0 +1,276 @@ +#include "engine.h" +#include +#include +#include +#include + +bool left = true; + +typedef struct { + int32_t x; + int32_t y; +} v2; + +#define SNAKE_MAX_LENGTH 64 + +typedef struct { + // position of the head of the snake in the framebuffer + v2 head_pos; + // -1 or +1 for both x and y + v2 dir; + // tail length + 1 for the head + uint16_t length; + // width and/or height + uint8_t size; + v2 body[SNAKE_MAX_LENGTH]; + uint64_t move_interval; + uint64_t next_move_time; + bool is_apple_alive; + bool direction_pending; + v2 pending_dir; + v2 apple_pos; +} snake_state_t; + +snake_state_t snake; + +bool +is_opposite_direction(v2 a, v2 b) +{ + return a.x == -b.x && a.y == -b.y; +} + +void +queue_direction(v2 new_dir) +{ + if (is_opposite_direction(new_dir, snake.dir)) { + return; + } + + // ignore additional direction inputs until the next movement + if (snake.direction_pending) { + return; + } + + snake.pending_dir = new_dir; + snake.direction_pending = true; +} + +// polling events every frame +void +client_poll_event() +{ + SDL_Event ev = game_state->event; + + if (ev.type == SDL_EVENT_QUIT) { + game_state->is_running = false; + } + + if (ev.type == SDL_EVENT_KEY_DOWN) { + switch (ev.key.key) { + case SDLK_Q: + game_state->is_running = false; + break; + case SDLK_W: + queue_direction((v2){0, -1}); + break; + case SDLK_S: + queue_direction((v2){0, 1}); + break; + case SDLK_A: + queue_direction((v2){-1, 0}); + break; + case SDLK_D: + queue_direction((v2){1, 0}); + break; + default: + break; + } + } +} + +/** + * draws a rectangle with its left-top corner at (x,y) with + * a width of 'w' and height of 'h' filled with color 'color' + */ +void +put_rect( + unsigned int x, + unsigned int y, + unsigned int w, + unsigned int h, + uint32_t color +) { + for (int i = x; i < x + w; i++) { + for (int j = y; j < y + h; j++) { + put_pixel(i, j, color); + } + } +} + +// a wrapper function for put_rect using a v2 struct +void +put_rect_v2( + v2 pos, + unsigned int w, + unsigned int h, + uint32_t color +) { + put_rect(pos.x, pos.y, w, h, color); +} + +bool +v2_eq( + v2 x, + v2 y +) { + return x.x == y.x && x.y == y.y; +} + +void +advance_snake_pos() +{ + if (snake.direction_pending) { + snake.dir = snake.pending_dir; + snake.direction_pending = false; + } + + int32_t new_head_x = (snake.body[0].x + snake.dir.x * snake.size) % game_state->width; + if (new_head_x < 0) new_head_x = game_state->width - snake.size; + int32_t new_head_y = (snake.body[0].y + snake.dir.y * snake.size) % game_state->height; + if (new_head_y < 0) new_head_y = game_state->height - snake.size; + + for (int i = snake.length - 1; i > 0; i--) { + snake.body[i] = snake.body[i - 1]; + } + + snake.body[0] = (v2){new_head_x, new_head_y}; +} + +void +spawn_apple_at_random_pos() +{ + snake.apple_pos = (v2){ + snake.size * (rand() % (game_state->width / snake.size)), + snake.size * (rand() % (game_state->height / snake.size)) + }; + snake.is_apple_alive = true; +} + +void +calculate_apple_collisions() +{ + if (snake.is_apple_alive && + v2_eq(snake.body[0], snake.apple_pos) + ) { + snake.is_apple_alive = false; + if (snake.length < SNAKE_MAX_LENGTH) { + snake.length++; + //snake.body[snake.length] = snake.body[snake.length - 1]; + } + } +} + +void +calculate_snake_death_collisions() +{ + for (int i = 1; i < snake.length; i++) { + if (v2_eq(snake.body[0], snake.body[i])) { + game_state->is_running = false; + } + } +} + +/** + * performed every frame + * runs on a separate worker thread - you can take advantage + * of sleep or usleep from to create delays + */ +void +client_update_framebuffer() +{ + uint64_t now = SDL_GetTicks(); + + if (now >= snake.next_move_time) { + + // ------- business logic -------- + + if (!snake.is_apple_alive) { + spawn_apple_at_random_pos(); + } + + calculate_apple_collisions(); + + calculate_snake_death_collisions(); + + advance_snake_pos(); + + // ------------------------------- + + snake.next_move_time += snake.move_interval; + + if (snake.next_move_time < now) { + snake.next_move_time = now + snake.move_interval; + } + + clear_framebuffer(0x000000); + + // draw the apple + if (snake.is_apple_alive) { + put_rect_v2( + snake.apple_pos, + snake.size, + snake.size, + 0xFF0000 + ); + } + + // draw the head + put_rect( + snake.body[0].x, snake.body[0].y, + snake.size, snake.size, 0x00FF00 + ); + // draw the tail + for (int i = 1; i < snake.length; i++) { + put_rect( + snake.body[i].x, snake.body[i].y, + snake.size, snake.size, 0x00AA00 + ); + } + } +} + +void +snake_init() +{ + srand(time(NULL)); + + // start at the left-top corner and go right + snake.body[0] = (v2){0, 0}; + snake.dir = (v2){1, 0}; + snake.length = 1; + snake.size = 20; + snake.move_interval = 300; + snake.next_move_time = snake.move_interval; + snake.is_apple_alive = false; + snake.pending_dir = snake.dir; + snake.direction_pending = false; +} + +int +main() +{ + // initialize the snake vars + snake_init(); + + // initialize the engine + if (engine_init( + client_poll_event, + client_update_framebuffer + )) { + engine_start(); + engine_stop(); + return 0; + } else { + return 1; + } +} \ No newline at end of file diff --git a/src/main.c b/src/main.c index dd79fa2..335912c 100644 --- a/src/main.c +++ b/src/main.c @@ -1,10 +1,9 @@ #include "engine.h" -bool left = true; - // polling events every frame void -client_poll_event() { +client_poll_event() +{ SDL_Event ev = game_state->event; if (ev.type == SDL_EVENT_QUIT) { @@ -15,43 +14,28 @@ client_poll_event() { switch (ev.key.key) { case SDLK_Q: game_state->is_running = false; + break; + default: + break; } } } -// performed every frame +/** + * performed every frame + * runs on a separate worker thread - you can take advantage + * of sleep or usleep from to create delays + */ void -client_update_framebuffer() { - // draw a green square - // clear_framebuffer(0x000000); - // for (int i = 0; i < 20; i++) { - // for (int j = 0; j < 20; j++) { - // put_pixel(i, j, 0x00FF00); - // } - // } - - // draw alternating green squares - if (left) { - clear_framebuffer(0x000000); - for (int i = 0; i < 20; i++) { - for (int j = 0; j < 20; j++) { - put_pixel(i, j, 0x00FF00); - } - } - left = false; - } else { - clear_framebuffer(0x000000); - for (int i = 20; i < 40; i++) { - for (int j = 20; j < 40; j++) { - put_pixel(i, j, 0x00FFFF); - } - } - left = true; - } - SDL_Delay(100); +client_update_framebuffer() +{ + // single green demo pixel + game_state->framebuffer[0] = 0x00FF00; } -int main() { +int +main() +{ // initialize the engine if (engine_init( client_poll_event,