/* * 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 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 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 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 struct { int svar, animlen; double speed; dvec_t pos, dir; ivec_t animstep, svarstep, ext; dbox_t fbox; SDL_Texture *tex; } dynentity_t; typedef struct { unsigned animframes; SDL_Rect src; SDL_Texture *tex; } objtype_t; typedef struct { unsigned type; ivec_t pos; } 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; } gamestate_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 propint(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 = propint(node, "x"); const int chunk_y = propint(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) { 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 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 *)"object") != 0) node = node->next; assert(NULL != node); unsigned id = propint(node, "gid"); 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 = propint(node, "width"); state->objtypes[id].src.h = propint(node, "height"); 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) { 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++]; // Load object type xmlChar *templ = xmlGetProp(node, (const xmlChar *)"template"); assert(templ); o->type = load_objtype(state, (const char *)templ, renderer); xmlFree(templ); // Load object location o->pos.x = propint(node, "x"); o->pos.y = propint(node, "y"); } while ((node = node->next) != NULL); } 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); } 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.svar = SPRITE_DIR_DOWN; state->p.animlen = PANIMLEN; state->p.animstep.x = PWIDTH; state->p.svarstep.y = PHEIGHT; 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; } 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 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) 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; } } 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 - type->src.h)); 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 (unsigned i = 0; i < state->objcol.n; ++i) render_objentity(state, renderer, t, &state->objcol.buf[i]); render_dynentity(state, renderer, &state->p, t); }