diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 3f5fc9d..247a9fb 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,10 +1,9 @@ -add_executable(game - main.c -) +add_executable(game main.c) set_default_target_options(game) target_include_directories(game PUBLIC include) target_link_libraries(game PRIVATE + engine m LibXml2::LibXml2 SDL2::SDL2 diff --git a/app/main.c b/app/main.c index a2699ec..1394757 100644 --- a/app/main.c +++ b/app/main.c @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -#include +#include "engine_hooks.h" + #include #include #include @@ -82,10 +83,9 @@ typedef struct { int svar, animlen; double speed; dvec_t pos, dir; - ivec_t animstep, svarstep; + ivec_t animstep, svarstep, ext; dbox_t fbox; SDL_Texture *tex; - SDL_Rect src; } entity_t; typedef struct { @@ -104,30 +104,32 @@ typedef struct { obj_t *buf; } objcol_t; -static const char *assetdir; -static SDL_Window *window; -static SDL_Renderer *renderer; -static unsigned map[MAPNLAYERS][MAPWIDTH][MAPHEIGHT]; -static SDL_Texture *tstex, *pidle, *pwalk; -static input_state_t input; -static dvec_t vpos = { -128, -96 }; +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; + entity_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[] = { 284, 485, 486, 525, 527, 566, 567, 731, 768, 770, 771, 804, 805, 806, 808, 845, }; -static objtype_t objtypes[MAXOBJTYPES]; -static objcol_t objcol; -static entity_t p = { - .svar = SPRITE_DIR_DOWN, - .animlen = PANIMLEN, - .animstep = { .x = PWIDTH }, - .svarstep = { .y = PHEIGHT }, - .fbox = { - .off = { .x = PFBOFFX, .y = PFBOFFY }, - .ext = { .x = PFBWIDTH, .y = PFBHEIGHT }, - }, - .src = { .w = PWIDTH, .h = PHEIGHT }, -}; static inline double mag(dvec_t v) { @@ -139,7 +141,8 @@ static inline double dot(dvec_t v, dvec_t u) return v.x * u.x + v.y * u.y; } -static inline unsigned tileat(int layeridx, double x, double y) +static inline unsigned +tileat(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; @@ -149,10 +152,10 @@ static inline unsigned tileat(int layeridx, double x, double y) return map[layeridx][row][col]; } -static inline bool tilepassable(int x, int y) +static inline bool tilepassable(const map_t map, int x, int y) { for (unsigned l = 0; l < MAPNLAYERS; ++l) { - const unsigned id = tileat(l, x, y); + const unsigned id = tileat(map, l, x, y); for (unsigned i = 0; i < NELEMS(impassable); ++i) { if (impassable[i] == id) return false; @@ -161,7 +164,7 @@ static inline bool tilepassable(int x, int y) return true; } -static void maploadlayer(xmlNodePtr layernode, int layeridx) +static void maploadlayer(map_t map, xmlNodePtr layernode, int layeridx) { xmlNodePtr node = layernode->xmlChildrenNode; while (NULL != node @@ -220,11 +223,12 @@ static void maploadlayer(xmlNodePtr layernode, int layeridx) } } -static unsigned objtypeload(const char *templ) +static unsigned +objtypeload(gamestate_t *state, const char *templ, SDL_Renderer *renderer) { static char buf[MAX_PATH_LEN]; - assert(strlen(assetdir) + strlen(templ) + 1 < MAX_PATH_LEN); - strcpy(buf, assetdir); + assert(strlen(state->assetdir) + strlen(templ) + 1 < MAX_PATH_LEN); + strcpy(buf, state->assetdir); strcat(buf, "/"); strcat(buf, templ); @@ -244,12 +248,12 @@ static unsigned objtypeload(const char *templ) assert(id < MAXOBJTYPES); // Populate objtype struct if not already loaded - if (NULL == objtypes[id].tex) { - objtypes[id].src.x = 0; - objtypes[id].src.y = 0; - objtypes[id].src.w + if (NULL == state->objtypes[id].tex) { + state->objtypes[id].src.x = 0; + state->objtypes[id].src.y = 0; + state->objtypes[id].src.w = atoi((const char *)xmlGetProp(node, (const xmlChar *)"width")); - objtypes[id].src.h = atoi( + state->objtypes[id].src.h = atoi( (const char *)xmlGetProp(node, (const xmlChar *)"height")); node = node->xmlChildrenNode; @@ -265,13 +269,13 @@ static unsigned objtypeload(const char *templ) const char *val = (const char *)xmlGetProp(node, (const xmlChar *)"value"); if (strcmp(key, "animframes") == 0) { - objtypes[id].animframes = atoi(val); + state->objtypes[id].animframes = atoi(val); } else if (strcmp(key, "assetpath") == 0) { - assert(strlen(assetdir) + strlen(val) < MAX_PATH_LEN); - strcpy(buf, assetdir); + assert(strlen(state->assetdir) + strlen(val) < MAX_PATH_LEN); + strcpy(buf, state->assetdir); strcat(buf, val); - objtypes[id].tex = IMG_LoadTexture(renderer, buf); - assert(NULL != objtypes[id].tex); + state->objtypes[id].tex = IMG_LoadTexture(renderer, buf); + assert(NULL != state->objtypes[id].tex); } else { assert(false); } @@ -281,12 +285,13 @@ static unsigned objtypeload(const char *templ) return id; } -static void maploadobjects(xmlNodePtr node) +static void +maploadobjects(gamestate_t *state, xmlNodePtr node, SDL_Renderer *renderer) { - objcol.n = 0; - objcol.cap = INITOBJCOLCAP; - objcol.buf = malloc(sizeof(obj_t) * objcol.cap); - assert(objcol.buf); + state->objcol.n = 0; + state->objcol.cap = INITOBJCOLCAP; + state->objcol.buf = malloc(sizeof(obj_t) * state->objcol.cap); + assert(state->objcol.buf); node = node->xmlChildrenNode; assert(NULL != node); @@ -295,17 +300,18 @@ static void maploadobjects(xmlNodePtr node) continue; // Get slot for object, growing buffer if needed. - if (objcol.n == objcol.cap) { - objcol.cap *= 2; - objcol.buf = realloc(objcol.buf, sizeof(obj_t) * objcol.cap); - assert(objcol.buf); + if (state->objcol.n == state->objcol.cap) { + state->objcol.cap *= 2; + state->objcol.buf = realloc( + state->objcol.buf, sizeof(obj_t) * state->objcol.cap); + assert(state->objcol.buf); } - obj_t *o = &objcol.buf[objcol.n++]; + obj_t *o = &state->objcol.buf[state->objcol.n++]; // Load object type const xmlChar *templ = xmlGetProp(node, (const xmlChar *)"template"); assert(NULL != templ); - o->type = objtypeload((const char *)templ); + o->type = objtypeload(state, (const char *)templ, renderer); // Load object location and set size from objtype o->pos.x @@ -315,7 +321,8 @@ static void maploadobjects(xmlNodePtr node) } while ((node = node->next) != NULL); } -static void mapload(const char *path) +static void +mapload(gamestate_t *state, const char *path, SDL_Renderer *renderer) { xmlDocPtr doc; xmlNodePtr node; @@ -328,20 +335,22 @@ static void mapload(const char *path) assert(NULL != node); do { if (xmlStrcmp(node->name, (const xmlChar *)"layer") == 0) - maploadlayer(node, layer++); + maploadlayer(state->map, node, layer++); else if (xmlStrcmp(node->name, (const xmlChar *)"objectgroup") == 0) - maploadobjects(node); + maploadobjects(state, node, renderer); } while ((node = node->next) != NULL); } -static void mapdraw(void) +static void mapdraw(const gamestate_t *state, SDL_Renderer *renderer) { - const int startx = TILESIZE * floor(vpos.x / TILESIZE); - const int starty = TILESIZE * floor(vpos.y / TILESIZE); + const int startx = TILESIZE * floor(state->vpos.x / TILESIZE); + const int starty = TILESIZE * floor(state->vpos.y / TILESIZE); + const int endx = state->vpos.x + VIEWWIDTH; + const int endy = state->vpos.y + VIEWHEIGHT; for (int l = 0; l < MAPNLAYERS; ++l) { - for (int y = starty; y < vpos.y + VIEWHEIGHT; y += TILESIZE) { - for (int x = startx; x < vpos.x + VIEWWIDTH; x += TILESIZE) { - const unsigned tileid = tileat(l, x, y); + for (int y = starty; y < endy; y += TILESIZE) { + for (int x = startx; x < endx; x += TILESIZE) { + const unsigned tileid = tileat(state->map, l, x, y); if (0 == tileid) continue; const SDL_Rect src = { @@ -351,18 +360,18 @@ static void mapdraw(void) .h = TILESIZE, }; const SDL_Rect dest = { - .x = (int)rint(SCALE * (x - vpos.x)), - .y = (int)rint(SCALE * (y - vpos.y)), + .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, tstex, &src, &dest); + SDL_RenderCopy(renderer, state->tstex, &src, &dest); } } } } -static void entityupdate(entity_t *e, double dt) +static void entityupdate(gamestate_t *state, entity_t *e, double dt) { if (0 == e->speed) { // Round position to nearest integer to align with pixel grid. @@ -394,42 +403,53 @@ static void entityupdate(entity_t *e, double dt) const dvec_t pfb = { .x = nextx + e->fbox.off.x, .y = nexty + e->fbox.off.y }; bool valid = true; - valid &= tilepassable(pfb.x, pfb.y); - valid &= tilepassable(pfb.x + e->fbox.ext.x, pfb.y); - valid &= tilepassable(pfb.x, pfb.y + e->fbox.ext.y); - valid &= tilepassable(pfb.x + e->fbox.ext.x, pfb.y + e->fbox.ext.y); + valid &= tilepassable(state->map, pfb.x, pfb.y); + valid &= tilepassable(state->map, pfb.x + e->fbox.ext.x, pfb.y); + valid &= tilepassable(state->map, pfb.x, pfb.y + e->fbox.ext.y); + valid &= tilepassable( + state->map, pfb.x + e->fbox.ext.x, pfb.y + e->fbox.ext.y); if (valid) { e->pos.x = nextx; e->pos.y = nexty; } } -static void entitydraw(entity_t *e, uint64_t t) +static void entitydraw( + const gamestate_t *state, SDL_Renderer *renderer, const entity_t *e, + uint64_t t) { const unsigned frame = (t / BASEANIMPERIOD) % e->animlen; - e->src.x = e->animstep.x * frame + e->svarstep.x * e->svar; - e->src.y = e->animstep.y * frame + e->svarstep.y * e->svar; - SDL_Rect dest = { - .x = (int)rint(SCALE * (e->pos.x - e->src.w / 2 - vpos.x)), - .y = (int)rint(SCALE * (e->pos.y - e->src.h / 2 - vpos.y)), - .w = SCALE * e->src.w, - .h = SCALE * e->src.h, + 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, }; - SDL_RenderCopy(renderer, e->tex, &e->src, &dest); + 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 objsdraw(uint64_t t) +static void +objsdraw(const gamestate_t *state, SDL_Renderer *renderer, uint64_t t) { - for (unsigned i = 0; i < objcol.n; ++i) { - const obj_t *obj = &objcol.buf[i]; + for (unsigned i = 0; i < state->objcol.n; ++i) { + const obj_t *obj = &state->objcol.buf[i]; assert(obj->type < MAXOBJTYPES); - const objtype_t *type = &objtypes[obj->type]; + 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 = (int)rint(SCALE * (obj->pos.x - vpos.x)), - .y = (int)rint(SCALE * (obj->pos.y - vpos.y - type->src.h)), + .x = x, + .y = y, .w = SCALE * type->src.w, .h = SCALE * type->src.h, }; @@ -437,164 +457,176 @@ static void objsdraw(uint64_t t) } } -int main(int argc, char *argv[]) +void game_init(int argc, char *argv[], void *mem, SDL_Renderer *renderer) { char path[MAX_PATH_LEN]; + gamestate_t *state = (gamestate_t *)mem; - if (2 != argc) { - fprintf(stderr, "Usage: %s ASSETS-DIR\n", argv[0]); - return 1; - } - assetdir = argv[1]; - - // Set up SDL window - int err = SDL_Init(SDL_INIT_VIDEO); - assert(0 == err); - window = SDL_CreateWindow( - "2D Game", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - SCALE * VIEWWIDTH, SCALE * VIEWHEIGHT, 0); - assert(NULL != window); - renderer = SDL_CreateRenderer(window, -1, 0); - assert(NULL != renderer); + assert(2 == argc); + state->assetdir = argv[1]; // Load map - assert(strlen(assetdir) + strlen(MAP_ASSET) < MAX_PATH_LEN); - strcpy(path, assetdir); + assert(strlen(state->assetdir) + strlen(MAP_ASSET) < MAX_PATH_LEN); + strcpy(path, state->assetdir); strcat(path, MAP_ASSET); - mapload(path); + mapload(state, path, renderer); // Load tileset - assert(strlen(assetdir) + strlen(TSASSET) < MAX_PATH_LEN); - strcpy(path, assetdir); + assert(strlen(state->assetdir) + strlen(TSASSET) < MAX_PATH_LEN); + strcpy(path, state->assetdir); strcat(path, TSASSET); - tstex = IMG_LoadTexture(renderer, path); - assert(NULL != tstex); + state->tstex = IMG_LoadTexture(renderer, path); + assert(NULL != state->tstex); // Load player spritesheets and initialize texture - assert(strlen(assetdir) + strlen(PIDLE_ASSET) < MAX_PATH_LEN); - strcpy(path, assetdir); + assert(strlen(state->assetdir) + strlen(PIDLE_ASSET) < MAX_PATH_LEN); + strcpy(path, state->assetdir); strcat(path, PIDLE_ASSET); - pidle = IMG_LoadTexture(renderer, path); - assert(NULL != pidle); - assert(strlen(assetdir) + strlen(PWALK_ASSET) < MAX_PATH_LEN); - strcpy(path, assetdir); + 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); - pwalk = IMG_LoadTexture(renderer, path); - assert(NULL != pwalk); - p.tex = pidle; + state->pwalk = IMG_LoadTexture(renderer, path); + assert(NULL != state->pwalk); - bool f = true; - SDL_SetRenderDrawColor(renderer, 0xff, 0x00, 0x00, 0xff); - SDL_Event event; - uint64_t prevt = SDL_GetTicks64(); - while (1) { - // Calculate dt - const uint64_t t = SDL_GetTicks64(); - const uint64_t dt_ms = t - prevt; - const double dt = dt_ms / 1000.0; - prevt = t; + // Set view position + state->vpos.x = -128; + state->vpos.y = -96; - // Handle events - SDL_PollEvent(&event); - switch (event.type) { - case SDL_QUIT: - goto quit; + // 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; +} - case SDL_KEYDOWN: - switch (event.key.keysym.sym) { - case SDLK_LEFT: - input.left = true; - break; - case SDLK_RIGHT: - input.right = true; - break; - case SDLK_UP: - input.up = true; - break; - case SDLK_DOWN: - input.down = true; - break; - default: - break; - } +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; + } +} - case SDL_KEYUP: - switch (event.key.keysym.sym) { - case SDLK_LEFT: - input.left = false; - break; - case SDLK_RIGHT: - input.right = false; - break; - case SDLK_UP: - input.up = false; - break; - case SDLK_DOWN: - input.down = false; - break; - default: - break; - } +gamestatus_t game_evthandle(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; - // Calculate player velocity and update player - p.dir.x = (input.left ? -1 : 0) + (input.right ? 1 : 0); - p.dir.y = (input.up ? -1 : 0) + (input.down ? 1 : 0); - const double dmag = mag(p.dir); - if (dmag != 0) { - p.dir.x /= dmag; - p.dir.y /= dmag; - p.tex = pwalk; - p.speed = WALKSPEED; - } else { - p.tex = pidle; - p.speed = 0; - - // Round view position to nearest integer to align with - // pixel grid. - vpos.x = rint(vpos.x); - vpos.y = rint(vpos.y); + 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; } - entityupdate(&p, dt); + break; - // Update view - const dvec_t pvdisp = { - .x = p.pos.x - (vpos.x + VIEWWIDTH / 2), - .y = p.pos.y - (vpos.y + VIEWHEIGHT / 2), - }; - if (mag(pvdisp) > 72 && dot(pvdisp, p.dir) > 0) { - const double nextx = vpos.x + dt * p.speed * p.dir.x; - const double nexty = vpos.y + dt * p.speed * p.dir.y; - const bool validx = nextx >= MAPMINX && nextx < MAPMAXX; - const bool validy = nexty >= MAPMINY && nexty < MAPMAXY; - if (validx && validy) { - vpos.x = nextx; - vpos.y = nexty; - } - } - - f = !f; - - SDL_RenderClear(renderer); - mapdraw(); - objsdraw(t); - entitydraw(&p, t); - if (f) { - const SDL_Rect i = { .x = 10, .y = 10, .w = 20, .h = 20 }; - SDL_RenderFillRect(renderer, &i); - } - SDL_RenderPresent(renderer); + default: + break; } -quit: - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - - return 0; + return GAMESTATUS_OK; +} + +gamestatus_t game_update(void *mem, unsigned 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 / 1000.0; + } 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); + } + entityupdate(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; +} + +void game_render(const void *mem, SDL_Renderer *renderer, long unsigned t) +{ + const gamestate_t *state = (const gamestate_t *)mem; + mapdraw(state, renderer); + objsdraw(state, renderer, t); + entitydraw(state, renderer, &state->p, t); }