/* * 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 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; 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 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 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; for (int l = 0; l < MAPNLAYERS; ++l) { do node = node->next; while (NULL != node && xmlStrcmp(node->name, (const xmlChar *)"layer") != 0); assert(NULL != node); maploadlayer(node, l); } } 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 = SCALE * (x - vpos.x), .y = 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) 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 = SCALE * (int)(e->pos.x - vpos.x - e->src.w / 2), .y = SCALE * (int)(e->pos.y - vpos.y - e->src.h / 2), .w = SCALE * e->src.w, .h = SCALE * e->src.h, }; SDL_RenderCopy(renderer, e->tex, &e->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; } // 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(argv[1]) + strlen(MAP_ASSET) < MAX_PATH_LEN); strcpy(path, argv[1]); strcat(path, MAP_ASSET); mapload(path); // Load tileset assert(strlen(argv[1]) + strlen(TSASSET) < MAX_PATH_LEN); strcpy(path, argv[1]); strcat(path, TSASSET); tstex = IMG_LoadTexture(renderer, path); assert(NULL != tstex); // Load player spritesheets assert(strlen(argv[1]) + strlen(PIDLE_ASSET) < MAX_PATH_LEN); strcpy(path, argv[1]); strcat(path, PIDLE_ASSET); pidle = IMG_LoadTexture(renderer, path); assert(NULL != pidle); assert(strlen(argv[1]) + strlen(PWALK_ASSET) < MAX_PATH_LEN); strcpy(path, argv[1]); strcat(path, PWALK_ASSET); pwalk = IMG_LoadTexture(renderer, path); assert(NULL != pwalk); // Initialize player entity_t p = { .svar = SPRITE_DIR_DOWN, .animlen = PANIMLEN, .tex = pidle, .animstep = { .x = PWIDTH }, .svarstep = { .y = PHEIGHT }, .fbox = { .off = { .x = PFBOFFX, .y = PFBOFFY }, .ext = { .x = PFBWIDTH, .y = PFBHEIGHT }, }, .src = { .w = PWIDTH, .h = PHEIGHT }, }; 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; } 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; } } SDL_RenderClear(renderer); mapdraw(); entitydraw(&p, t); SDL_RenderPresent(renderer); } quit: SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }