/* * 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 32 #define VIEWWIDTH (8 * TILESIZE) #define VIEWHEIGHT (6 * TILESIZE) #define MAX_PATH_LEN 128 #define MAP_ASSET "/map.tmx" #define MAPWIDTH 112 #define MAPHEIGHT 112 #define MAPSHIFTX 48 #define MAPSHIFTY 48 #define TSASSET "/tileset.png" #define TSCOLS 56 #define PIASSET "/player/idle/down.png" #define PIWIDTH 48 #define PIHEIGHT 64 #define PIANIMLEN 8 #define WALKSPEED 72 // pixels per second #define BASEANIMPERIOD 200 typedef struct { bool left, right, up, down; } input_state_t; typedef struct { double x, y; } dvec_t; static SDL_Window *window; static SDL_Renderer *renderer; static unsigned map[MAPWIDTH][MAPHEIGHT]; static SDL_Texture *tstex, *pitex; static input_state_t input; static inline double mag(dvec_t v) { return sqrt(v.x * v.x + v.y * v.y); } 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); // Find chunk nodes in map XML xmlDocPtr doc; xmlNodePtr node; assert(strlen(argv[1]) + strlen(MAP_ASSET) < MAX_PATH_LEN); strcpy(path, argv[1]); strcat(path, MAP_ASSET); doc = xmlParseFile(path); assert(NULL != doc); node = xmlDocGetRootElement(doc); assert(0 == xmlStrcmp(node->name, (const xmlChar *)"map")); node = node->xmlChildrenNode; while (NULL != node && xmlStrcmp(node->name, (const xmlChar *)"layer") != 0) node = node->next; assert(NULL != node); node = node->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[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[x][y] = (unsigned)atoi((const char *)buf); } x = chunk_x + MAPSHIFTX; ++y; p = ++q; break; default: ++q; break; } } } // 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 idle spritesheet assert(strlen(argv[1]) + strlen(PIASSET) < MAX_PATH_LEN); strcpy(path, argv[1]); strcat(path, PIASSET); pitex = IMG_LoadTexture(renderer, path); assert(NULL != pitex); // Initialize view and player dvec_t vpos = { 512, 0 }; SDL_Rect psrc = { .y = 0, .w = PIWIDTH, .h = PIHEIGHT }; SDL_Rect pdest = { .w = SCALE * PIWIDTH, .h = SCALE * PIHEIGHT }; dvec_t pvel = { 0, 0 }, ppos = { 640, 96 }; SDL_Event event; uint64_t prevt = SDL_GetTicks64(); while (1) { // Calculate dt const uint64_t t = SDL_GetTicks64(); const uint64_t dt = t - prevt; 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 and apply velocity pvel.x = (input.left ? -1 : 0) + (input.right ? 1 : 0); pvel.y = (input.up ? -1 : 0) + (input.down ? 1 : 0); const double pspeed = mag(pvel); if (pspeed != 0) { pvel.x *= WALKSPEED / pspeed; pvel.y *= WALKSPEED / pspeed; ppos.x += (double)dt / 1000.0 * pvel.x; ppos.y += (double)dt / 1000.0 * pvel.y; } // Update view const dvec_t pvdisp = { .x = ppos.x - (vpos.x + VIEWWIDTH / 2), .y = ppos.y - (vpos.y + VIEWHEIGHT / 2), }; if (mag(pvdisp) > 72) { vpos.x += (double)dt / 1000.0 * pvel.x; vpos.y += (double)dt / 1000.0 * pvel.y; } SDL_RenderClear(renderer); // Draw map const int startx = TILESIZE * floor(vpos.x / TILESIZE); const int starty = TILESIZE * floor(vpos.y / TILESIZE); for (int y = starty; y < vpos.y + VIEWHEIGHT; y += TILESIZE) { for (int x = startx; x < vpos.x + VIEWWIDTH; x += TILESIZE) { const unsigned row = x / TILESIZE + MAPSHIFTX; const unsigned col = y / TILESIZE + MAPSHIFTY; if (row >= MAPWIDTH || col >= MAPHEIGHT) continue; const unsigned tileid = map[row][col]; 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); } } // Draw player const unsigned piframe = (t / BASEANIMPERIOD) % PANIMLEN; psrc.x = PWIDTH * piframe; pdest.x = SCALE * (int)(ppos.x - vpos.x - PWIDTH / 2); pdest.y = SCALE * (int)(ppos.y - vpos.y - PHEIGHT / 2); SDL_RenderCopy(renderer, pidle, &psrc, &pdest); SDL_RenderPresent(renderer); } quit: SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return 0; }