/* * Copyright (c) Camden Dixie O'Brien * SPDX-License-Identifier: AGPL-3.0-only */ #include #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; dbox_t fbox; SDL_Texture *tex; SDL_Rect src; } entity_t; typedef struct { unsigned animframes; SDL_Rect src; SDL_Texture *tex; } objtype_t; typedef struct { unsigned type; ivec_t pos; } obj_t; typedef struct { size_t n, cap; 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 }; 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) { 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 unsigned tileat(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 bool tilepassable(int x, int y) { for (unsigned l = 0; l < MAPNLAYERS; ++l) { const unsigned id = tileat(l, x, y); for (unsigned i = 0; i < NELEMS(impassable); ++i) { if (impassable[i] == id) return false; } } return true; } static void maploadlayer(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; xmlChar *x_attr, *y_attr; x_attr = xmlGetProp(node, (const xmlChar *)"x"); y_attr = xmlGetProp(node, (const xmlChar *)"y"); const int chunk_x = atoi((const char *)x_attr); const int chunk_y = atoi((const char *)y_attr); 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 objtypeload(const char *templ) { static char buf[MAX_PATH_LEN]; assert(strlen(assetdir) + strlen(templ) + 1 < MAX_PATH_LEN); strcpy(buf, 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 = atoi((const char *)xmlGetProp(node, (const xmlChar *)"gid")); 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 = atoi((const char *)xmlGetProp(node, (const xmlChar *)"width")); objtypes[id].src.h = atoi( (const char *)xmlGetProp(node, (const xmlChar *)"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; const char *key = (const char *)xmlGetProp(node, (const xmlChar *)"name"); const char *val = (const char *)xmlGetProp(node, (const xmlChar *)"value"); if (strcmp(key, "animframes") == 0) { objtypes[id].animframes = atoi(val); } else if (strcmp(key, "assetpath") == 0) { assert(strlen(assetdir) + strlen(val) < MAX_PATH_LEN); strcpy(buf, assetdir); strcat(buf, val); objtypes[id].tex = IMG_LoadTexture(renderer, buf); assert(NULL != objtypes[id].tex); } else { assert(false); } } } return id; } static void maploadobjects(xmlNodePtr node) { objcol.n = 0; objcol.cap = INITOBJCOLCAP; objcol.buf = malloc(sizeof(obj_t) * objcol.cap); assert(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 (objcol.n == objcol.cap) { objcol.cap *= 2; objcol.buf = realloc(objcol.buf, sizeof(obj_t) * objcol.cap); assert(objcol.buf); } obj_t *o = &objcol.buf[objcol.n++]; // Load object type const xmlChar *templ = xmlGetProp(node, (const xmlChar *)"template"); assert(NULL != templ); o->type = objtypeload((const char *)templ); // Load object location and set size from objtype o->pos.x = atoi((const char *)xmlGetProp(node, (const xmlChar *)"x")); o->pos.y = atoi((const char *)xmlGetProp(node, (const xmlChar *)"y")); } while ((node = node->next) != NULL); } static void mapload(const char *path) { xmlDocPtr doc; xmlNodePtr node; doc = xmlParseFile(path); assert(NULL != doc); 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) maploadlayer(node, layer++); else if (xmlStrcmp(node->name, (const xmlChar *)"objectgroup") == 0) maploadobjects(node); } while ((node = node->next) != NULL); } static void mapdraw(void) { const int startx = TILESIZE * floor(vpos.x / TILESIZE); const int starty = TILESIZE * floor(vpos.y / TILESIZE); 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); 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 - vpos.x)), .y = (int)rint(SCALE * (y - vpos.y)), .w = SCALE * TILESIZE, .h = SCALE * TILESIZE, }; SDL_RenderCopy(renderer, tstex, &src, &dest); } } } } static void entityupdate(entity_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 &= 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); if (valid) { e->pos.x = nextx; e->pos.y = nexty; } } static void entitydraw(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, }; SDL_RenderCopy(renderer, e->tex, &e->src, &dest); } static void objsdraw(uint64_t t) { for (unsigned i = 0; i < objcol.n; ++i) { const obj_t *obj = &objcol.buf[i]; assert(obj->type < MAXOBJTYPES); const objtype_t *type = &objtypes[obj->type]; assert(NULL != type->tex); SDL_Rect src = type->src; src.x += type->src.w * ((t / BASEANIMPERIOD) % type->animframes); SDL_Rect dest = { .x = (int)rint(SCALE * (obj->pos.x - vpos.x)), .y = (int)rint(SCALE * (obj->pos.y - vpos.y - type->src.h)), .w = SCALE * type->src.w, .h = SCALE * type->src.h, }; SDL_RenderCopy(renderer, type->tex, &src, &dest); } } int main(int argc, char *argv[]) { char path[MAX_PATH_LEN]; 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); // Load map assert(strlen(assetdir) + strlen(MAP_ASSET) < MAX_PATH_LEN); strcpy(path, assetdir); strcat(path, MAP_ASSET); mapload(path); // Load tileset assert(strlen(assetdir) + strlen(TSASSET) < MAX_PATH_LEN); strcpy(path, assetdir); strcat(path, TSASSET); tstex = IMG_LoadTexture(renderer, path); assert(NULL != tstex); // Load player spritesheets and initialize texture assert(strlen(assetdir) + strlen(PIDLE_ASSET) < MAX_PATH_LEN); strcpy(path, 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); strcat(path, PWALK_ASSET); pwalk = IMG_LoadTexture(renderer, path); assert(NULL != pwalk); p.tex = pidle; 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; // Handle events SDL_PollEvent(&event); switch (event.type) { case SDL_QUIT: goto quit; 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; } 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; } break; default: 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); } entityupdate(&p, dt); // 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); } quit: SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }