/* * Copyright (c) Camden Dixie O'Brien * SPDX-License-Identifier: AGPL-3.0-only */ #include "engine_hooks.h" #include #include #include #include #include #include #include #define SCALE 4 #define TILESIZE 16 #define VIEWWIDTH (16 * TILESIZE) #define VIEWHEIGHT (12 * TILESIZE) #define MAX_PATH_LEN 128 #define MAP_ASSET "/map.tmx" #define MAPNLAYERS 2 #define MAPWIDTH 64 #define MAPHEIGHT 64 #define MAPSHIFTX 32 #define MAPSHIFTY 32 #define MAPMINX (TILESIZE * (-32)) #define MAPMAXX (TILESIZE * 32) #define MAPMINY (TILESIZE * (-32)) #define MAPMAXY (TILESIZE * 32) #define OBJSPRITES "/objectsprites.tsx" #define TSASSET "/overworld.png" #define TSCOLS 40 #define PIDLE_ASSET "/player/idle.png" #define PWALK_ASSET "/player/walk.png" #define PWIDTH 48 #define PHEIGHT 64 #define PANIMLEN 8 #define PBBWIDTH 16 #define PBBHEIGHT 24 #define PFBWIDTH 20 #define PFBHEIGHT 16 #define PFBOFFX -10 #define PFBOFFY 2 #define WALKSPEED 72 // pixels per second #define BASEANIMPERIOD 100 #define MAXOBJTYPES 8 #define INITOBJCOLCAP 16 #define MAXENTITIES 32 #define QSORT_STACKSIZE 64 #define NELEMS(a) (sizeof(a) / sizeof(a[0])) typedef enum { SPRITE_DIR_DOWN = 0, SPRITE_DIR_LEFT_DOWN = 1, SPRITE_DIR_LEFT_UP = 2, SPRITE_DIR_UP = 3, SPRITE_DIR_RIGHT_UP = 4, SPRITE_DIR_RIGHT_DOWN = 5, } sprite_dir_t; typedef struct { bool left, right, up, down; } input_state_t; typedef struct { int x, y; } ivec_t; typedef struct { double x, y; } dvec_t; typedef struct { dvec_t off, ext; } dbox_t; typedef enum { ENTITY_DYN, ENTITY_OBJ, } entitytag_t; /* * Common entity header -- this must be at the start of each type of * entity structure. * * The tag is used to distinguish different types of entity when * accessing them through the drawlist. The prev and next pointers * allow the drawlist to be threaded through all entities as a * doubly-linked list. */ typedef struct entity { entitytag_t tag; struct entity *prev; struct entity *next; } entity_t; typedef struct { entity_t e; int svar, animlen; double speed; dvec_t pos, dir; ivec_t animstep, svarstep, ext; dbox_t bbox, fbox; SDL_Texture *tex; } dynentity_t; typedef struct { unsigned animframes; SDL_Rect src; SDL_Texture *tex; dbox_t bbox; } objtype_t; typedef struct { entity_t e; unsigned type; ivec_t pos; dbox_t bbox; } objentity_t; typedef struct { size_t n, cap; objentity_t *buf; } objcol_t; typedef unsigned map_t[MAPNLAYERS][MAPWIDTH][MAPHEIGHT]; typedef struct { const char *assetdir; map_t map; SDL_Texture *tstex, *pidle, *pwalk; input_state_t input; dvec_t vpos; objtype_t objtypes[MAXOBJTYPES]; objcol_t objcol; dynentity_t p; entity_t *drawlist; } gamestate_t; typedef struct { double y; entity_t *e; } y_entity_ref_t; const engineconf_t game_conf = { .win = { .title = "2D Game", .w = SCALE * VIEWWIDTH, .h = SCALE * VIEWHEIGHT, }, .memsize = sizeof(gamestate_t), }; static const unsigned impassable_tiles[] = { 284, 485, 486, 525, 527, 566, 567, 731, 768, 770, 771, 804, 805, 806, 808, 845, }; static inline unsigned map_tile(const map_t map, int layeridx, double x, double y) { const unsigned row = (unsigned)(floor(x / TILESIZE) + MAPSHIFTX); const unsigned col = (unsigned)(floor(y / TILESIZE) + MAPSHIFTY); if (row >= MAPWIDTH || col >= MAPHEIGHT) return 0; else return map[layeridx][row][col]; } static inline int prop_int(xmlNodePtr node, const char *prop) { xmlChar *str = xmlGetProp(node, (const xmlChar *)prop); assert(str); int val = atoi((const char *)str); xmlFree(str); return val; } static void load_layer(map_t map, xmlNodePtr layernode, int layeridx) { xmlNodePtr node = layernode->xmlChildrenNode; while (NULL != node && xmlStrcmp(node->name, (const xmlChar *)"data") != 0) node = node->next; assert(NULL != node); // Iterate through chunks and populate map array. xmlNodePtr chunk_contents; for (node = node->xmlChildrenNode; NULL != node; node = node->next) { if (0 != xmlStrcmp(node->name, (const xmlChar *)"chunk")) continue; const int chunk_x = prop_int(node, "x"); const int chunk_y = prop_int(node, "y"); chunk_contents = node->xmlChildrenNode; assert( 0 == xmlStrcmp(chunk_contents->name, (const xmlChar *)"text")); int x = chunk_x + MAPSHIFTX, y = chunk_y + MAPSHIFTY; xmlChar buf[10]; const xmlChar *p, *q; q = chunk_contents->content; while (isspace((const char)*q)) ++q; p = q; while ('\0' != *q) { switch (*q) { case (xmlChar)',': memset(buf, 0, sizeof(buf)); memcpy(buf, p, sizeof(xmlChar) * (q - p)); assert(x < MAPWIDTH && y < MAPHEIGHT); map[layeridx][x][y] = (unsigned)atoi((const char *)buf); ++x; p = ++q; break; case (xmlChar)'\n': if (x < MAPWIDTH) { memset(buf, 0, sizeof(buf)); memcpy(buf, p, sizeof(xmlChar) * (q - p)); assert(y < MAPHEIGHT); map[layeridx][x][y] = (unsigned)atoi((const char *)buf); } x = chunk_x + MAPSHIFTX; ++y; p = ++q; break; default: ++q; break; } } } } static unsigned load_objtype( gamestate_t *state, const char *templ, SDL_Renderer *renderer, xmlNodePtr sprites) { static char buf[MAX_PATH_LEN]; assert(strlen(state->assetdir) + strlen(templ) + 1 < MAX_PATH_LEN); strcpy(buf, state->assetdir); strcat(buf, "/"); strcat(buf, templ); // Identify object type ID and sprite ID xmlDocPtr doc = xmlParseFile(buf); assert(NULL != doc); xmlNodePtr node = xmlDocGetRootElement(doc); assert(NULL != node); assert(xmlStrcmp(node->name, (const xmlChar *)"template") == 0); node = node->xmlChildrenNode; while (node != NULL && xmlStrcmp(node->name, (const xmlChar *)"tileset") != 0) node = node->next; assert(NULL != node); const int sidoff = prop_int(node, "firstgid"); while (node != NULL && xmlStrcmp(node->name, (const xmlChar *)"object") != 0) node = node->next; assert(NULL != node); const unsigned id = prop_int(node, "type"); assert(id < MAXOBJTYPES); // Populate objtype struct if not already loaded if (NULL == state->objtypes[id].tex) { state->objtypes[id].src.x = 0; state->objtypes[id].src.y = 0; state->objtypes[id].src.w = prop_int(node, "width"); state->objtypes[id].src.h = prop_int(node, "height"); // Load bounding box from sprites document const int sid = prop_int(node, "gid") - sidoff; xmlNodePtr snode = sprites->xmlChildrenNode; while (snode && (xmlStrcmp(snode->name, (const xmlChar *)"tile") != 0 || prop_int(snode, "id") != sid)) snode = snode->next; assert(snode); snode = snode->xmlChildrenNode; while (snode && xmlStrcmp(snode->name, (const xmlChar *)"objectgroup") != 0) snode = snode->next; assert(snode); snode = snode->xmlChildrenNode; while (snode && xmlStrcmp(snode->name, (const xmlChar *)"object") != 0) snode = snode->next; assert(snode); state->objtypes[id].bbox.off.x = (double)prop_int(snode, "x"); state->objtypes[id].bbox.off.y = (double)prop_int(snode, "y"); state->objtypes[id].bbox.ext.x = (double)prop_int(snode, "width"); state->objtypes[id].bbox.ext.y = (double)prop_int(snode, "height"); // Load custom properties node = node->xmlChildrenNode; while (node != NULL && xmlStrcmp(node->name, (const xmlChar *)"properties") != 0) node = node->next; assert(NULL != node); for (node = node->xmlChildrenNode; NULL != node; node = node->next) { if (xmlStrcmp(node->name, (const xmlChar *)"property") != 0) continue; xmlChar *key = xmlGetProp(node, (const xmlChar *)"name"); assert(key); xmlChar *val = xmlGetProp(node, (const xmlChar *)"value"); assert(val); if (xmlStrcmp(key, (const xmlChar *)"animframes") == 0) { state->objtypes[id].animframes = atoi((const char *)val); } else if (xmlStrcmp(key, (const xmlChar *)"assetpath") == 0) { assert( strlen(state->assetdir) + strlen((const char *)val) < MAX_PATH_LEN); strcpy(buf, state->assetdir); strcat(buf, (const char *)val); state->objtypes[id].tex = IMG_LoadTexture(renderer, buf); assert(NULL != state->objtypes[id].tex); } else { assert(false); } xmlFree(key); xmlFree(val); } } xmlFreeDoc(doc); return id; } static void load_objects(gamestate_t *state, xmlNodePtr node, SDL_Renderer *renderer) { char path[MAX_PATH_LEN]; assert(strlen(state->assetdir) + strlen(OBJSPRITES) < MAX_PATH_LEN); strcpy(path, state->assetdir); strcat(path, OBJSPRITES); xmlDocPtr spritesdoc = xmlParseFile(path); xmlNodePtr sprites = xmlDocGetRootElement(spritesdoc); state->objcol.n = 0; state->objcol.cap = INITOBJCOLCAP; state->objcol.buf = malloc(sizeof(objentity_t) * state->objcol.cap); assert(state->objcol.buf); node = node->xmlChildrenNode; assert(NULL != node); do { if (xmlStrcmp(node->name, (const xmlChar *)"object") != 0) continue; // Get slot for object, growing buffer if needed. if (state->objcol.n == state->objcol.cap) { state->objcol.cap *= 2; state->objcol.buf = realloc( state->objcol.buf, sizeof(objentity_t) * state->objcol.cap); assert(state->objcol.buf); } objentity_t *o = &state->objcol.buf[state->objcol.n++]; o->e.tag = ENTITY_OBJ; // Load object type xmlChar *templ = xmlGetProp(node, (const xmlChar *)"template"); assert(templ); o->type = load_objtype(state, (const char *)templ, renderer, sprites); xmlFree(templ); const objtype_t *type = state->objtypes + o->type; // Load object location o->pos.x = prop_int(node, "x"); o->pos.y = prop_int(node, "y") - type->src.h; // Load bounding box from object type o->bbox = type->bbox; } while ((node = node->next) != NULL); xmlFreeDoc(spritesdoc); } static void load_map(gamestate_t *state, const char *path, SDL_Renderer *renderer) { xmlDocPtr doc = xmlParseFile(path); assert(NULL != doc); xmlNodePtr node = xmlDocGetRootElement(doc); assert(0 == xmlStrcmp(node->name, (const xmlChar *)"map")); node = node->xmlChildrenNode; int layer = 0; assert(NULL != node); do { if (xmlStrcmp(node->name, (const xmlChar *)"layer") == 0) load_layer(state->map, node, layer++); else if (xmlStrcmp(node->name, (const xmlChar *)"objectgroup") == 0) load_objects(state, node, renderer); } while ((node = node->next) != NULL); xmlFreeDoc(doc); } static void y_entity_ref_swap(y_entity_ref_t *a, y_entity_ref_t *b) { y_entity_ref_t tmp = *a; *a = *b; *b = tmp; } void init_drawlist(gamestate_t *state) { y_entity_ref_t buf[MAXENTITIES]; int n = 0; // Populate buffer with player and objects buf[n].y = (int)rint(state->p.pos.y) + state->p.ext.y / 2; buf[n++].e = (entity_t *)&state->p; for (unsigned i = 0; i < state->objcol.n; ++i) { assert(n < MAXENTITIES); objentity_t *obj = state->objcol.buf + i; buf[n].y = obj->pos.y; buf[n++].e = (entity_t *)obj; } // Quicksort on y value int stack[QSORT_STACKSIZE], *sp = stack; int l = 0, h = n; assert(n != 0); while (1) { int pivot = buf[l].y; int i = l, j = h; do { for (++i; buf[i].y <= pivot; ++i) ; for (--j; buf[j].y > pivot; --j) ; if (i < j) y_entity_ref_swap(buf + i, buf + j); } while (i < j); y_entity_ref_swap(buf + l, buf + j); assert(sp + 2 <= stack + QSORT_STACKSIZE); if (j + 2 < h) { *sp++ = j + 1; *sp++ = h; } if (l + 1 < j) { h = j; } else if (sp != stack) { h = *(--sp); l = *(--sp); } else { break; } } // Set up drawlist from buffer state->drawlist = buf[0].e; int i = 0; buf[i].e->prev = NULL; buf[i].e->next = buf[i + 1].e; for (++i; i < n - 1; ++i) { buf[i].e->prev = buf[i - 1].e; buf[i].e->next = buf[i + 1].e; } buf[i].e->prev = buf[i - 1].e; buf[i].e->next = NULL; } void game_init(int argc, char *argv[], void *mem, SDL_Renderer *renderer) { char path[MAX_PATH_LEN]; gamestate_t *state = (gamestate_t *)mem; assert(2 == argc); state->assetdir = argv[1]; // Load map assert(strlen(state->assetdir) + strlen(MAP_ASSET) < MAX_PATH_LEN); strcpy(path, state->assetdir); strcat(path, MAP_ASSET); load_map(state, path, renderer); // Load tileset assert(strlen(state->assetdir) + strlen(TSASSET) < MAX_PATH_LEN); strcpy(path, state->assetdir); strcat(path, TSASSET); state->tstex = IMG_LoadTexture(renderer, path); assert(NULL != state->tstex); // Load player spritesheets and initialize texture assert(strlen(state->assetdir) + strlen(PIDLE_ASSET) < MAX_PATH_LEN); strcpy(path, state->assetdir); strcat(path, PIDLE_ASSET); state->pidle = IMG_LoadTexture(renderer, path); assert(NULL != state->pidle); assert(strlen(state->assetdir) + strlen(PWALK_ASSET) < MAX_PATH_LEN); strcpy(path, state->assetdir); strcat(path, PWALK_ASSET); state->pwalk = IMG_LoadTexture(renderer, path); assert(NULL != state->pwalk); // Set view position state->vpos.x = -128; state->vpos.y = -96; // Initialize player state state->p.e.tag = ENTITY_DYN; state->p.svar = SPRITE_DIR_DOWN; state->p.animlen = PANIMLEN; state->p.animstep.x = PWIDTH; state->p.svarstep.y = PHEIGHT; state->p.bbox.off.x = -PBBWIDTH / 2; state->p.bbox.off.y = -PBBHEIGHT / 2; state->p.bbox.ext.x = PBBWIDTH; state->p.bbox.ext.y = PBBHEIGHT; state->p.fbox.off.x = PFBOFFX; state->p.fbox.off.y = PFBOFFY; state->p.fbox.ext.x = PFBWIDTH; state->p.fbox.ext.y = PFBHEIGHT; state->p.ext.x = PWIDTH; state->p.ext.y = PHEIGHT; state->p.tex = state->pidle; init_drawlist(state); } void game_teardown(void *mem) { gamestate_t *state = (gamestate_t *)mem; SDL_DestroyTexture(state->tstex); SDL_DestroyTexture(state->pidle); SDL_DestroyTexture(state->pwalk); for (int i = 0; i < MAXOBJTYPES; ++i) { if (NULL != state->objtypes[i].tex) SDL_DestroyTexture(state->objtypes[i].tex); else break; } free(state->objcol.buf); } gamestatus_t game_evt(void *mem, const SDL_Event *evt) { gamestate_t *state = (gamestate_t *)mem; switch (evt->type) { case SDL_QUIT: return GAMESTATUS_QUIT; case SDL_KEYDOWN: switch (evt->key.keysym.sym) { case SDLK_LEFT: state->input.left = true; break; case SDLK_RIGHT: state->input.right = true; break; case SDLK_UP: state->input.up = true; break; case SDLK_DOWN: state->input.down = true; break; default: break; } break; case SDL_KEYUP: switch (evt->key.keysym.sym) { case SDLK_LEFT: state->input.left = false; break; case SDLK_RIGHT: state->input.right = false; break; case SDLK_UP: state->input.up = false; break; case SDLK_DOWN: state->input.down = false; break; default: break; } break; default: break; } return GAMESTATUS_OK; } static inline double mag(dvec_t v) { return sqrt(v.x * v.x + v.y * v.y); } static inline double dot(dvec_t v, dvec_t u) { return v.x * u.x + v.y * u.y; } static inline bool tile_passable(const map_t map, int x, int y) { for (unsigned l = 0; l < MAPNLAYERS; ++l) { const unsigned id = map_tile(map, l, x, y); for (unsigned i = 0; i < NELEMS(impassable_tiles); ++i) { if (impassable_tiles[i] == id) return false; } } return true; } static inline double dynentity_bottom(const dynentity_t *e) { return e->pos.y + e->bbox.off.y + e->bbox.ext.y; } static inline double objentity_bottom(const objentity_t *e) { return (double)e->pos.y + e->bbox.off.y + e->bbox.ext.y; } static inline double entity_bottom(entity_t *e) { switch (e->tag) { case ENTITY_DYN: return dynentity_bottom((dynentity_t *)e); case ENTITY_OBJ: return objentity_bottom((objentity_t *)e); } } static void update_drawlist(dynentity_t *e, entity_t **drawlist) { if (e->dir.y == 0) return; if (e->dir.y > 0) { // The entity moved down -- check if it needs to be moved // along in the drawlist. entity_t *n = e->e.next; if (n == NULL) // The entity is already at the end -- no update required. return; const double y = dynentity_bottom(e); double nexty = entity_bottom(n); if (y <= nexty) // The entity is still behind the next entity -- no update // required. return; // Update required -- find the earliest entity that is in // front of e, or the end of the list. while (n->next != NULL && y > nexty) { n = n->next; nexty = entity_bottom(n); } assert(n != NULL); if (y > nexty) { // The end of the list was reached without finding a // larger or equal y -- move the entity to the end of the // list (after n). e->e.prev->next = e->e.next; e->e.next->prev = e->e.prev; e->e.prev = n; e->e.next = NULL; n->next = (entity_t *)e; } else { // An entity with greater or equal y was found -- move the // entity to just before n. if (e->e.prev == NULL) { // The entity is at the beginning of the list, we need // to update the drawlist pointer. *drawlist = e->e.next; e->e.next->prev = NULL; } else { e->e.prev->next = e->e.next; e->e.next->prev = e->e.prev; } e->e.prev = n->prev; e->e.next = n; n->prev->next = (entity_t *)e; n->prev = (entity_t *)e; } } else { // The entity moved up -- this is mostly symmetric with the // above case. entity_t *n = e->e.prev; if (n == NULL) return; const double y = dynentity_bottom(e); double prevy = entity_bottom(n); if (y >= prevy) return; while (n->prev != NULL && y < prevy) { n = n->prev; prevy = entity_bottom(n); } assert(n != NULL); if (y < prevy) { e->e.prev->next = e->e.next; e->e.next->prev = e->e.prev; e->e.prev = NULL; e->e.next = n; *drawlist = n->prev = (entity_t *)e; } else { if (e->e.next == NULL) { e->e.prev->next = NULL; } else { e->e.next->prev = e->e.prev; e->e.prev->next = e->e.next; } e->e.next = n->next; e->e.prev = n; n->next->prev = (entity_t *)e; n->next = (entity_t *)e; } } } static void update_dynentity(gamestate_t *state, dynentity_t *e, double dt) { if (0 == e->speed) { // Round position to nearest integer to align with pixel grid. e->pos.x = rint(e->pos.x); e->pos.y = rint(e->pos.y); return; } // Update sprite variant if (e->dir.y >= 0) { if (e->dir.x > 0) e->svar = SPRITE_DIR_RIGHT_DOWN; else if (e->dir.x < 0) e->svar = SPRITE_DIR_LEFT_DOWN; else e->svar = SPRITE_DIR_DOWN; } else if (e->dir.y < 0) { if (e->dir.x > 0) e->svar = SPRITE_DIR_RIGHT_UP; else if (e->dir.x < 0) e->svar = SPRITE_DIR_LEFT_UP; else e->svar = SPRITE_DIR_UP; } // Apply velocity, handling map collisions and updating the // drawlist const double nextx = e->pos.x + dt * e->speed * e->dir.x; const double nexty = e->pos.y + dt * e->speed * e->dir.y; const dvec_t pfb = { .x = nextx + e->fbox.off.x, .y = nexty + e->fbox.off.y }; bool valid = true; valid &= tile_passable(state->map, (int)rint(pfb.x), (int)rint(pfb.y)); valid &= tile_passable( state->map, (int)rint(pfb.x + e->fbox.ext.x), (int)rint(pfb.y)); valid &= tile_passable( state->map, (int)rint(pfb.x), (int)rint(pfb.y + e->fbox.ext.y)); valid &= tile_passable( state->map, (int)rint(pfb.x + e->fbox.ext.x), (int)rint(pfb.y + e->fbox.ext.y)); if (valid) { e->pos.x = nextx; e->pos.y = nexty; update_drawlist(e, &state->drawlist); } } gamestatus_t game_update(void *mem, double dt) { gamestate_t *state = (gamestate_t *)mem; // Calculate player velocity and update player state->p.dir.x = (state->input.left ? -1 : 0) + (state->input.right ? 1 : 0); state->p.dir.y = (state->input.up ? -1 : 0) + (state->input.down ? 1 : 0); const double dmag = mag(state->p.dir); if (dmag != 0) { state->p.dir.x /= dmag; state->p.dir.y /= dmag; state->p.tex = state->pwalk; state->p.speed = WALKSPEED; } else { state->p.tex = state->pidle; state->p.speed = 0; // Round view position to nearest integer to align with // pixel grid. state->vpos.x = rint(state->vpos.x); state->vpos.y = rint(state->vpos.y); } update_dynentity(state, &state->p, dt); // Update view const dvec_t pvdisp = { .x = state->p.pos.x - (state->vpos.x + VIEWWIDTH / 2), .y = state->p.pos.y - (state->vpos.y + VIEWHEIGHT / 2), }; if (mag(pvdisp) > 72 && dot(pvdisp, state->p.dir) > 0) { const double nextx = state->vpos.x + dt * state->p.speed * state->p.dir.x; const double nexty = state->vpos.y + dt * state->p.speed * state->p.dir.y; const bool validx = nextx >= MAPMINX && nextx < MAPMAXX; const bool validy = nexty >= MAPMINY && nexty < MAPMAXY; if (validx && validy) { state->vpos.x = nextx; state->vpos.y = nexty; } } return GAMESTATUS_OK; } static void render_map(const gamestate_t *state, SDL_Renderer *renderer) { const int startx = TILESIZE * floor(state->vpos.x / TILESIZE); const int starty = TILESIZE * floor(state->vpos.y / TILESIZE); const int endx = ceil(state->vpos.x + VIEWWIDTH); const int endy = ceil(state->vpos.y + VIEWHEIGHT); for (int l = 0; l < MAPNLAYERS; ++l) { for (int y = starty; y < endy; y += TILESIZE) { for (int x = startx; x < endx; x += TILESIZE) { const unsigned tileid = map_tile(state->map, l, x, y); if (0 == tileid) continue; const SDL_Rect src = { .x = TILESIZE * ((tileid - 1) % TSCOLS), .y = TILESIZE * ((tileid - 1) / TSCOLS), .w = TILESIZE, .h = TILESIZE, }; const SDL_Rect dest = { .x = (int)rint(SCALE * (x - state->vpos.x)), .y = (int)rint(SCALE * (y - state->vpos.y)), .w = SCALE * TILESIZE, .h = SCALE * TILESIZE, }; SDL_RenderCopy(renderer, state->tstex, &src, &dest); } } } } static void render_dynentity( const gamestate_t *state, SDL_Renderer *renderer, const dynentity_t *e, uint64_t t) { const unsigned frame = (t / BASEANIMPERIOD) % e->animlen; const SDL_Rect src = { .x = e->animstep.x * frame + e->svarstep.x * e->svar, .y = e->animstep.y * frame + e->svarstep.y * e->svar, .w = e->ext.x, .h = e->ext.y, }; const SDL_Rect dest = { .x = (int)rint(SCALE * (e->pos.x - e->ext.x / 2 - state->vpos.x)), .y = (int)rint(SCALE * (e->pos.y - e->ext.y / 2 - state->vpos.y)), .w = SCALE * e->ext.x, .h = SCALE * e->ext.y, }; SDL_RenderCopy(renderer, e->tex, &src, &dest); } static void render_objentity( const gamestate_t *state, SDL_Renderer *renderer, uint64_t t, const objentity_t *obj) { assert(obj->type < MAXOBJTYPES); const objtype_t *type = &state->objtypes[obj->type]; assert(NULL != type->tex); SDL_Rect src = type->src; src.x += type->src.w * ((t / BASEANIMPERIOD) % type->animframes); const int x = rint(SCALE * (obj->pos.x - state->vpos.x)); const int y = rint(SCALE * (obj->pos.y - state->vpos.y)); SDL_Rect dest = { .x = x, .y = y, .w = SCALE * type->src.w, .h = SCALE * type->src.h, }; SDL_RenderCopy(renderer, type->tex, &src, &dest); } void game_render(const void *mem, SDL_Renderer *renderer, long unsigned t) { const gamestate_t *state = (const gamestate_t *)mem; render_map(state, renderer); for (const entity_t *e = state->drawlist; e != NULL; e = e->next) { switch (e->tag) { case ENTITY_DYN: render_dynentity(state, renderer, (const dynentity_t *)e, t); break; case ENTITY_OBJ: render_objentity(state, renderer, t, (const objentity_t *)e); break; default: assert(false); } } }