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 }