diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4f82aa934e..50364cc90a 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -186,6 +186,7 @@ add_sdl_example_executable(renderer-primitives SOURCES renderer/02-primitives/re add_sdl_example_executable(audio-simple-playback SOURCES audio/01-simple-playback/simple-playback.c) add_sdl_example_executable(audio-simple-playback-callback SOURCES audio/02-simple-playback-callback/simple-playback-callback.c) add_sdl_example_executable(audio-load-wav SOURCES audio/03-load-wav/load-wav.c DATAFILES ${CMAKE_CURRENT_SOURCE_DIR}/../test/sample.wav) +add_sdl_example_executable(snake SOURCES game/01-snake/main.c game/01-snake/snake.c) if(PSP) diff --git a/examples/game/01-snake/main.c b/examples/game/01-snake/main.c new file mode 100644 index 0000000000..37c873bcfb --- /dev/null +++ b/examples/game/01-snake/main.c @@ -0,0 +1,161 @@ +/* + * This example code implements a Snake game that showcases some of the + * functionalities of SDL, such as timer callbacks and event handling. + * + * This code is public domain. Feel free to use it for any purpose! + */ +#define SDL_MAIN_USE_CALLBACKS 1 /* use the callbacks instead of main() */ +#include +#include +#include /* malloc(), free() */ + +#include "snake.h" + +#define STEP_RATE_IN_MILLISECONDS 125 +#define SNAKE_BLOCK_SIZE_IN_PIXELS 24 +#define SDL_WINDOW_WIDTH (SNAKE_BLOCK_SIZE_IN_PIXELS * SNAKE_GAME_WIDTH) +#define SDL_WINDOW_HEIGHT (SNAKE_BLOCK_SIZE_IN_PIXELS * SNAKE_GAME_HEIGHT) + +typedef struct +{ + SDL_Window *window; + SDL_Renderer *renderer; + SDL_TimerID step_timer; + SnakeContext snake_ctx; +} AppState; + +static Uint32 sdl_timer_callback_(void *payload, SDL_TimerID timer_id, Uint32 interval) +{ + SDL_Event e; + SDL_UserEvent ue; + /* NOTE: snake_step is not called here directly for multithreaded concerns. */ + (void)payload; + ue.type = SDL_EVENT_USER; + ue.code = 0; + ue.data1 = NULL; + ue.data2 = NULL; + e.type = SDL_EVENT_USER; + e.user = ue; + SDL_PushEvent(&e); + return interval; +} + +static int handle_key_event_(SnakeContext *ctx, SDL_Scancode key_code) +{ + switch (key_code) { + /* Quit. */ + case SDL_SCANCODE_ESCAPE: + case SDL_SCANCODE_Q: + return SDL_APP_SUCCESS; + /* Restart the game as if the program was launched. */ + case SDL_SCANCODE_R: + snake_initialize(ctx, SDL_rand); + break; + /* Decide new direction of the snake. */ + case SDL_SCANCODE_RIGHT: + snake_redir(ctx, SNAKE_DIR_RIGHT); + break; + case SDL_SCANCODE_UP: + snake_redir(ctx, SNAKE_DIR_UP); + break; + case SDL_SCANCODE_LEFT: + snake_redir(ctx, SNAKE_DIR_LEFT); + break; + case SDL_SCANCODE_DOWN: + snake_redir(ctx, SNAKE_DIR_DOWN); + break; + default: + break; + } + return SDL_APP_CONTINUE; +} + +static void set_rect_xy_(SDL_FRect *r, short x, short y) +{ + r->x = x * SNAKE_BLOCK_SIZE_IN_PIXELS; + r->y = y * SNAKE_BLOCK_SIZE_IN_PIXELS; +} + +int SDL_AppIterate(void *appstate) +{ + AppState *as; + SnakeContext *ctx; + SDL_FRect r; + unsigned i; + unsigned j; + int ct; + as = (AppState *)appstate; + ctx = &as->snake_ctx; + r.w = r.h = SNAKE_BLOCK_SIZE_IN_PIXELS; + SDL_SetRenderDrawColor(as->renderer, 0, 0, 0, 255); + SDL_RenderClear(as->renderer); + for (i = 0; i < SNAKE_GAME_WIDTH; i++) { + for (j = 0; j < SNAKE_GAME_HEIGHT; j++) { + ct = snake_cell_at(ctx, i, j); + if (ct == SNAKE_CELL_NOTHING) + continue; + set_rect_xy_(&r, i, j); + if (ct == SNAKE_CELL_FOOD) + SDL_SetRenderDrawColor(as->renderer, 0, 0, 128, 255); + else /* body */ + SDL_SetRenderDrawColor(as->renderer, 0, 128, 0, 255); + SDL_RenderFillRect(as->renderer, &r); + } + } + SDL_SetRenderDrawColor(as->renderer, 255, 255, 0, 255); /*head*/ + set_rect_xy_(&r, ctx->head_xpos, ctx->head_ypos); + SDL_RenderFillRect(as->renderer, &r); + SDL_RenderPresent(as->renderer); + return SDL_APP_CONTINUE; +} + +int SDL_AppInit(void **appstate, int argc, char *argv[]) +{ + (void)argc; + (void)argv; + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0) { + return SDL_APP_FAILURE; + } + SDL_SetHint("SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR", "0"); + AppState *as = malloc(sizeof(AppState)); + *appstate = as; + as->step_timer = 0; + if (SDL_CreateWindowAndRenderer("examples/game/snake", SDL_WINDOW_WIDTH, SDL_WINDOW_HEIGHT, 0, &as->window, &as->renderer) == -1) { + return SDL_APP_FAILURE; + } + snake_initialize(&as->snake_ctx, SDL_rand); + as->step_timer = SDL_AddTimer( + STEP_RATE_IN_MILLISECONDS, + sdl_timer_callback_, + NULL); + if (as->step_timer == 0) { + return SDL_APP_FAILURE; + } + return SDL_APP_CONTINUE; +} + +int SDL_AppEvent(void *appstate, const SDL_Event *event) +{ + SnakeContext *ctx = &((AppState *)appstate)->snake_ctx; + switch (event->type) { + case SDL_EVENT_QUIT: + return SDL_APP_SUCCESS; + case SDL_EVENT_USER: + snake_step(ctx, SDL_rand); + break; + case SDL_EVENT_KEY_DOWN: + return handle_key_event_(ctx, event->key.scancode); + } + return SDL_APP_CONTINUE; +} + +void SDL_AppQuit(void *appstate) +{ + if (appstate != NULL) { + AppState *as = (AppState *)appstate; + SDL_RemoveTimer(as->step_timer); + SDL_DestroyRenderer(as->renderer); + SDL_DestroyWindow(as->window); + free(as); + } +} diff --git a/examples/game/01-snake/snake.c b/examples/game/01-snake/snake.c new file mode 100644 index 0000000000..1f5a9c61fe --- /dev/null +++ b/examples/game/01-snake/snake.c @@ -0,0 +1,154 @@ +/* + * Logic implementation of the Snake game. It is designed to efficiently + * represent in memory the state of the game. + * + * This code is public domain. Feel free to use it for any purpose! + */ +#include "snake.h" + +#include /* CHAR_BIT, CHAR_MAX */ +#include /* memcpy() */ + +#define THREE_BITS 0x7U /* ~CHAR_MAX >> (CHAR_BIT - SNAKE_CELL_MAX_BITS) */ +#define SHIFT(x, y) (((x) + ((y) * SNAKE_GAME_WIDTH)) * SNAKE_CELL_MAX_BITS) + +static void put_cell_at_(SnakeContext *ctx, char x, char y, SnakeCell ct) +{ + const int shift = SHIFT(x, y); + const int adjust = shift % CHAR_BIT; + unsigned char *const pos = ctx->cells + (shift / CHAR_BIT); + unsigned short range; + memcpy(&range, pos, sizeof(range)); + range &= ~(THREE_BITS << adjust); /* clear bits */ + range |= (ct & THREE_BITS) << adjust; + memcpy(pos, &range, sizeof(range)); +} + +static int are_cells_full_(SnakeContext *ctx) +{ + return ctx->occupied_cells == SNAKE_GAME_WIDTH * SNAKE_GAME_HEIGHT; +} + +static void new_food_pos_(SnakeContext *ctx, RandFunc rand) +{ + char x; + char y; + for (;;) { + x = rand(SNAKE_GAME_WIDTH); + y = rand(SNAKE_GAME_HEIGHT); + if (snake_cell_at(ctx, x, y) == SNAKE_CELL_NOTHING) { + put_cell_at_(ctx, x, y, SNAKE_CELL_FOOD); + break; + } + } +} + +void snake_initialize(SnakeContext *ctx, RandFunc rand) +{ + int i; + memset(ctx, 0, sizeof ctx->cells); + ctx->head_xpos = ctx->tail_xpos = SNAKE_GAME_WIDTH / 2; + ctx->head_ypos = ctx->tail_ypos = SNAKE_GAME_HEIGHT / 2; + ctx->next_dir = SNAKE_DIR_RIGHT; + ctx->inhibit_tail_step = ctx->occupied_cells = 4; + --ctx->occupied_cells; + put_cell_at_(ctx, ctx->tail_xpos, ctx->tail_ypos, SNAKE_CELL_SRIGHT); + for (i = 0; i < 4; i++) { + new_food_pos_(ctx, rand); + ++ctx->occupied_cells; + } +} + +void snake_redir(SnakeContext *ctx, SnakeDirection dir) +{ + SnakeCell ct = snake_cell_at(ctx, ctx->head_xpos, ctx->head_ypos); + if ((dir == SNAKE_DIR_RIGHT && ct != SNAKE_CELL_SLEFT) || + (dir == SNAKE_DIR_UP && ct != SNAKE_CELL_SDOWN) || + (dir == SNAKE_DIR_LEFT && ct != SNAKE_CELL_SRIGHT) || + (dir == SNAKE_DIR_DOWN && ct != SNAKE_CELL_SUP)) + ctx->next_dir = dir; +} + +static void wrap_around_(char *val, char max) +{ + if (*val < 0) + *val = max - 1; + if (*val > max - 1) + *val = 0; +} + +void snake_step(SnakeContext *ctx, RandFunc rand) +{ + const SnakeCell dir_as_cell = (SnakeCell)(ctx->next_dir + 1); + SnakeCell ct; + char prev_xpos; + char prev_ypos; + /* Move tail forward */ + if (--ctx->inhibit_tail_step == 0) { + ++ctx->inhibit_tail_step; + ct = snake_cell_at(ctx, ctx->tail_xpos, ctx->tail_ypos); + put_cell_at_(ctx, ctx->tail_xpos, ctx->tail_ypos, SNAKE_CELL_NOTHING); + switch (ct) { + case SNAKE_CELL_SRIGHT: + ctx->tail_xpos++; + break; + case SNAKE_CELL_SUP: + ctx->tail_ypos--; + break; + case SNAKE_CELL_SLEFT: + ctx->tail_xpos--; + break; + case SNAKE_CELL_SDOWN: + ctx->tail_ypos++; + break; + default: + break; + } + wrap_around_(&ctx->tail_xpos, SNAKE_GAME_WIDTH); + wrap_around_(&ctx->tail_ypos, SNAKE_GAME_HEIGHT); + } + /* Move head forward */ + prev_xpos = ctx->head_xpos; + prev_ypos = ctx->head_ypos; + switch (ctx->next_dir) { + case SNAKE_DIR_RIGHT: + ++ctx->head_xpos; + break; + case SNAKE_DIR_UP: + --ctx->head_ypos; + break; + case SNAKE_DIR_LEFT: + --ctx->head_xpos; + break; + case SNAKE_DIR_DOWN: + ++ctx->head_ypos; + break; + } + wrap_around_(&ctx->head_xpos, SNAKE_GAME_WIDTH); + wrap_around_(&ctx->head_ypos, SNAKE_GAME_HEIGHT); + /* Collisions */ + ct = snake_cell_at(ctx, ctx->head_xpos, ctx->head_ypos); + if (ct != SNAKE_CELL_NOTHING && ct != SNAKE_CELL_FOOD) { + snake_initialize(ctx, rand); + return; + } + put_cell_at_(ctx, prev_xpos, prev_ypos, dir_as_cell); + put_cell_at_(ctx, ctx->head_xpos, ctx->head_ypos, dir_as_cell); + if (ct == SNAKE_CELL_FOOD) { + if (are_cells_full_(ctx)) { + snake_initialize(ctx, rand); + return; + } + new_food_pos_(ctx, rand); + ++ctx->inhibit_tail_step; + ++ctx->occupied_cells; + } +} + +SnakeCell snake_cell_at(const SnakeContext *ctx, char x, char y) +{ + const int shift = SHIFT(x, y); + unsigned short range; + memcpy(&range, ctx->cells + (shift / CHAR_BIT), sizeof(range)); + return (SnakeCell)((range >> (shift % CHAR_BIT)) & THREE_BITS); +} diff --git a/examples/game/01-snake/snake.h b/examples/game/01-snake/snake.h new file mode 100644 index 0000000000..71b395dcfd --- /dev/null +++ b/examples/game/01-snake/snake.h @@ -0,0 +1,51 @@ +/* + * Interface definition of the Snake game. + * + * This code is public domain. Feel free to use it for any purpose! + */ +#ifndef SNAKE_H +#define SNAKE_H +#define SNAKE_GAME_WIDTH 24U +#define SNAKE_GAME_HEIGHT 18U +#define SNAKE_MATRIX_SIZE (SNAKE_GAME_WIDTH * SNAKE_GAME_HEIGHT) + +typedef enum +{ + SNAKE_CELL_NOTHING = 0U, + SNAKE_CELL_SRIGHT = 1U, + SNAKE_CELL_SUP = 2U, + SNAKE_CELL_SLEFT = 3U, + SNAKE_CELL_SDOWN = 4U, + SNAKE_CELL_FOOD = 5U +} SnakeCell; + +#define SNAKE_CELL_MAX_BITS 3U /* floor(log2(SNAKE_CELL_FOOD)) + 1 */ + +typedef enum +{ + SNAKE_DIR_RIGHT, + SNAKE_DIR_UP, + SNAKE_DIR_LEFT, + SNAKE_DIR_DOWN +} SnakeDirection; + +typedef struct +{ + unsigned char cells[(SNAKE_MATRIX_SIZE * SNAKE_CELL_MAX_BITS) / 8U]; + char head_xpos; + char head_ypos; + char tail_xpos; + char tail_ypos; + char next_dir; + char inhibit_tail_step; + unsigned occupied_cells; +} SnakeContext; + +typedef int (*RandFunc)(int n); + +void snake_initialize(SnakeContext *ctx, RandFunc rand); +void snake_redir(SnakeContext *ctx, SnakeDirection dir); +void snake_step(SnakeContext *ctx, RandFunc rand); +SnakeCell snake_cell_at(const SnakeContext *ctx, char x, char y); + +#endif /* SNAKE_H */