diff --git a/include/dangerzone/render/anim.h b/include/dangerzone/render/anim.h index ba544dd..01cdbdb 100644 --- a/include/dangerzone/render/anim.h +++ b/include/dangerzone/render/anim.h @@ -1,22 +1,55 @@ -#pragma once +#ifndef DANGERZONE_RENDER_ANIM_H +#define DANGERZONE_RENDER_ANIM_H -#include +#include +#include #include -/** - * animate a rectangle's position changing over time - * => x0 -> x1, y0 -> y1, all else stays the same - * - * ONLY DOES ONE FRAME OF THE ANIMATION +#define ANIM_MAX_TRACKS 64 + +typedef struct { + float x; + float y; + float w; + float h; +} anim_rect_t; + +/* payload is application data; the animation module does not interpret it. */ +typedef struct { + uint32_t payload; + anim_rect_t from; + anim_rect_t to; +} anim_track_t; + +typedef struct { + uint32_t payload; + anim_rect_t rect; +} anim_item_t; + +typedef struct { + anim_track_t tracks[ANIM_MAX_TRACKS]; + anim_item_t items[ANIM_MAX_TRACKS]; + size_t count; + float elapsed; + float duration; + bool active; +} anim_t; + +void anim_init(anim_t *anim); + +/* + * Start tracks from their initial rectangles to their destination rectangles. + * Returns false for invalid input or if count exceeds ANIM_MAX_TRACKS. + * A zero-duration animation completes immediately. */ -void -anim_rect_pos( - framebuffer_e framebuffer, - uint32_t x0, - uint32_t y0, - uint32_t x1, - uint32_t y1, - uint32_t w, - uint32_t h, - uint32_t color -); +bool anim_start(anim_t *anim, const anim_track_t *tracks, + size_t count, float duration); + +/* Advance by nonnegative seconds. Returns true once the animation is finished. */ +bool anim_update(anim_t *anim, float dt); + +bool anim_is_active(const anim_t *anim); +size_t anim_count(const anim_t *anim); +const anim_item_t *anim_items(const anim_t *anim); + +#endif diff --git a/src/engine/render/anim.c b/src/engine/render/anim.c index d554945..e8a7288 100644 --- a/src/engine/render/anim.c +++ b/src/engine/render/anim.c @@ -1,38 +1,89 @@ #include +#include + void -anim_rect_pos( - framebuffer_e framebuffer, - uint32_t x0, - uint32_t y0, - uint32_t x1, - uint32_t y1, - uint32_t w, - uint32_t h, - uint32_t color -) { - // there's nowhere to move - if (x0 == x1 && y0 == y1) { - return; +anim_init(anim_t *anim) +{ + if (anim != NULL) + memset(anim, 0, sizeof *anim); +} + +bool +anim_start(anim_t *anim, const anim_track_t *tracks, + size_t count, float duration) +{ + if (anim == NULL || (count != 0 && tracks == NULL) || + count > ANIM_MAX_TRACKS || duration < 0.0f) { + return false; } + + anim->count = count; + anim->elapsed = 0.0f; + anim->duration = duration; + anim->active = duration > 0.0f && count > 0; - uint32_t dirx = (x1 - x0 > 0) ? 1 : -1; - uint32_t diry = (y1 - y0 > 0) ? 1 : -1; + for (size_t i = 0; i < count; ++i) { + anim->tracks[i] = tracks[i]; + anim->items[i].payload = tracks[i].payload; + anim->items[i].rect = tracks[i].from; + } - if (x0 == x1) { - dirx = 0; + if (!anim->active) { + for (size_t i = 0; i < count; ++i) + anim->items[i].rect = tracks[i].to; } - - if (y0 == y1) { - diry = 0; + + return true; +} + +bool +anim_update(anim_t *anim, float dt) +{ + if (anim == NULL || !anim->active) + return true; + + if (dt < 0.0f) + dt = 0.0f; + + anim->elapsed += dt; + + float t = anim->elapsed / anim->duration; + if (t >= 1.0f) { + t = 1.0f; + anim->active = false; } - /** - * 1. shift the rectangle by one pixel in the direction of movement - * 2. remove the trailing row / column left - */ + /* Ease-out. Replace with t for linear motion. */ + float eased = 1.0f - (1.0f - t) * (1.0f - t); - if (true) { + for (size_t i = 0; i < anim->count; ++i) { + const anim_track_t *track = &anim->tracks[i]; + anim_rect_t *rect = &anim->items[i].rect; + rect->x = track->from.x + (track->to.x - track->from.x) * eased; + rect->y = track->from.y + (track->to.y - track->from.y) * eased; + rect->w = track->from.w + (track->to.w - track->from.w) * eased; + rect->h = track->from.h + (track->to.h - track->from.h) * eased; } + + return !anim->active; +} + +bool +anim_is_active(const anim_t *anim) +{ + return anim != NULL && anim->active; +} + +size_t +anim_count(const anim_t *anim) +{ + return anim != NULL ? anim->count : 0; +} + +const anim_item_t * +anim_items(const anim_t *anim) +{ + return anim != NULL ? anim->items : NULL; } diff --git a/src/examples/2048.c b/src/examples/2048.c index cd37b04..a62758e 100644 --- a/src/examples/2048.c +++ b/src/examples/2048.c @@ -1,8 +1,10 @@ #include +#include #include #include #include +#include #include #include #include @@ -10,6 +12,8 @@ #include #define SIZE 4 +#define MOVE_DURATION 0.12f +#define BORDER_WIDTH 10 typedef enum { LEFT, @@ -18,492 +22,381 @@ typedef enum { DOWN } dir_e; -typedef struct { - uint32_t x; - uint32_t y; -} v2_t; - typedef struct { uint32_t value; - v2_t pos; -} tile_t; + int row; + int col; +} line_tile_t; typedef struct { - tile_t area[SIZE][SIZE]; + uint32_t area[SIZE][SIZE]; dir_e direction; uint32_t score; } game_t; static game_t game; -#define MOVE_DURATION 0.12f +static anim_t movement; +static anim_track_t move_tracks[SIZE * SIZE]; +static size_t move_track_count; -typedef struct { - uint32_t value; - int from_row, from_col; - int to_row, to_col; - bool merge_target; -} tile_anim_t; +static double last_frame_time; -static tile_anim_t anims[SIZE * SIZE]; -static size_t anim_count; -static float anim_elapsed; -static bool anim_active; -uint32_t before[SIZE][SIZE]; - -void -print_area() +/* Pixel size is calculated from the current window size while drawing. */ +static uint32_t +colors(uint32_t number) { - printf("AREA:\n"); - for (int i = 0; i < SIZE; i++) { - printf("["); - for (int j = 0; j < SIZE - 1; j++) { - printf("%d, ", game.area[i][j].value); - } - printf("%d]\n", game.area[i][SIZE - 1].value); + switch (number) { + case 0: return 0xFFFFFF; + case 2: return 0x00FF00; + case 4: return 0xFF0000; + case 8: return 0x0000FF; + default: return 0xFFFFFF; } - printf("\n"); } -bool -move() +/* + * Return the board coordinate for position p in a line, where p=0 is + * the side toward which tiles move. + */ +static void +line_coordinate(dir_e direction, int line, int p, int *row, int *col) { - bool retval = false; - tile_t *current, *next; - int k_start, k_end, k_step; - switch (game.direction) { + switch (direction) { case LEFT: - case UP: - k_start = 0; - k_end = SIZE - 1; - k_step = 1; + *row = line; + *col = p; break; case RIGHT: + *row = line; + *col = SIZE - 1 - p; + break; + case UP: + *row = p; + *col = line; + break; case DOWN: - k_start = SIZE - 1; - k_end = 0; - k_step = -1; - break; - default: + *row = SIZE - 1 - p; + *col = line; break; } - - for (int i = 0; i < SIZE; i++) { - for (int j = 0; j < SIZE; j++) { - for (int k = k_start; (k_start <= k_end) ? k < k_end : k > k_end; k += k_step) { - switch (game.direction) { - case LEFT: - case RIGHT: - current = &game.area[i][k]; - next = &game.area[i][k + k_step]; - break; - case UP: - case DOWN: - current = &game.area[k][i]; - next = &game.area[k + k_step][i]; - break; - } - - if (current->value == 0 && next->value != 0) { - current->pos = (v2_t) { - .x = next->pos.x, - .y = next->pos.y - }; - current->value = next->value; - next->value = 0; - retval = true; - } - } - } - } - - return retval; } +/* + * Convert a board cell into the pixel rectangle used to draw its tile. + * Keep this consistent with draw_tile_rect() and the cell backgrounds. + */ +static anim_rect_t +tile_rect(int row, int col, uint32_t tile_size) +{ + float side = (float)(tile_size - BORDER_WIDTH); + return (anim_rect_t) { + .x = BORDER_WIDTH / 2.0f + col * (float)tile_size, + .y = BORDER_WIDTH / 2.0f + row * (float)tile_size, + .w = side, + .h = side + }; +} + +static void +add_move_track(uint32_t value, + int from_row, int from_col, + int to_row, int to_col) +{ + if (move_track_count >= SIZE * SIZE) + return; + + uint32_t tile_size = game_state->width / SIZE; + + move_tracks[move_track_count++] = (anim_track_t) { + .payload = value, + .from = tile_rect(from_row, from_col, tile_size), + .to = tile_rect(to_row, to_col, tile_size) + }; +} + +/* + * Process one row or column in movement order. Update the board and record + * pixel-space animation tracks for every original tile. + */ static bool -merge_line( - uint32_t line[SIZE] -) { - uint32_t out[SIZE] = {0}; - int write = 0; - bool previous_was_merged = false; +process_line(dir_e direction, int line, uint32_t output[SIZE]) +{ + line_tile_t input[SIZE]; + int input_count = 0; + int output_count = 0; - for (int read = 0; read < SIZE; ++read) { - uint32_t value = line[read]; + for (int p = 0; p < SIZE; ++p) { + int row, col; + line_coordinate(direction, line, p, &row, &col); - if (value == 0) - continue; - - if (write > 0 && - out[write - 1] == value && - !previous_was_merged) { - out[write - 1] *= 2; - game.score += out[write - 1]; - previous_was_merged = true; - } else { - out[write++] = value; - previous_was_merged = false; + uint32_t value = game.area[row][col]; + if (value != 0) { + input[input_count++] = (line_tile_t) { + .value = value, + .row = row, + .col = col + }; } } - bool changed = memcmp(line, out, sizeof out) != 0; - memcpy(line, out, sizeof out); + memset(output, 0, sizeof(uint32_t) * SIZE); + + for (int i = 0; i < input_count; ) { + int dest_row, dest_col; + line_coordinate(direction, line, output_count, + &dest_row, &dest_col); + + if (i + 1 < input_count && + input[i].value == input[i + 1].value) { + uint32_t merged_value = input[i].value * 2; + + output[output_count] = merged_value; + game.score += merged_value; + + /* Both source tiles travel to the same merge destination. */ + add_move_track(input[i].value, + input[i].row, input[i].col, + dest_row, dest_col); + add_move_track(input[i + 1].value, + input[i + 1].row, input[i + 1].col, + dest_row, dest_col); + + i += 2; + } else { + output[output_count] = input[i].value; + + add_move_track(input[i].value, + input[i].row, input[i].col, + dest_row, dest_col); + + ++i; + } + + ++output_count; + } + + bool changed = false; + for (int p = 0; p < SIZE; ++p) { + int row, col; + line_coordinate(direction, line, p, &row, &col); + + if (game.area[row][col] != output[p]) + changed = true; + + game.area[row][col] = output[p]; + } + return changed; } -bool -combine( - bool has_moved -) { +static bool +slide(dir_e direction) +{ + if (anim_is_active(&movement)) + return false; + + game.direction = direction; + move_track_count = 0; + bool changed = false; - uint32_t line[SIZE]; - for (int n = 0; n < SIZE; ++n) { - for (int p = 0; p < SIZE; ++p) { - int r, c; - - switch (game.direction) { - case LEFT: r = n; c = p; break; - case RIGHT: r = n; c = SIZE - 1 - p; break; - case UP: r = p; c = n; break; - case DOWN: r = SIZE - 1 - p; c = n; break; - default: return has_moved; - } - - line[p] = game.area[r][c].value; - } - - changed |= merge_line(line); - - for (int p = 0; p < SIZE; ++p) { - int r, c; - - switch (game.direction) { - case LEFT: r = n; c = p; break; - case RIGHT: r = n; c = SIZE - 1 - p; break; - case UP: r = p; c = n; break; - case DOWN: r = SIZE - 1 - p; c = n; break; - default: return has_moved; - } - - game.area[r][c].value = line[p]; - } + for (int line = 0; line < SIZE; ++line) { + uint32_t output[SIZE]; + if (process_line(direction, line, output)) + changed = true; } - return has_moved || changed; + if (!changed) { + move_track_count = 0; + return false; + } + + return anim_start(&movement, move_tracks, move_track_count, + MOVE_DURATION); } -// finds a random empty position and puts a two there -void -new_2() +static void +new_2(void) { - // collect all zeroes - v2_t zeroes[SIZE * SIZE] = {0}; - uint32_t zero_count = 0; - for (int i = 0; i < SIZE; i++) { - for (int j = 0; j < SIZE; j++) { - if (game.area[i][j].value == 0) { - zeroes[zero_count++] = (v2_t){.x=j,.y=i}; + int empty_rows[SIZE * SIZE]; + int empty_cols[SIZE * SIZE]; + int count = 0; + + for (int r = 0; r < SIZE; ++r) { + for (int c = 0; c < SIZE; ++c) { + if (game.area[r][c] == 0) { + empty_rows[count] = r; + empty_cols[count] = c; + ++count; } } } - // choose among them - uint32_t rand_index = rand() % zero_count; - game.area[zeroes[rand_index].y][zeroes[rand_index].x].value = 2; + if (count == 0) + return; + + int pick = rand() % count; + game.area[empty_rows[pick]][empty_cols[pick]] = 2; +} + +/* Polling events every frame. */ +void +client_poll_event(void) +{ + SDL_Event ev = game_state->event; + + if (ev.type == SDL_EVENT_QUIT) { + game_state->is_running = false; + return; + } + + if (ev.type != SDL_EVENT_KEY_DOWN) + return; + + switch (ev.key.key) { + case SDLK_Q: + game_state->is_running = false; + break; + case SDLK_W: + slide(UP); + break; + case SDLK_S: + slide(DOWN); + break; + case SDLK_A: + slide(LEFT); + break; + case SDLK_D: + slide(RIGHT); + break; + default: + break; + } +} + +static double +monotonic_seconds(void) +{ + struct timespec ts; + timespec_get(&ts, TIME_UTC); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +} + +static void +draw_tile_rect(uint32_t value, float x, float y, float w, float h) +{ + uint32_t ix = (uint32_t)x; + uint32_t iy = (uint32_t)y; + uint32_t iw = (uint32_t)w; + uint32_t ih = (uint32_t)h; + uint32_t color = colors(value); + + put_rect(OVERDRAW, ix, iy, iw, ih, color); + + if (value != 0) { + char text[16]; + snprintf(text, sizeof text, "%u", value); + put_string(OVERDRAW, text, + ix + BORDER_WIDTH, iy + BORDER_WIDTH, + 3, 1, 12, 0x000000, color); + } +} + +static void +draw_tile(uint32_t value, int row, int col, uint32_t tile_size) +{ + anim_rect_t rect = tile_rect(row, col, tile_size); + draw_tile_rect(value, rect.x, rect.y, rect.w, rect.h); } void -slide() +client_update_framebuffer(void) { - // 1. move all squares in one direction - bool has_moved = move(); + double now = monotonic_seconds(); + float dt = last_frame_time == 0.0 + ? 0.0f + : (float)(now - last_frame_time); + last_frame_time = now; - // 2. then combine them and shift the potential rest by one in the given direction - bool has_combined = combine(has_moved); - - // 3. generate a new 2 - if (has_moved || has_combined) { + if (dt > 0.05f) + dt = 0.05f; + + if (anim_is_active(&movement) && anim_update(&movement, dt)) { new_2(); } -} - -// 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: - game.direction = UP; - for (int r = 0; r < SIZE; ++r) - for (int c = 0; c < SIZE; ++c) - before[r][c] = game.area[r][c].value; - slide(); - break; - case SDLK_S: - game.direction = DOWN; - for (int r = 0; r < SIZE; ++r) - for (int c = 0; c < SIZE; ++c) - before[r][c] = game.area[r][c].value; - slide(); - break; - case SDLK_A: - game.direction = LEFT; - for (int r = 0; r < SIZE; ++r) - for (int c = 0; c < SIZE; ++c) - before[r][c] = game.area[r][c].value; - slide(); - break; - case SDLK_D: - game.direction = RIGHT; - for (int r = 0; r < SIZE; ++r) - for (int c = 0; c < SIZE; ++c) - before[r][c] = game.area[r][c].value; - slide(); - break; - default: - break; - } - } -} - -#define CHR(ord) ((ord) + 48) -#define ORD(chr) ((chr) - 48) - -uint32_t -colors( - uint32_t number -) { - switch (number) { - case 0: - return 0xFFFFFF; - case 2: - return 0x00FF00; - case 4: - return 0xFF0000; - case 8: - return 0x0000FF; - default: - return 0xFFFFFF; - } -} - -uint32_t -numlen( - uint32_t n -) { - uint32_t len = 0; - - while (n > 0) { - len++; - n /= 10; - } - - return len; -} - -uint32_t -power( - uint32_t x, - uint32_t n -) { - // x ^ n - uint32_t acc = 1; - for (int i = 0; i < n; i++) { - acc *= x; - } - - return acc; -} - -uint32_t -numrev( - uint32_t n -) { - uint32_t len = numlen(n); - uint32_t new = 0; - int i = 0; - - while (n > 0) { - new += (n % 10) * power(10, len - (++i)); - n /= 10; - } - - return new; -} - -v2_t last_area_positions[SIZE][SIZE] = {0}; - -// performed every frame -void -client_update_framebuffer() -{ - // clear_framebuffer(0xFFFF00); - // put_string("Welcome to the most\n*DANGEROUS* 2048 game!", - // 20, 20, 2, 2, 3, 0x000000, 0xFFFF00 - // ); - - // FIXME: why? - //clear_framebuffer(BASE, 0x000000); uint32_t tile_size = game_state->width / SIZE; - uint8_t border_width = 10; - char *n_str; - bool alloced = false; - for (int i = 0; i < SIZE; i++) { - for (int j = 0; j < SIZE; j++) { - alloced = true; - // background - uint32_t x = (border_width / 2) + i * tile_size; - uint32_t y = (border_width / 2) + j * tile_size; - uint32_t w = tile_size - border_width; - uint32_t h = tile_size - border_width; - uint32_t background_color = colors(game.area[j][i].value); + uint32_t border_width = BORDER_WIDTH; - put_rect(BASE, x, y, w, h, 0xFFFFFF); - put_rect(OVERDRAW, x, y, w, h, background_color); + clear_framebuffer(OVERDRAW, 0x000000); - // number in foreground - uint32_t foreground_color = 0x000000; - uint8_t font_size = 3; - - uint32_t n = game.area[j][i].value; - uint32_t n_len = numlen(n); - n_str = calloc(n_len + 1, sizeof(char)); - if (n_str == NULL) { - game_state->is_running = false; - } - sprintf(n_str, "%d", n); - uint32_t n_rev = numrev(n); - uint32_t digit_offset = 1; - if (n != 0) { - put_string(OVERDRAW, n_str, x + border_width, y + border_width, font_size, digit_offset, 12, foreground_color, background_color); + /* Draw all cell backgrounds first. */ + for (int r = 0; r < SIZE; ++r) { + for (int c = 0; c < SIZE; ++c) { + uint32_t x = border_width / 2 + (uint32_t)c * tile_size; + uint32_t y = border_width / 2 + (uint32_t)r * tile_size; + uint32_t side = tile_size - border_width; + + put_rect(BASE, x, y, side, side, 0xFFFFFF); + put_rect(OVERDRAW, x, y, side, side, 0xFFFFFF); + } + } + + if (anim_is_active(&movement)) { + const anim_item_t *items = anim_items(&movement); + size_t count = anim_count(&movement); + + for (size_t i = 0; i < count; ++i) { + draw_tile_rect(items[i].payload, + items[i].rect.x, items[i].rect.y, + items[i].rect.w, items[i].rect.h); + } + } else { + for (int r = 0; r < SIZE; ++r) { + for (int c = 0; c < SIZE; ++c) { + draw_tile(game.area[r][c], r, c, tile_size); } } } - // score display line (single line) - char *score_text = calloc(7 + 6 + 1, sizeof(char)); - if (score_text == NULL) { - game_state->is_running = false; - } - alloced = true; - - sprintf(score_text, "score: %6d", game.score); - put_string(OVERDRAW, score_text, border_width / 2, tile_size * SIZE + border_width / 2, 6, 1, 12, 0xFFFFFF, 0x000000); - - if (alloced) { - free(n_str); - free(score_text); - } + char score_text[32]; + snprintf(score_text, sizeof score_text, "score: %u", game.score); + put_string(OVERDRAW, score_text, + border_width / 2, tile_size * SIZE + border_width / 2, + 6, 1, 12, 0xFFFFFF, 0x000000); } -bool -v2_eq(v2_t a, v2_t b) +static void +game_init(void) { - return a.x == b.x && a.y == b.y; + memset(&game, 0, sizeof game); + anim_init(&movement); + srand((unsigned)time(NULL)); + + /* Start with two 2-tiles. */ + new_2(); + new_2(); } -bool -v2_contains(v2_t v[], size_t size, v2_t elem) -{ - for (int i = 0; i < size; i++) { - if (v2_eq(v[i], elem)) { - return true; - } - } - - return false; -} - -void -game_init( - uint32_t window_width -) { - srand(time(NULL)); - - // generate MAX_NUMBER_OF_4 + MAX_NUMBER_OF_2 random indices in range 0..15 (row in 0..3, col in 0..3) - // this means that there will be at least MAX_NUMBER_OF_2 of 2s and MAX_NUMBER_OF_4 <= MAX_NUMBER_OF_2 4s - // => usually less 4s than 2s because the indices can get replaced -#define MAX_NUMBER_OF_4 1 -#define MAX_NUMBER_OF_2 2 - - uint8_t row, col; - v2_t elem; - v2_t fours[MAX_NUMBER_OF_4], twos[MAX_NUMBER_OF_2]; - - int i = 0; - while (i < MAX_NUMBER_OF_4) { - row = rand() % SIZE; - col = rand() % SIZE; - elem = (v2_t){ .x = col, .y = row }; - if (!v2_contains(twos, MAX_NUMBER_OF_4, elem)) { - fours[i++] = elem; - } - } - - i = 0; - while (i < MAX_NUMBER_OF_2) { - row = rand() % SIZE; - col = rand() % SIZE; - elem = (v2_t){ .x = col, .y = row }; - if (!v2_contains(twos, MAX_NUMBER_OF_2, elem)) { - twos[i++] = elem; - } - } - - for (int row = 0; row < SIZE; row++) { - for (int col = 0; col < SIZE; col++) { - game.area[row][col].pos = (v2_t){ - .x = col * (window_width / SIZE), - .y = row * (window_width / SIZE) - }; - game.area[row][col].value = 0; - } - } - - for (i = 0; i < MAX_NUMBER_OF_4; i++) { - game.area[fours[i].y][fours[i].x].value = 4; - } - - for (i = 0; i < MAX_NUMBER_OF_2; i++) { - game.area[twos[i].y][twos[i].x].value = 2; - } - - game.direction = RIGHT; - game.score = 0; -} - -int -main() +int +main(void) { uint32_t window_width = 500; - uint32_t window_height = window_width * 1.12f; + uint32_t window_height = (uint32_t)(window_width * 1.12f); - game_init(window_width); + game_init(); - // initialize the engine - if (engine_init( - window_width, window_height, - "Dangerous 2048", - client_poll_event, - client_update_framebuffer - )) { - engine_start(); - engine_stop(); - return 0; - } else { - return 1; - } + if (engine_init(window_width, window_height, + "Dangerous 2048", + client_poll_event, + client_update_framebuffer)) { + engine_start(); + engine_stop(); + return 0; + } + + return 1; } +