From 937ecedfe3dd16d5bedba38f1f9c010814e0d63e Mon Sep 17 00:00:00 2001 From: Quinn Date: Wed, 25 Jun 2025 15:51:27 +0200 Subject: [PATCH] implement new code, and remove deprecated old code. Overal optimisation. this code is currently broken, but this'll be fixed in a subsequent commit --- src/game/game.c | 169 +++++++++------------------------ src/game/game.h | 44 ++++----- src/game/gametime.h | 28 ------ src/game/paths.c | 68 -------------- src/game/paths.h | 14 --- src/game/tetromino/placing.c | 175 +++++++++++++++-------------------- src/game/tetromino/placing.h | 19 +--- src/io/render.c | 147 ++++++++++++----------------- src/io/render.h | 26 +----- src/io/window.c | 51 ++++++---- src/io/window.h | 4 +- src/main.c | 57 ++---------- 12 files changed, 252 insertions(+), 550 deletions(-) delete mode 100644 src/game/gametime.h delete mode 100644 src/game/paths.c delete mode 100644 src/game/paths.h diff --git a/src/game/game.c b/src/game/game.c index 96d88c5..4a627f4 100644 --- a/src/game/game.c +++ b/src/game/game.c @@ -1,152 +1,71 @@ #include "game.h" -#include -#include -#include #include #include #include +#include #include -#include "../io/audio.h" +#include "../io/colour/colour8.h" #include "../util/types.h" +#include "../util/vec.h" #include "./tetromino/shapes.h" -#include "gametime.h" #include "tetromino/placing.h" -/* shuffle the array using a Fisher–Yates shuffle */ -static inline void shuffle(uint8_t const size, u8* const elmnts) { - for (uint i = 0; i < (size - 1); i++) { - uint const j = i + rand() % (size - i); - u8 const cache = elmnts[i]; - elmnts[i] = elmnts[j]; - elmnts[j] = cache; +static colour8 rowdat[COLUMNS * ROWS] = {0}; // contains the raw data of the rows, in no particular order +static struct gamedata dat = {0}; + +/* shuffle an array using the Fisher–Yates shuffle algorithm. + * `nmemb` is the number of members. + * `membs` is the byte size of each member */ +static void shuffle(void* restrict ptr, size_t nmemb, size_t membs) { + u8 dat[membs]; + + for (size_t i = 0; i < nmemb; i++) { + size_t j = i + rand() % (nmemb - i); + void* ptri = (u8*)ptr + i * membs; + void* ptrj = (u8*)ptr + j * membs; + memcpy(dat, ptri, membs); + memcpy(ptri, ptrj, membs); + memcpy(ptrj, ptri, membs); } } -void next_shape(gamedata* const dat) { - dat->curr_idx++; // increase the current shape index - dat->sel_x = COLUMNS / 2 - SHAPE_WIDTH / 2; // move the shape position to the centre - dat->sel_y = 0; +void next_shape(void) { + // as long as we're not at the last shape, we can just increment + dat.pdat.sel = (i8vec2){COLUMNS / 2 - SHAPE_WIDTH / 2, 0}; + dat.pdat.idx++; + if (dat.pdat.idx < TETROMINO_COUNT - 1) return; - // return if know which shape is next - if (dat->curr_idx < (TETROMINO_COUNT - 1)) - return; + // shuffle all next shapes, preserving the last + dat.pdat.idx = 0; + shuffle(dat.pdat.nxt, TETROMINO_COUNT - 1, sizeof(u8)); - dat->curr_idx = 0; - - shuffle(TETROMINO_COUNT - 1, dat->nxt); - u8 cache = dat->nxt[0]; - dat->nxt[0] = dat->nxt[TETROMINO_COUNT - 1]; - dat->nxt[TETROMINO_COUNT - 1] = cache; + // swap the first and last shape, thus preserving what the shape would've been + u8 cache = dat.pdat.nxt[0]; + dat.pdat.nxt[0] = dat.pdat.nxt[TETROMINO_COUNT - 1]; + dat.pdat.nxt[TETROMINO_COUNT - 1] = cache; } -void game_init(gamedata* const dat) { - // set a random seed using the system clock +struct gamedata* game_init(void) { srand(time(NULL)); - struct gametime gt = {{0}, 0}; - gametime_get(>.ts); + // populate the data arrays + for (int i = 0; i < ROWS; i++) + dat.rows[i] = rowdat + i * COLUMNS; + for (int i = 0; i < TETROMINO_COUNT; i++) + dat.pdat.nxt[i] = i; - // initialize audio device - audio_device_init(32000, AUDIO_S16, 1, 4096); - - *dat = (gamedata){ - {0}, // rowdat - {0}, // row - gt, // time - audio_wav_load("korobeiniki.wav"), // music - audio_wav_load("place.wav"), // place_sfx - 0, // score - {0}, // nxt - 0, // curr_idx - 0, // sel_x - 0, // sel_y - true, // run - }; - - // initialize the rows within the game data - for (int8_t i = 0; i < ROWS; i++) { - dat->rows[i] = dat->rowdat + (i * COLUMNS); - } - - // set the shape data in each slot to it's corrsponding ID - for (uint i = 0; i < TETROMINO_COUNT; i++) - dat->nxt[i] = i; - - dat->curr_idx = -1; // set the current index to the max so it becomes zero after increasement - next_shape(dat); // select the next shape (shuffle should not be triggered) - shuffle(TETROMINO_COUNT, dat->nxt); // manually trigger a shuffle -} - -// updates the gametime -static inline void update_gametime(gamedata* dat) { - struct timespec ts; - gametime_get(&ts); - dat->time.ms = ts.tv_sec * 1000 + ts.tv_nsec / 1000000; - dat->time.ts = ts; + // initialise the placing data correctly + dat.pdat.sel = (i8vec2){COLUMNS / 2 - SHAPE_WIDTH / 2, 0}; + shuffle(dat.pdat.nxt, TETROMINO_COUNT, sizeof(u8)); + return &dat; } // called every time the game's state is updated -void game_update(gamedata* const dat) { - static time_t timer_update = 0; - static time_t timer_music = 0; - static time_t timer_move = 0; - static time_t timer_rot = 0; - update_gametime(dat); - uint8_t const* keys = SDL_GetKeyboardState(NULL); - - if (keys[SDL_SCANCODE_ESCAPE]) - dat->run = false; - - input_data move = MOVE_NONE; // contains the move data - time_t ctime = dat->time.ms; - - if (ctime > timer_update) { - timer_update = ctime + 500; - move |= MOVE_DOWN; - } - - if (ctime > timer_music) { - timer_music = ctime + (dat->music.ms); - audio_play(&dat->music); - } - - // for rotation updating - if (ctime > timer_rot) { - input_data urot = MOVE_NONE; - if (keys[SDL_SCANCODE_Q]) urot |= MOVE_ROTLEFT; - if (keys[SDL_SCANCODE_E]) urot |= MOVE_ROTRIGHT; - - if (urot != MOVE_NONE) { - timer_rot = ctime + 100; - move |= urot; - } - } - - // for movement updating - if (ctime > timer_move) { - input_data umove = MOVE_NONE; - if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_A]) umove |= MOVE_LEFT; - if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_D]) umove |= MOVE_RIGHT; - if (keys[SDL_SCANCODE_DOWN] || keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_SPACE]) umove |= MOVE_DOWN; - - if (umove != MOVE_NONE) { - timer_move = ctime + 20; - move |= umove; - } - } - - // update the block position - if (move != MOVE_NONE) - place_update(dat, move); +void game_update(int movdat) { + place_update(&dat, movdat); } -void game_free(gamedata* const dat) { - audio_wav_unload(&dat->music); - audio_wav_unload(&dat->place_sfx); - audio_device_free(); - - // zero-out the rest of the data - *dat = (gamedata){0}; +void game_free(void) { } diff --git a/src/game/game.h b/src/game/game.h index f272cf0..bf272ea 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -3,10 +3,10 @@ #include #include -#include "../io/audio.h" #include "../io/colour/colour8.h" #include "../util/types.h" -#include "gametime.h" +#include "../util/vec.h" +#include "tetromino/shapes.h" // constants for pi(π) and tau(τ) #define PI (M_PI) // π constant @@ -15,27 +15,27 @@ #define TAUf (M_PIf * 2.0F) // τ constant as a 32-bit floating point // stores the data used in the game -#define COLUMNS ((int8_t)10) -#define ROWS ((int8_t)24) +#define COLUMNS 10 +#define ROWS 24 -typedef colour8 const* const row_const; -typedef colour8* row; +/* contains the placement data */ +struct pdat { + u8 nxt[TETROMINO_COUNT]; + i8vec2 sel; + u8 idx; +}; -typedef struct { - colour8 rowdat[ROWS * COLUMNS]; +/* contains game data that's commonly shared */ +struct gamedata { + struct pdat pdat; colour8* rows[ROWS]; - struct gametime time; - audiodata music; - audiodata place_sfx; - uint16_t score; - u8 nxt[7]; // the order of the shape ids that they should appear in - uint8_t curr_idx; // current shape index - int8_t sel_x; // selected shape x position - int8_t sel_y; // selected shape y position - bool run; -} gamedata; + u16 pnts; +}; -void next_shape(gamedata*); // initializes everything needed to start the game; outputs to gamedata -void game_init(gamedata*); // initializes the game -void game_update(gamedata*); // causes an update to occur within the game -void game_free(gamedata*); // frees the resources associated with the game + +/* increments to the next shape, shuffling the next shapes, if there isn't a next shape immediately after the current one. */ +void next_shape(void); + +struct gamedata* game_init(void); +void game_update(int); +void game_free(void); diff --git a/src/game/gametime.h b/src/game/gametime.h deleted file mode 100644 index aa136fb..0000000 --- a/src/game/gametime.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include - -struct gametime { - struct timespec ts; - time_t ms; -}; - -#if __has_include() -#include -#endif -#if __has_include() && _POSIX_C_SOURCE >= 199309L -#include -static inline void gametime_get(struct timespec* ts) { - clock_gettime(CLOCK_MONOTONIC, ts); -} -#elif defined(_WIN32) -#include -#include -#include -static inline void gametime_get(struct timespec* ts) { - LARGE_INTEGER cnt, frq; - QueryPerformanceCounter(&cnt); - QueryPerformanceFrequency(&frq); - ts->tv_sec = (time_t)(cnt.QuadPart / frq.QuadPart); - ts->tv_nsec = (time_t)((cnt.QuadPart % frq.QuadPart) * 1000000000 / frq.QuadPart); -} -#endif diff --git a/src/game/paths.c b/src/game/paths.c deleted file mode 100644 index 90db4ed..0000000 --- a/src/game/paths.c +++ /dev/null @@ -1,68 +0,0 @@ -#include "paths.h" - -#include -#include - -#include "../util/compat.h" - -char const* restrict path_dat = NULL; -char const* restrict path_opts = NULL; -char const* restrict path_font = NULL; -char const* restrict path_music = NULL; -char const* restrict path_place_sfx = NULL; - -// gets the game's data path, returns 0 on failure, otherwise the datapath's string length -static unsigned getdatpath(void) { - char const* home = getenv(unixonly("HOME") winonly("APPDATA")); // get the user data directory path (appropriated for each platform) - if (!home) home = "."; // if this failed, set the path to `.`, which represents cwd - - unsigned len = strlen(home); - len += 1 + 20 unixonly(+13); // add 21 bytes to the home length, to account for adding .local/share later - char* datpath = malloc(len); - if (!datpath) return 0; - - // copy the data from home into the datapath - strcpy(datpath, home); - -#ifdef __unix__ - // include the .local/share directory, if the HOME environment variable was valid - if (home[0] != '.') strcat(datpath, "/.local/share"); - else { - // if the HOME directory wasn't defined, shrink the string - len -= 13; - void* ptr = realloc(datpath, len); - if (ptr) datpath = ptr; // likely doesn't actually change the pointer, but just to be sure - } -#endif - - strcat(datpath, PATH_SEP_STR "quinns_tetris_clone"); - path_dat = datpath; - return len; -} - -static inline char const* init_path(char const* restrict const str, unsigned len) { - void* ptr = malloc(len); - if (!ptr) return NULL; - strcpy(ptr, path_dat); - strcat(ptr, str); - return ptr; -} - -int paths_init(void) { - unsigned len = getdatpath(); - if (!len) return 1; - - // these are explicitly static, as string literals just work like that - path_opts = init_path("/opts.cfg", len + 9); // TODO: shouldn't opts be stored at .config/? - path_font = init_path("/pixeldroid_botic-regular.ttf", len + 29); // TODO: these three paths should not be stored like opts - path_music = init_path("/korobeiniki.wav", len + 16); - path_place_sfx = init_path("place.wav", len + 10); - return -(!path_opts || !path_font || !path_music || !path_place_sfx); -} - -void paths_free(void) { - free((void*)path_dat), path_dat = NULL; - free((void*)path_opts), path_opts = NULL; - free((void*)path_music), path_music = NULL; - free((void*)path_place_sfx), path_place_sfx = NULL; -} diff --git a/src/game/paths.h b/src/game/paths.h deleted file mode 100644 index 7f4c0f3..0000000 --- a/src/game/paths.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -extern char const* restrict path_dat; -extern char const* restrict path_opts; -extern char const* restrict path_font; -extern char const* restrict path_music; -extern char const* restrict path_place_sfx; - -/* initializes the paths, paths are evaluated to NULL upon failure. - returns 0 upon success, >0 upon failure <0 upon non-critical failure */ -int paths_init(void); - -/* frees allocated data */ -void paths_free(void); diff --git a/src/game/tetromino/placing.c b/src/game/tetromino/placing.c index c355b97..07f6b53 100644 --- a/src/game/tetromino/placing.c +++ b/src/game/tetromino/placing.c @@ -1,71 +1,67 @@ #include "placing.h" #include +#include +#include -#include "../../io/audio.h" -#include "../../io/colour/colour8.h" +#include "../../io/input.h" +#include "../../io/window.h" #include "../../util/types.h" +#include "../../util/vec.h" #include "../game.h" #include "shapes.h" -static int is_filled(row_const const row) { - for (int8_t x = 0; x < COLUMNS; x++) { - if (row[x] == 0) { - return 0; - } - } - - return 1; +/* checks if `row` of `COLUMNS` wide contains anything with `0`. + * returns `1` if it doesn't, otherwise returns `0` */ +__attribute_const__ static int is_filled(u8 const* restrict row) { + int res = 0; +#if defined(__clang__) +#pragma unroll COLUMNS +#elif defined(__GNUC__) +#pragma GCC unroll COLUMNS +#endif + for (int i = 0; i < COLUMNS; i++) res |= !row[i]; + return !res; } -static void clear_rows(row* const rows, uint16_t* const score) { - row cache[4] = {0}; // you can only clear four rows at a time - unsigned filled = 0; - unsigned checked = 0; +/* checks for filled rows, clearing a max of 4 consecutive rows, if present. + * all row pointers will be moved down to fill the gap, the filled rows will be inserted at the top, and zeroed-out */ +static void clear_rows(u8* restrict* restrict rows, u16* const score) { + u8* cache[4] = {0}; // the I piece is 4 long, meaning only 4 rows can be cleared at a time + uint fillc = 0; + uint checkc = 0; - // loop through each row (excluding the empty rows at the top when clearing a line) - for (unsigned y = 0; y < (ROWS - filled); y++) { - int const i = (ROWS - 1) - y; // get the index starting from the bottom + for (uint y = 0; y < (ROWS - fillc); y++) { + int i = (ROWS - 1) - y; // invert the index direction, so we start at the highest index + rows[i] = rows[i - fillc]; // either assigns the current row to itself, or to the "next" row - rows[i] = rows[i - filled]; // set the row to the new or same address - - // continue if the line isn't filled or the max amount has been reached - if (checked >= 4 || !is_filled(rows[i])) { - if (filled > 0 && checked < 4) checked++; - continue; // continue to the next line - } - - cache[filled] = rows[i]; // cache the current row address - filled++; // increase filled, and keep the row in the cache - checked++; // increase the checked count - y--; // decrease y to check this line again - } - - if (filled == 0) return; - *score += 8 << filled; - - // do while, as we already know that entering the loop that filled is non-zero - do { - filled--; - rows[filled] = cache[filled]; - - // zero out the filled row - for (unsigned x = 0; x < COLUMNS; x++) - cache[filled][x] = 0; - } while (filled > 0); -} - -// sets a shape to the screen -static void set_shape_i(row const* const row, u8 const id, int8_t const pos_x) { - u16 const shape = shape_from_id(id); - colour8 const colour = colour_from_id(id); - for (int8_t y = 0; y < SHAPE_HEIGHT; y++) { - u8 const shape_row = shape_get_row(shape, y); -// TODO: this is suboptimal, ditch the entire "representing shapes as binary-formatted data" and instead use a switch...case. - - if (shape_row == 0) + if (checkc >= 4 || !is_filled(rows[i])) { + checkc += (fillc > 0 && checkc < 4); continue; + } + + cache[fillc] = rows[i]; // cache the current row address, since it's going to be overridden + fillc++; + checkc++; + y--; + } + + *score += (8 << fillc) * !!fillc; + for (uint i = 0; i < fillc; i++) { + memset(cache[i], 0, COLUMNS); + rows[i] = cache[i]; + } +} + +// TODO: this is suboptimal, ditch the entire "representing shapes as binary-formatted data" and instead use a switch...case. +/* writes a shape to the screen */ +static void set_shape(u8* restrict const* restrict row, u8 id, int pos_x) { + u16 shape = shape_from_id(id); + u8 colour = colour_from_id(id); + + for (uint y = 0; y < SHAPE_HEIGHT; y++) { + u8 const shape_row = shape_get_row(shape, y); for (int8_t x = 0; x < SHAPE_WIDTH; x++) if (shape_is_set(shape_row, x)) @@ -73,11 +69,7 @@ static void set_shape_i(row const* const row, u8 const id, int8_t const pos_x) { } } -static inline void set_shape(row const* const row, u8 const id, int8_t const pos_x, int8_t const pos_y) { - set_shape_i(&row[pos_y], id, pos_x); // calls itself, but omitting the pos_y argument, instead opting for specifying the row -} - -static int shape_intersects(row const* const rows, u8 const id, int8_t const x, int8_t const y) { +static int shape_intersects(u8* restrict const* restrict const rows, u8 const id, i8vec2 pos) { u16 const shape = shape_from_id(id); for (int y0 = 0; y0 < SHAPE_HEIGHT; y0++) { @@ -86,8 +78,8 @@ static int shape_intersects(row const* const rows, u8 const id, int8_t const x, for (int x0 = 0; x0 < SHAPE_WIDTH; x0++) { if (shape_is_set(shape_row, x0) == false) continue; // if the bit isn't set at this index; continue - int const x1 = x + x0; - int const y1 = y + y0; + int x1 = pos[0] + x0; + int y1 = pos[1] + y0; if (x1 < 0 || x1 >= COLUMNS) return 1; // if X is out of bounds if (y1 < 0 || y1 >= ROWS) return 1; // if Y is out of bounds @@ -97,50 +89,37 @@ static int shape_intersects(row const* const rows, u8 const id, int8_t const x, return 0; } -static inline u8 rotate_id(u8 const id, int const dir) { - return (id + dir) & 31; -} - -void place_update(gamedata* const game_data, input_data const move) { +void place_update(struct gamedata* gdat, int movdat) { // store the current index and ID, only changes when placed (which yields no movement) and rotation (which occurs last) - uint8_t const curr_idx = game_data->curr_idx; - u8 curr_id = game_data->nxt[curr_idx]; - + int tmp; + u8 idx = gdat->pdat.idx; + u8 id = gdat->pdat.nxt[idx]; // set the shape if we moved vertically and intersected - if (move & 4) { - int8_t const y = game_data->sel_y + 1; - if (shape_intersects(game_data->rows, curr_id, game_data->sel_x, y)) { - set_shape(game_data->rows, curr_id, game_data->sel_x, game_data->sel_y); // if the shape intersects vertically, write the shape at the current position and return - clear_rows(game_data->rows, &game_data->score); // clear the rows that have been completed + if (movdat & MOVD) { + i8 y = gdat->pdat.sel[1] + 1; + if (shape_intersects(gdat->rows, id, gdat->pdat.sel)) { + set_shape(gdat->rows + gdat->pdat.sel[1], id, gdat->pdat.sel[0]); // if the shape intersects vertically, write the shape at the current position and return + clear_rows(gdat->rows, &gdat->pnts); // clear the rows that have been completed - audio_play(&game_data->place_sfx); + // TODO: play place_sfx - next_shape(game_data); - if (shape_intersects(game_data->rows, game_data->curr_idx, game_data->sel_x, game_data->sel_y)) - game_data->run = false; - return; - } + next_shape(); + if (shape_intersects(gdat->rows, gdat->pdat.idx, gdat->pdat.sel)) { + window_close(); + return; + } - // otherwise, just set Y - game_data->sel_y = y; - } - - // update shape's X coordinate movement - if ((move & 3) != 3 && (move & 3)) { - int8_t const x = game_data->sel_x + ((move & 3) == 1 ? -1 : 1); // either move along -x or +x - if (shape_intersects(game_data->rows, curr_id, x, game_data->sel_y) == false) { - game_data->sel_x = x; // set X if the shape does not intersect + // otherwise, just set Y + gdat->pdat.sel[1] = y; } } - // update the shape's rotation - if (move & 8 || move & 16) { - u8 const id = move & 8 // check which direction we should move - ? rotate_id(curr_id, -8) - : rotate_id(curr_id, 8); - if (shape_intersects(game_data->rows, id, game_data->sel_x, game_data->sel_y) == false) { - game_data->nxt[curr_idx] = id; - } - } + // update X axis + tmp = !!(movdat & MOVL) * -1 + !!(movdat & MOVR); + gdat->pdat.sel[0] += (tmp && shape_intersects(gdat->rows, id, gdat->pdat.sel + (i8vec2){tmp, 0})) * tmp; + + // update roll + tmp = id - (((!!(movdat & MOVRL) * -8 + !!(movdat & MOVRR) * 8) + id) & 31); + gdat->pdat.nxt[idx] += (tmp && shape_intersects(gdat->rows, tmp, gdat->pdat.sel)) * tmp; } diff --git a/src/game/tetromino/placing.h b/src/game/tetromino/placing.h index df6b6cc..9ac59f1 100644 --- a/src/game/tetromino/placing.h +++ b/src/game/tetromino/placing.h @@ -5,18 +5,7 @@ #include "../game.h" -typedef uint8_t input_data; -enum { - MOVE_NONE = 0, - MOVE_LEFT = 1, - MOVE_RIGHT = 2, - MOVE_DOWN = 4, - MOVE_ROTLEFT = 8, - MOVE_ROTRIGHT = 16, -}; - -void place_update(gamedata* game_data, input_data move); - -#ifdef DEBUG -void dbg_set_all(gamedata* game_data); -#endif +// TODO: this singular function does too much, break it up via different returns and such. +/* updates the movement of the pdat structure, updating the rows when colliding downwards. + * closes window when the next shape intersects with the current one */ +void place_update(struct gamedata* gdat, int movdat); diff --git a/src/io/render.c b/src/io/render.c index e897322..febad48 100644 --- a/src/io/render.c +++ b/src/io/render.c @@ -7,81 +7,63 @@ #include #include #include +#include #include #include -#include #include #include "../error.h" #include "../game/game.h" #include "../game/tetromino/shapes.h" #include "../util/types.h" +#include "../util/vec.h" #include "colour/colour32.h" #include "colour/colour8.h" #define COLOUR_SCORE COLOUR32_YELLOW -void render_init(renderdata* const render_dat, gamedata const* const game_dat) { - SDL_Window* const window = SDL_CreateWindow("tetris clone", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN); - if (window == NULL) fatal(ERROR_SDL_RENDERING_INIT, "Window failed to be created! SDL Error: %s", SDL_GetError()); +SDL_Renderer* rend = NULL; +TTF_Font* font = NULL; +struct gamedata const* gdat = NULL; - SDL_Renderer* const renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED); - if (renderer == NULL) fatal(ERROR_SDL_RENDERING_INIT, "Renderer failed to be created! SDL Error: %s", SDL_GetError()); +static SDL_Surface* score_surface = NULL; +static SDL_Texture* score_texture = NULL; - TTF_Font* const font = TTF_OpenFont("pixeldroid_botic-regular.ttf", PX_DENS); +void render_init(SDL_Window* win, struct gamedata const* game_data) { + rend = SDL_CreateRenderer(win, -1, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED); + if (rend == NULL) fatal(ERROR_SDL_RENDERING_INIT, "Renderer failed to be created! SDL Error: %s", SDL_GetError()); + + font = TTF_OpenFont("pixeldroid_botic-regular.ttf", PX_DENS); if (font == NULL) error("Failed to open font! TTF Error: %s", TTF_GetError()); - // initialize the render data - *render_dat = (renderdata){ - game_dat, - window, - renderer, - font, - calloc(1, sizeof(struct render_cache)), // zero-initialize the memory as we read from it - }; + gdat = game_data; } -static inline int32_t get_column_pos(uint8_t column) { +static inline int32_t get_column_pos(uint column) { return column * BLOCK_WIDTH + 1 + TET_PADDING; } -static inline int32_t get_row_pos(uint8_t row) { +static inline int32_t get_row_pos(uint row) { return row * BLOCK_HEIGHT + 1 + TET_PADDING; } -static void draw_score_text(renderdata const* dat) { - struct render_cache* const cache = dat->cache; - uint16_t const score = dat->game_dat->score; +static void draw_score_text(void) { + static u16 prev_pts = 0xFFFF; - SDL_Renderer* const renderer = dat->renderer; - TTF_Font* const font = dat->font; + if (prev_pts ^ gdat->pnts) { + char score_text[6] = "0"; // max digits of a u16 + null terminator + prev_pts = gdat->pnts; + if (gdat->pnts) sprintf(score_text, "%hu0", gdat->pnts); - if (cache->prevscore != score || cache->score_texture == NULL) { - char score_text[6]; // max digits of a uint16 + \0 terminator - if (!score) sprintf(score_text, "0"); - else sprintf(score_text, "%hu0", score); - - SDL_Surface* const txt_surface = TTF_RenderText_Solid(font, score_text, (SDL_Colour){COLOUR_SCORE.r, COLOUR_SCORE.g, COLOUR_SCORE.b, COLOUR_SCORE.a}); - SDL_Texture* const txt_texture = SDL_CreateTextureFromSurface(renderer, txt_surface); - - if (cache->score_texture != NULL || cache->score_surface != NULL) { - // free old data - SDL_FreeSurface(cache->score_surface); - SDL_DestroyTexture(cache->score_texture); - } - - // write data to cache - cache->score_surface = txt_surface; - cache->score_texture = txt_texture; + SDL_FreeSurface(score_surface); + SDL_DestroyTexture(score_texture); + score_surface = TTF_RenderText_Solid(font, score_text, (SDL_Colour){COLOUR_SCORE.r, COLOUR_SCORE.g, COLOUR_SCORE.b, COLOUR_SCORE.a}); + score_texture = SDL_CreateTextureFromSurface(rend, score_surface); } - if (cache->score_surface == NULL || cache->score_texture == NULL) { - error("the score texture was unavailable!", ); - return; - } - - SDL_Rect text_rect = {get_column_pos(COLUMNS + 1), get_row_pos(0), cache->score_surface->w, cache->score_surface->h}; - SDL_RenderCopy(renderer, cache->score_texture, NULL, &text_rect); + assert(score_surface && score_texture); + SDL_Rect text_rect = {get_column_pos(COLUMNS + 1), get_row_pos(0), score_surface->w, score_surface->h}; + SDL_RenderCopy(rend, score_texture, NULL, &text_rect); } // TODO: this is suboptimal, since each block will be 2 triangles, wasting perf. Consider using switch...case hard-coded drawing @@ -92,72 +74,61 @@ static inline int draw_block(SDL_Renderer* const renderer, int8_t const x, int8_ } // draws a shape at the specified position -static void draw_shape(SDL_Renderer* const renderer, u8 const id, int8_t const pos_x, int8_t const pos_y) { +static void draw_shape(u8 const id, i8vec2 pos) { u16 shape = shape_from_id(id); - set_colour8(renderer, colour_from_id(id)); + set_colour8(rend, colour_from_id(id)); for (int8_t y = 0; y < SHAPE_HEIGHT; y++) { u8 shape_row = shape_get_row(shape, y); - if (shape_row == 0) - continue; + if (shape_row == 0) continue; for (int8_t x = 0; x < SHAPE_WIDTH; x++) if (shape_is_set(shape_row, x)) - draw_block(renderer, pos_x + x, pos_y + y); + draw_block(rend, pos[0] + x, pos[1] + y); } } // draw the block data in the level -static void render_level(SDL_Renderer* const renderer, gamedata const* const data) { - for (int8_t y = 0; y < ROWS; y++) { - row_const const row = data->rows[y]; +static void render_level(void) { + for (int y = 0; y < ROWS; y++) { + u8 const* row = gdat->rows[y]; - for (int8_t x = 0; x < COLUMNS; x++) { + for (int x = 0; x < COLUMNS; x++) { if (row[x] != 0) { - set_colour8(renderer, row[x]); - draw_block(renderer, x, y); + set_colour8(rend, row[x]); + draw_block(rend, x, y); } } } } -void render_update(renderdata const* const dat) { - SDL_Renderer* const renderer = dat->renderer; - gamedata const* const game_data = dat->game_dat; - - int success = 0; // if an error occurs, this value is <0 - - // clear render - set_colour32(renderer, COLOUR32_BLACK); // using colour32 is more efficient, as it sets the colours directly - success |= SDL_RenderClear(renderer); - - set_colour32(renderer, COLOUR32_WHITE); +void render_update(void) { + set_colour32(rend, COLOUR32_BLACK); + SDL_RenderClear(rend); + set_colour32(rend, COLOUR32_WHITE); static SDL_Rect const field_size = {TET_PADDING, TET_PADDING, TET_WIDTH + 1, TET_HEIGHT + 1}; - SDL_RenderDrawRect(renderer, &field_size); - draw_shape(renderer, game_data->nxt[game_data->curr_idx + 1], COLUMNS + 1, 3); // draw the next shape + SDL_RenderDrawRect(rend, &field_size); - if (dat->font) - draw_score_text(dat); + if (font) draw_score_text(); - render_level(renderer, dat->game_dat); - draw_shape(renderer, game_data->nxt[game_data->curr_idx], game_data->sel_x, game_data->sel_y); // draw the current shape + render_level(); + draw_shape(gdat->pdat.nxt[gdat->pdat.idx], gdat->pdat.sel); + draw_shape(gdat->pdat.nxt[gdat->pdat.idx + 1], (i8vec2){COLUMNS + 1, 3}); - if (success < 0) { - warn("something went wrong whilst renderering! SDL Error: %s\n", SDL_GetError()); - return; - } - - SDL_RenderPresent(renderer); + SDL_RenderPresent(rend); } -void render_free(renderdata* const render_data) { - SDL_DestroyRenderer(render_data->renderer); - SDL_DestroyWindow(render_data->window); - TTF_CloseFont(render_data->font); - SDL_FreeSurface(render_data->cache->score_surface); - SDL_DestroyTexture(render_data->cache->score_texture); - free(render_data->cache); - *render_data = (renderdata){0}; +void render_free(void) { + assert(rend); + SDL_DestroyRenderer(rend); + rend = NULL; + + TTF_CloseFont(font); + SDL_FreeSurface(score_surface); + SDL_DestroyTexture(score_texture); + font = NULL; + score_surface = NULL; + score_texture = NULL; } diff --git a/src/io/render.h b/src/io/render.h index 2aa610e..a907908 100644 --- a/src/io/render.h +++ b/src/io/render.h @@ -1,10 +1,8 @@ #pragma once -#include -#include -#include #include #include +#include #include "../game/game.h" @@ -17,22 +15,6 @@ #define BLOCK_WIDTH (TET_WIDTH / COLUMNS) // width of a block #define BLOCK_HEIGHT (TET_HEIGHT / ROWS) // height of a block -// contains the data that's cached between renders -struct render_cache { - SDL_Texture* score_texture; - SDL_Surface* score_surface; - uint16_t prevscore; -}; - -// contains the data necessary for rendering -typedef struct { - gamedata const* game_dat; - SDL_Window* window; - SDL_Renderer* renderer; - TTF_Font* font; - struct render_cache* cache; -} renderdata; - -void render_init(renderdata*, gamedata const*); // initializes the renderer, outputs to render_data -void render_update(renderdata const*); // causes a draw to occur, will also determine update rate -void render_free(renderdata*); // frees the memory allocated to the renderer in render_data +__nonnull((1, 2)) void render_init(SDL_Window*, struct gamedata const*); +void render_update(void); // causes a draw to occur, will also determine update rate +void render_free(void); // frees the memory allocated to the renderer in render_data diff --git a/src/io/window.c b/src/io/window.c index 8da1279..c92d233 100644 --- a/src/io/window.c +++ b/src/io/window.c @@ -5,17 +5,20 @@ #include #include #include +#include #include "../error.h" #include "../game/game.h" +#include "../game/time.h" #include "audio.h" #include "input.h" #include "render.h" static SDL_Window* win = NULL; static bool close = false; +static audiodata music; -void window_init(void) { +static void window_init(struct gamedata const* gdat) { assert(!win && !close); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) fatal(ERROR_SDL_INIT, "SDL could not initialize! SDL Error: %s", SDL_GetError()); @@ -24,27 +27,37 @@ void window_init(void) { if (window == NULL) fatal(ERROR_SDL_RENDERING_INIT, "Window failed to be created! SDL Error: %s", SDL_GetError()); - render_init(win); + render_init(win, gdat); audio_device_init(32000, AUDIO_S16, 1, 4096); + music = audio_wav_load("korobeiniki.wav"); +} + +static void window_free(void) { + assert(win); + // render_free(); + SDL_DestroyWindow(win); + win = NULL; + close = false; + + audio_wav_unload(&music); + audio_device_free(); +} + +void window_open(struct gamedata const* gdat) { + window_init(gdat); + + while (!close) { + game_update(input_getdat()); + render_update(); + + static time_t timeout = 0; + if (time_poll(time_pull(), music.ms, &timeout)) + audio_play(&music); + } + + window_free(); } void window_close(void) { close = true; } - -void window_loop(void) { - assert(win); - - while (!close) { - game_update(input_getdat()); - render_update(); - } -} - -void window_free(void) { - assert(win); - render_free(); - SDL_DestroyWindow(win); - win = NULL; - close = false; -} diff --git a/src/io/window.h b/src/io/window.h index 06647f9..ad5eed7 100644 --- a/src/io/window.h +++ b/src/io/window.h @@ -6,7 +6,5 @@ #define SCREEN_WIDTH ((COLUMNS + 6) * PX_DENS) // window width #define SCREEN_HEIGHT ((COLUMNS) * PX_DENS / COLUMNS * ROWS) // window height -void window_init(void); +void window_open(struct gamedata const*); void window_close(void); -void window_loop(void); -void window_free(void); diff --git a/src/main.c b/src/main.c index 8d4823f..9aa6c56 100644 --- a/src/main.c +++ b/src/main.c @@ -1,62 +1,23 @@ #include -#include -#include -#include +#include #include "error.h" #include "game/game.h" -#include "game/paths.h" -#include "io/render.h" +#include "io/window.h" -// initialized in init(), reading beforehand is undefined behaviour -static gamedata gdat; -static renderdata rdat; - -// initialize the game -static void init(void) { - // initialize SDL - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) fatal(ERROR_SDL_INIT, "SDL could not initialize! SDL Error: %s", SDL_GetError()); - if (TTF_Init() != 0) fatal(ERROR_SDL_FONT_INIT, "the TTF module of SDL could not initialize! TTF Error: %s", TTF_GetError()); - - // initialize other game components - paths_init(); - game_init(&gdat); - render_init(&rdat, &gdat); -} - -// perform the updates to the game -static void update(void) { - // update the input - { - SDL_Event e; - while (SDL_PollEvent(&e)) { - switch (e.type) { - case SDL_QUIT: - gdat.run = false; - break; - } - } - } - - // perform updates - game_update(&gdat); - render_update(&rdat); +static void stop(void) { + debug("stopping...", ); + window_close(); + SDL_Quit(); } // entry-point of the application int main(int argc, char** argv) { (void)argc, (void)argv; + // register stop as exit function + atexit(stop); - init(); - debug("successfully initialized!", ); + window_open(game_init()); - while (gdat.run == true) - update(); - - debug("done! starting to free resources...", ); - game_free(&gdat); - render_free(&rdat); - paths_free(); - SDL_Quit(); return 0; }