fangflecked

90s style ARPG
git clone git://moonbender.net/fangflecked
Log | Files | Refs | README

fangflecked.c (17873B)


      1 #define SDL_MAIN_USE_CALLBACKS 1  /* use the callbacks instead of main() */
      2 #include <SDL3/SDL.h>
      3 #include <SDL3/SDL_main.h>
      4 
      5 #include <stdio.h>
      6 
      7 #define LEVEL_MAX_WIDTH 8
      8 #define LEVEL_MAX_HEIGHT 8
      9 #define SCREEN_WIDTH 1024
     10 #define SCREEN_HEIGHT 768
     11 #define TILE_PIXEL_HEIGHT 64 
     12 #define TILE_WIDTH (TILE_PIXEL_HEIGHT * 2 * scale)
     13 #define TILE_HEIGHT (TILE_PIXEL_HEIGHT * scale)
     14 
     15 #define MAP_RENDER_OFFSET_X 0
     16 #define MAP_RENDER_OFFSET_Y SCREEN_HEIGHT / 2
     17 
     18 typedef uint8_t u8;
     19 typedef uint16_t u16;
     20 typedef uint32_t u32;
     21 typedef uint64_t u64;
     22 
     23 float scale = 1;
     24 
     25 static SDL_Window *window = NULL;
     26 static SDL_Renderer *renderer = NULL;
     27 
     28 /* assets */
     29 SDL_Texture *texture_tile, *texture_tile2, *texture_tile3;
     30 SDL_Texture *texture_warrior_stand, *texture_warrior_walk;
     31 
     32 /* pathing data */
     33 typedef struct {
     34     u32 size;
     35     u32 cur;
     36     u32 tiles[LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT];
     37 } NavPath;
     38 
     39 /* player world info */
     40 typedef struct {
     41     u32 pos;
     42     u32 pos_next;
     43     u8 angle;
     44     u8 state;   
     45     u8 frame;
     46 
     47     /* timers */
     48     u32 frame_next;
     49     u32 state_next;
     50 
     51     NavPath path;
     52 } PC;
     53 
     54 PC player;
     55 
     56 enum {
     57     PLAYER_STATE_STANDING = 0,
     58     PLAYER_STATE_WALKING
     59 };
     60 
     61 enum {
     62     ANGLE_DOWN = 0,
     63     ANGLE_DOWN_LEFT,
     64     ANGLE_LEFT,
     65     ANGLE_UP_LEFT,
     66     ANGLE_UP,
     67     ANGLE_UP_RIGHT,
     68     ANGLE_RIGHT,
     69     ANGLE_DOWN_RIGHT
     70 };
     71 
     72 /* level data */
     73 u8 level_tiles[LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT];
     74 
     75 typedef struct {
     76     float x, y;
     77 } Vecf;
     78 
     79 /*
     80  * functions for drawing and interacting with the level
     81  * ====================================================
     82  * TODO: give these better names
     83  */
     84 
     85 /* lerp between two points */
     86 Vecf vecf_lerp(Vecf p1, Vecf p2, float t) {
     87     Vecf result;
     88     
     89     result.x = p1.x * (1.0 - t) + p2.x * t;
     90     result.y = p1.y * (1.0 - t) + p2.y * t;
     91     return result;
     92 }
     93 
     94 /* Take a tile index and return level coordinates */
     95 Vecf pos_world(u32 tile) {
     96     Vecf result;
     97 
     98     result.x = SDL_floor(tile % LEVEL_MAX_WIDTH);
     99     result.y = SDL_floor(tile / LEVEL_MAX_WIDTH);
    100     return result;
    101 }
    102 
    103 /*
    104  * Return the screen position of the passed tile
    105  * so we know where to draw it. Essentially this
    106  * function does the work of converting an axis-
    107  * aligned grid into an isometric one, as well
    108  * as performing translations based on the camera
    109  * position
    110 */
    111 Vecf iso_coords(u32 tile) {
    112     Vecf world;
    113     Vecf result;
    114     u32 offs_x, offs_y;
    115 
    116     /*
    117      * We offset every graphic by half its width, to
    118      * make positioning easier
    119      */
    120     offs_x = TILE_WIDTH / 2;
    121     offs_y = TILE_HEIGHT / 2;
    122     
    123     /* get the level coordinates of the passed tile index */
    124     world = pos_world(tile);
    125 
    126     /*
    127      * Do some math to make the tiles line up along a 45
    128      * degree line, rather than horizontal
    129      */
    130     result.x = world.x * offs_x + world.y * offs_x;
    131     result.y = world.y * offs_y - world.x * offs_y;
    132 
    133     /*
    134      * Add a constant offset which serves to position
    135      * our drawing of the level correctly on screen
    136      */
    137     result.x += MAP_RENDER_OFFSET_X;
    138     result.y += MAP_RENDER_OFFSET_Y;
    139 
    140     return result;
    141 }
    142 
    143 /*
    144  * Calculates a NavPath between two points (essentially
    145  * just an array of tile indices) using linear interpolation
    146  */
    147 u8 path_line(u32 t1, u32 t2, NavPath *line) {
    148     u32 dx, dy;
    149     u32 i;
    150     u32 tx, ty;
    151     Vecf p1, p2;
    152     Vecf lerp_point;
    153 
    154     /* Error out if the start and end tiles are the same */
    155     if (t1 == t2) { return 1; }
    156 
    157     line->cur = 0;
    158 
    159     /* get level coordinates of the start and end tiles */
    160     p1 = pos_world(t1);
    161     p2 = pos_world(t2);
    162 
    163     /*
    164      * Calculate how many tiles are in the NavPath. This is 
    165      * just the diagonal distance between the start and end
    166      * tiles
    167      */
    168     dx = SDL_abs(p2.x - p1.x);
    169     dy = SDL_abs(p2.y - p1.y);
    170     line->size = dx > dy ? dx : dy;
    171 
    172     /*
    173      * We start at 1 so as not to include the start tile
    174      * in the NavPath array
    175      */
    176     for (i = 1; i <= line->size; i++) {
    177         /*
    178          * lerp along the line between the two points. Each iteration
    179          * we step up by 1 / (diagonal distance) to account for each
    180          * tile in the NavPath
    181          */
    182         lerp_point = vecf_lerp(p1, p2, (float) i / (float) line->size);
    183 
    184          /* Round to nearest integer to get the level grid coordinates */
    185         tx = SDL_round(lerp_point.x);
    186         ty = SDL_round(lerp_point.y);
    187         
    188         /*
    189          * Now convert from xy to a tile index and push
    190          * to the NavPath array
    191          */
    192         line->tiles[i-1] = ty * LEVEL_MAX_WIDTH + tx;
    193     }
    194     return 0;
    195 }
    196 
    197 /* For iterating through the array of tiles in a NavPath */
    198 u8 path_next(NavPath *path, u32 *tile) {
    199     if (path->cur >= path->size) { return 1; }
    200     *tile = path->tiles[path->cur];
    201     path->cur++;
    202     return 0;
    203 }
    204 
    205 /* Initialize or clear out the values in a NavPath */
    206 u8 path_reset(NavPath *path) {
    207     path->cur = 0;
    208     path->size = 0;
    209     return 0;
    210 }
    211 
    212 /*
    213  * Find the tile that the cursor is currently hovering over.
    214  * Essentially does the opposite of the iso_coords() function.
    215 */
    216 u8 cursor_tile(float screen_x, float screen_y, u32 *tile) {
    217     u32 result;
    218     float world_x, world_y, offs_x, offs_y;
    219 
    220     *tile = 0;
    221     
    222     /*
    223      * Sprites are always offset by their width to make positioning easier.
    224      * They're also offset by a constant amount which determines where the 
    225      * entire level is drawn in relation to the screen. Here we account for
    226      * this as a first step to determining where the screen position of the 
    227      * cursor falls in relation to the level coordinates.
    228      */
    229     offs_x = TILE_WIDTH / 2 + MAP_RENDER_OFFSET_X;
    230     offs_y = TILE_HEIGHT / 2 + MAP_RENDER_OFFSET_Y;
    231 
    232     screen_x -= offs_x;
    233     screen_y -= offs_y;
    234 
    235     /*
    236      * Taking isometric coordinates and converting back into level xy.
    237     */ 
    238     world_x = SDL_roundf(screen_x / TILE_WIDTH - screen_y / TILE_HEIGHT);
    239     world_y = SDL_roundf(screen_x / TILE_WIDTH + screen_y / TILE_HEIGHT);
    240 
    241     /* Sanity check the results */
    242     if (world_x < 0 || world_y < 0)
    243         return 1;
    244     if (world_y > LEVEL_MAX_HEIGHT || world_x >= LEVEL_MAX_WIDTH)
    245         return 1;
    246 
    247     /* Convert from level xy to a tile index */
    248     result = world_y * LEVEL_MAX_WIDTH + world_x;
    249 
    250     /* 
    251      * Another sanity check and assign the passed tile
    252      * to our result
    253      */
    254     if (result < (LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT)) {
    255         *tile = result;
    256         return 0;
    257     } else {
    258         return 1;
    259     }
    260 }
    261 
    262 /* 
    263  * Loops through all the tiles in the level_tiles array
    264  * and draws the associated graphics to the screen
    265  */
    266 void draw_level(void) {
    267     u32 i;
    268     Vecf pos;
    269     SDL_FRect dest;
    270     SDL_FRect src;
    271 
    272     dest.w = (float) 128 * scale;
    273     dest.h = (float) 64 * scale;
    274     src.w = (float) 128;
    275     src.h = (float) 64;
    276     src.x = 0;
    277     src.y = 0;
    278     
    279     /* Iterate through each tile in the level */
    280     for(i = 0; i < LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT; i++) {
    281         /*
    282          * Figure out the screen coordinates where the
    283          * tile should be drawn
    284          */
    285         pos = iso_coords(i);
    286         dest.x = pos.x;
    287         dest.y = pos.y;
    288 
    289         /*
    290          * Simple if/then block to determine which tile graphic
    291          * to draw based on the integer value in the array. This
    292          * will quickly get out of hand as we add more tile graphics,
    293          * so a better implementation will be necessary for the
    294          * next refactor.
    295          */
    296         if (level_tiles[i] == 1) {
    297             SDL_RenderTexture(renderer, texture_tile, &src, &dest);
    298         } else if (level_tiles[i] == 2) {
    299             SDL_RenderTexture(renderer, texture_tile2, &src, &dest);
    300         } else if (level_tiles[i] == 3) {
    301             SDL_RenderTexture(renderer, texture_tile3, &src, &dest);
    302         }
    303     }
    304 }
    305 
    306 /* 
    307  * Player drawing and logic
    308  * =====================================================
    309  */
    310 
    311 /*
    312  * check if player can move to the passed tile,
    313  * then handle the movement
    314  */
    315 u8 player_move(u32 tile) {
    316     u32 target_x, target_y, current_x, current_y;
    317 
    318     /* No need to move to the tile we're already standing on */
    319     if (tile == player.pos) { return 1; } 
    320 
    321     /* Don't interrupt if player is already between tiles */
    322     if (player.pos != player.pos_next) { return 1; } 
    323 
    324     /*
    325      * Convert tile index to x and y coordinates
    326      * target is the tile the player wants to move to
    327      * current is the tile the player is currently standing on
    328      */
    329     target_x = tile % LEVEL_MAX_WIDTH;
    330     target_y = tile / LEVEL_MAX_WIDTH;
    331     current_x = player.pos % LEVEL_MAX_WIDTH;
    332     current_y = player.pos / LEVEL_MAX_WIDTH;
    333     
    334     /*
    335      * Make sure the target tile is adjacent to the one the player's standing on.
    336      * if so, update the player's angle and continue.
    337      */
    338 
    339     /* axis-aligned movement (relative to the screen) is kitty corner according to the world coordinates */
    340     if (target_x - current_x == 1 && target_y - current_y == 1) {
    341         player.angle = ANGLE_RIGHT;
    342     } else if (target_x - current_x == 1 && target_y - current_y == -1) {
    343         player.angle = ANGLE_UP;
    344     } else if (target_x - current_x == -1 && target_y - current_y == -1) {
    345         player.angle = ANGLE_LEFT;
    346     } else if (target_x - current_x == -1 && target_y - current_y == 1) {
    347         player.angle = ANGLE_DOWN;
    348     /* diagonal (relative to the screen) is along the grid in world coordinates */
    349     } else if (target_x - current_x == 1 && target_y - current_y == 0) {
    350         player.angle = ANGLE_UP_RIGHT;
    351     } else if (target_x - current_x == 0 && target_y - current_y == -1) {
    352         player.angle = ANGLE_UP_LEFT;
    353     } else if (target_x - current_x == -1 && target_y - current_y == 0) {
    354         player.angle = ANGLE_DOWN_LEFT;
    355     } else if (target_x - current_x == 0 && target_y - current_y == 1) {
    356         player.angle = ANGLE_DOWN_RIGHT;
    357     } else {
    358         /* tile not adjacent */
    359         return 1;
    360     }
    361 
    362     /*
    363      * Set the player to walking state and reset animation variables
    364      * in preparation for playing the player's walk animation.
    365     */
    366     player.pos_next = tile;
    367     player.state = PLAYER_STATE_WALKING;
    368     player.frame = 0;
    369     player.state_next = SDL_GetTicks() + 400;
    370     player.frame_next = SDL_GetTicks() + 50;
    371     return 0;
    372 }
    373 
    374 /*
    375  * Use lerp to calculate a NavPath between player's current position
    376  * and destination tile
    377 */
    378 u8 player_path(u32 tile) {
    379     /* adjacent tile, no pathing needed */
    380     if (player_move(tile) == 0) { return 0; }
    381 
    382     if (path_line(player.pos_next, tile, &player.path) == 0) {
    383         return 0;
    384     }
    385     return 1;
    386 }
    387 
    388 /*
    389  * Draws the player sprite, as well as handling animations
    390  * and state changes. This function is a mess, and should
    391  * probably be divided up. I'll try to comment it on the 
    392  * next refactor.
    393  */
    394 u8 draw_player() {
    395     SDL_FRect dest;
    396     SDL_FRect src;
    397     Vecf offs;
    398     u32 tile_next;
    399 
    400     src.y = (float) player.angle * 96;
    401     src.x = (float) player.frame * 96;
    402     src.w = (float) 96;
    403     src.h = (float) 96;
    404 
    405     dest.w = (float) 160 * scale;
    406     dest.h = (float) 160 * scale;
    407 
    408     if (SDL_GetTicks() >= player.frame_next) {
    409         player.frame++;
    410         player.frame_next = SDL_GetTicks() + 50;
    411 
    412         if (player.state == PLAYER_STATE_STANDING) { /* standing anim has 10 frames */
    413             if (player.frame > 9) { player.frame = 0; }
    414         } else if (player.state == PLAYER_STATE_WALKING) {
    415             if (player.frame > 7) { player.frame = 0; }
    416         }
    417     }
    418     
    419     /*
    420      * logic for switching states and moving tiles
    421      * mixed in with animation code
    422      * TODO: definitely want to untangle this
    423      */
    424 
    425     if (player.state == PLAYER_STATE_WALKING) {
    426         if (SDL_GetTicks() >= player.state_next) {
    427             player.pos = player.pos_next;
    428             offs = iso_coords(player.pos);
    429             
    430             /*
    431              * check if there's another tile left in the pathing queue
    432              * if not, switch state back to standing
    433              */
    434 
    435             if (path_next(&player.path, &tile_next) == 0) {
    436                 player_move(tile_next);
    437             } else {
    438                 player.frame = 0;
    439                 player.frame_next = SDL_GetTicks() + 50;
    440                 player.state = PLAYER_STATE_STANDING;
    441                 path_reset(&player.path);
    442             }
    443         } else {
    444             /* lerp to figure out where to draw the player sprite as they walk between tiles */
    445             offs = vecf_lerp(iso_coords(player.pos_next), iso_coords(player.pos), (float) (player.state_next - SDL_GetTicks()) / 400);
    446         }
    447     } else if (player.state == PLAYER_STATE_STANDING) {
    448         if (path_next(&player.path, &tile_next) == 0) {
    449             player_move(tile_next);
    450         }
    451         offs = iso_coords(player.pos);
    452     }
    453 
    454     dest.x = offs.x;
    455     dest.y = offs.y - dest.h + TILE_HEIGHT;
    456 
    457     if (player.state == PLAYER_STATE_WALKING) {
    458         SDL_RenderTextureRotated(renderer, texture_warrior_walk, &src , &dest, 0, NULL, SDL_FLIP_NONE);
    459     } else if (player.state == PLAYER_STATE_STANDING) {
    460         SDL_RenderTextureRotated(renderer, texture_warrior_stand, &src , &dest, 0, NULL, SDL_FLIP_NONE);
    461     }
    462     return 0;
    463 }
    464 
    465 /*
    466  * SDL Callbacks
    467  * ========================================================
    468  */
    469 
    470 /* Called at launch */
    471 SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
    472     SDL_Surface *buffer;
    473     u8 i;
    474     
    475     for (i = 0; i < LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT; i++) {
    476         /* level_tiles[i] = i % 2 + 1; */
    477         level_tiles[i] = 1;
    478     }
    479 
    480 
    481     SDL_Init(SDL_INIT_EVENTS | SDL_INIT_VIDEO);
    482     if (!SDL_CreateWindowAndRenderer("FANGFLECKED", SCREEN_WIDTH, SCREEN_HEIGHT, 0, &window, &renderer)) {
    483         SDL_Log("Couldn't create window and renderer: %s", SDL_GetError());
    484         return SDL_APP_FAILURE;
    485     }
    486 
    487     /*
    488      * Load graphics from file, set color FF00FF (bright pink)
    489      * to transparent, convert/assign to an SDL_Texture object
    490      */
    491     buffer = SDL_LoadBMP("assets/ph_tile.bmp");
    492     SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
    493     texture_tile = SDL_CreateTextureFromSurface(renderer, buffer);
    494     buffer = SDL_LoadBMP("assets/ph_tile2.bmp");
    495     SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
    496     texture_tile2 = SDL_CreateTextureFromSurface(renderer, buffer);
    497     buffer = SDL_LoadBMP("assets/ph_tile3.bmp");
    498     SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
    499     texture_tile3 = SDL_CreateTextureFromSurface(renderer, buffer);
    500     buffer = SDL_LoadBMP("assets/d1warriorstand.bmp");
    501     SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
    502     texture_warrior_stand = SDL_CreateTextureFromSurface(renderer, buffer);
    503     buffer = SDL_LoadBMP("assets/d1warriorwalk.bmp");
    504     SDL_SetSurfaceColorKey(buffer, true, SDL_MapRGB(SDL_GetPixelFormatDetails(buffer->format), NULL, 0xFF, 0x00, 0xFF));
    505     texture_warrior_walk = SDL_CreateTextureFromSurface(renderer, buffer);
    506     SDL_DestroySurface(buffer);
    507 
    508     /*
    509      * Initialize player position and angle
    510      * to arbitrary values and player state
    511      * to standing/ready (as opposed to moving
    512      * between tiles or performing some other
    513      * action)
    514      */
    515     player.pos = 17;
    516     player.pos_next = 17;
    517     player.angle = ANGLE_DOWN;
    518     player.frame_next = 0;
    519     player.frame = 0;
    520     player.state = PLAYER_STATE_STANDING;
    521 
    522     return SDL_APP_CONTINUE;
    523 }
    524 
    525 /* Called when an input event is received */
    526 SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
    527     if (event->type == SDL_EVENT_QUIT) {
    528         return SDL_APP_SUCCESS;
    529     }
    530     return SDL_APP_CONTINUE;
    531 }
    532 
    533 /* Called every frame */
    534 SDL_AppResult SDL_AppIterate(void *appstate) {
    535     SDL_MouseButtonFlags mouse_state;
    536     float mouse_x, mouse_y;
    537     u32 hovertile;
    538     u32 i;
    539 
    540     /* key_state = SDL_GetKeyboardState(NULL); */
    541 
    542     /*
    543      * Initialize the level data array into a flat room made
    544      * up of pathable tiles
    545      */
    546     for (i = 0; i < LEVEL_MAX_WIDTH * LEVEL_MAX_HEIGHT; i++) {
    547         level_tiles[i] = 1;
    548     }
    549 
    550     /* Get mourse coordinates relative to the game screen */
    551     mouse_state = SDL_GetMouseState(&mouse_x, &mouse_y);
    552 
    553     /*
    554      * Set draw color to black and clear the screen so we have
    555      * a blank canvas on which we can draw our next frame
    556      */
    557     SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
    558     SDL_RenderClear(renderer);
    559 
    560     /*
    561     if (key_state[SDL_SCANCODE_UP]) { posy--; }
    562     if (key_state[SDL_SCANCODE_LEFT]) { posx--; }
    563     if (key_state[SDL_SCANCODE_DOWN]) { posy++; }
    564     if (key_state[SDL_SCANCODE_RIGHT]) { posx++; }
    565     */
    566 
    567     /*
    568      * On left click, find the tile the player has clicked on
    569      * and try to find a NavPath from the player's current
    570      * position to the clicked tile
    571      */
    572     if (mouse_state & SDL_BUTTON_LEFT) {
    573         if (cursor_tile(mouse_x, mouse_y, &hovertile) == 0) {
    574             player_path(hovertile);
    575         }
    576     }
    577 
    578     /*
    579      * Quick and dirty debug code to change the data
    580      * for the tiles in the player's current NavPath.
    581      * so we can draw them in distinct colors.
    582      */
    583     for (i = 0; i < player.path.size; i++) {
    584         level_tiles[player.path.tiles[i]] = 2;
    585     }
    586     level_tiles[player.pos_next] = 3;
    587 
    588     /* Draw current frame */
    589     draw_level();
    590     draw_player();
    591 
    592 
    593     /* Tell SDL to present the frame we've just drawn */
    594     SDL_RenderPresent(renderer);
    595     return SDL_APP_CONTINUE;
    596 }
    597 
    598 /* Cleanup on game quit */
    599 void SDL_AppQuit(void *appstate, SDL_AppResult result) {
    600     SDL_DestroyTexture(texture_tile);
    601     SDL_DestroyTexture(texture_tile2);
    602     SDL_DestroyTexture(texture_tile3);
    603     SDL_DestroyTexture(texture_warrior_stand);
    604     SDL_DestroyTexture(texture_warrior_walk);
    605 }