a-game

2D platformer written from scratch.
git clone git://git.amin.space/a-game.git
Log | Files | Refs | README | LICENSE

game.c (26348B)


      1 #include "game.h"
      2 
      3 #if !defined(PLATFORM_WASM)
      4 #include "am_gl.c"
      5 #endif
      6 
      7 #include "shader.c"
      8 #include "memory.c"
      9 #include "collision.c"
     10 #include "render.c"
     11 #include "world.c"
     12 #include "input.c"
     13 #include "image.c"
     14 
     15 #include "w_tutorial.h"
     16 
     17 internal inline bool timer_is_expired(struct Timer t)
     18 {
     19     return t.elapsed_ms >= t.limit_ms;
     20 }
     21 
     22 internal inline void timer_init(struct Timer *t, i64 limit_ms)
     23 {
     24     *t = (struct Timer) {
     25         .elapsed_ms = 0,
     26         .limit_ms = limit_ms,
     27     };
     28 }
     29 
     30 internal inline void timer_update(struct Timer *t, i64 elapsed_ms)
     31 {
     32     t->elapsed_ms += elapsed_ms;
     33 }
     34 
     35 internal void move_mode_print(enum MoveMode s)
     36 {
     37     switch(s)
     38     {
     39         case MOVE_MODE_FALLING:
     40             printf("FALLING\n");
     41             break;
     42         case MOVE_MODE_GROUNDED:
     43             printf("GROUNDED\n");
     44             break;
     45         case MOVE_MODE_JUMPING:
     46             printf("JUMPING\n");
     47             break;
     48         case MOVE_MODE_CLIMBING:
     49             printf("CLIMBING\n");
     50             break;
     51         case MOVE_MODE_FLOATING:
     52             printf("FLOATING\n");
     53             break;
     54         default:
     55             printf("INVALID\n");
     56             break;
     57     }
     58 }
     59 
     60 internal bool entity_is_adjacent_to_solid_tiles(struct World *world, struct AbsolutePos entity_p, rect entity_rect, enum Direction dir)
     61 {
     62     v2 tile_p_to_search_from_min = {0};
     63     v2 tile_p_to_search_from_max = {0};
     64     switch(dir)
     65     {
     66         case DIR_RIGHT:
     67             tile_p_to_search_from_min = (v2) {math_ceil(entity_rect.max.x), math_floor(entity_rect.min.y)};
     68             tile_p_to_search_from_max = (v2) {math_ceil(entity_rect.max.x), math_floor(entity_rect.max.y)};
     69             break;
     70         case DIR_LEFT:
     71             tile_p_to_search_from_min = (v2) {math_floor(entity_rect.min.x - 1.0f), math_floor(entity_rect.min.y)};
     72             tile_p_to_search_from_max = (v2) {math_floor(entity_rect.min.x - 1.0f), math_floor(entity_rect.max.y)};
     73             break;
     74         case DIR_UP:
     75             tile_p_to_search_from_min = (v2) {math_floor(entity_rect.min.x), math_ceil(entity_rect.max.y)};
     76             tile_p_to_search_from_max = (v2) {math_floor(entity_rect.max.x), math_ceil(entity_rect.max.y)};
     77             break;
     78         case DIR_DOWN:
     79             tile_p_to_search_from_min = (v2) {math_floor(entity_rect.min.x), math_floor(entity_rect.max.y - 1.0f)};
     80             tile_p_to_search_from_max = (v2) {math_floor(entity_rect.max.x), math_floor(entity_rect.min.y - 1.0f)};
     81             break;
     82         default:
     83             assert(false);
     84             break;
     85     }
     86 
     87     bool result = false;
     88     if (world_tile_is_solid(world, (struct AbsolutePos) {.room = entity_p.room, .local = tile_p_to_search_from_min})
     89         || world_tile_is_solid(world, (struct AbsolutePos) {.room = entity_p.room, .local = tile_p_to_search_from_max}))
     90     {
     91         result = true;
     92     }
     93     return result;
     94 }
     95 
     96 internal void game_init(struct GameMemory *game_memory, v2u framebuffer)
     97 {
     98     assert(sizeof(struct GameState) <= game_memory->buffer_size);
     99     struct GameState *game_state = (struct GameState *)game_memory->buffer;
    100 
    101     // init player
    102 
    103     // In Knytt, the player is 9 by 14 texels and a tile is 24 by 24 texels.
    104     // These dimensions are relative to a square 'meter', one tile
    105     game_state->player.dimensions = (v2) {0.375f, 0.583f};
    106     game_state->player.move_mode = MOVE_MODE_FALLING;
    107     game_state->player.facing = DIR_RIGHT;
    108 
    109     // TODO: make player spawn location part of the world data
    110     game_state->player.pos = (struct AbsolutePos) {
    111         .room = {0, 0},
    112         // TODO: Handle player wall spawning properly (extrude them)
    113         .local = {6.0f, 6.0f + (game_state->player.dimensions.y * 0.5f)},
    114     };
    115 
    116     size_t temp_memory_size = MEBIBYTES(2);
    117     size_t world_memory_size = game_memory->buffer_size - (sizeof(struct GameState) + temp_memory_size);
    118     mem_st_init(
    119         &game_state->temp_allocator,
    120         game_memory->buffer + sizeof(struct GameState),
    121         temp_memory_size);
    122     mem_st_init(
    123         &game_state->world_allocator,
    124         game_state->temp_allocator.base + temp_memory_size,
    125         world_memory_size);
    126 
    127     game_state->world = mem_st_alloc_struct(&game_state->world_allocator, struct World);
    128     game_state->world->num_rooms = 0;
    129 
    130     for (u32 i = 0; i < W_TUTORIAL_NUM_ROOMS; i++)
    131     {
    132         world_room_set(game_state->world, w_tutorial_rooms[i], &game_state->world_allocator);
    133     }
    134 
    135     size_t asset_loading_free_marker = mem_st_get_marker(&game_state->temp_allocator);
    136     struct PlatformApi platform = game_memory->platform;
    137     // game_shader_load
    138     {
    139         size_t temp_free_marker = mem_st_get_marker(&game_state->temp_allocator);
    140         char *v_source = (char *)platform.platform_read_entire_file("shader/main_v.glsl", NULL, &game_state->temp_allocator);
    141         char *f_source = (char *)platform.platform_read_entire_file("shader/main_f.glsl", NULL, &game_state->temp_allocator);
    142         struct Shader main_shader = {0};
    143         if (!shader_compile(v_source, f_source, &main_shader))
    144         {
    145             exit(1);
    146             // TODO: handle error
    147         }
    148         game_state->renderer.shader = main_shader;
    149         mem_st_free_to_marker(&game_state->temp_allocator, temp_free_marker);
    150     }
    151 
    152     struct Image img = {0};
    153     // game_load_images
    154     {
    155         size_t temp_free_marker = mem_st_get_marker(&game_state->temp_allocator);
    156         size_t file_len = 0;
    157         u8 *t = platform.platform_read_entire_file("assets/tileset0.tga", &file_len, &game_state->temp_allocator);
    158         assert(t);
    159 
    160         i32 img_w = 0;
    161         i32 img_h = 0;
    162         u8 *data = img_load_from_memory(t, file_len, &img_w, &img_h, &game_state->temp_allocator);
    163         mem_st_free_to_marker(&game_state->temp_allocator, temp_free_marker);
    164         assert(data);
    165 
    166         img.data = data;
    167         img.dim = (v2u) {img_w, img_h};
    168     }
    169 
    170     renderer_init(&game_state->renderer, &img, 1, &game_state->world_allocator);
    171     mem_st_free_to_marker(&game_state->temp_allocator, asset_loading_free_marker);
    172 }
    173 
    174 // NOTE(amin): For now updating and rendering are interleaved. We simulate the
    175 // world at the same rate we render it. Our timestep is not fixed. We may want
    176 // to change this in the future.
    177 void game_update_and_render(struct GameMemory *game_memory, struct GameInput *game_input, v2u framebuffer)
    178 {
    179     assert(sizeof(struct GameState) <= game_memory->buffer_size);
    180     struct GameState *game_state = (struct GameState *)game_memory->buffer;
    181     f32 dt = game_input->dt;
    182 
    183     // TODO: pass total elapsed ms in as an i64. Use that for the timer.
    184     timer_update(&game_state->jump_allowance_timer, (i64)(dt * 1000.0f));
    185 
    186     v2i current_room_i = game_state->player.pos.room;
    187     struct Room *current_room = world_room_get(game_state->world, current_room_i);
    188 
    189     assert(current_room);
    190     assert(math_v2i_eq(current_room_i, current_room->index));
    191 
    192     // game_update_input
    193     {
    194         u32 button_changes = game_input->prev_button_states ^ game_input->button_states;
    195         game_input->button_ups = button_changes & (~game_input->button_states);
    196         game_input->button_downs = button_changes & game_input->button_states;
    197         game_input->prev_button_states = game_input->button_states;
    198     }
    199 
    200     // game_update_player
    201     {
    202         struct Entity *player = &game_state->player;
    203         u32 button_states = game_input->button_states;
    204         if (dt >= 0.5f)
    205         {
    206             // NOTE(amin): If our dt is 0.5 seconds, we've probably been
    207             // debugging and sitting at a breakpoint, and should just update as
    208             // if we've been going at a steady 60fps.
    209             printf("WARNING: Clamping frame dt to 1/60\n");
    210             dt = 1.0f / 60.0f;
    211         }
    212 
    213         f32 max_run_speed = 6.0f;
    214         f32 max_climb_speed = 7.0f;
    215         f32 max_fall_speed = 9.0f;
    216 
    217         f32 acceleration_rate = 50.0f;
    218         f32 gravity = 30.0f;
    219         f32 friction = 0.7f;
    220         enum Direction previous_facing_dir = player->facing;
    221 
    222         if (btn_is_down(button_states, BTN_LEFT))
    223         {
    224             player->facing = DIR_LEFT;
    225             player->acceleration.x = -acceleration_rate;
    226         }
    227         else if (btn_is_down(button_states, BTN_RIGHT))
    228         {
    229             player->facing = DIR_RIGHT;
    230             player->acceleration.x = acceleration_rate;
    231         }
    232         else
    233         {
    234             player->acceleration.x = 0.0f;
    235             player->velocity.x = player->velocity.x * friction;
    236         }
    237 
    238         if (btn_was_just_pressed(game_input, BTN_DEBUG_FLOAT))
    239         {
    240             if (player->move_mode == MOVE_MODE_FLOATING)
    241             {
    242                 player->move_mode = MOVE_MODE_FALLING;
    243             }
    244             else
    245             {
    246                 player->move_mode = MOVE_MODE_FLOATING;
    247             }
    248         }
    249 
    250         rect player_rect = math_rect_from_center_dim(player->pos.local, player->dimensions);
    251 
    252         // determine this frame's movement mode
    253         switch(player->move_mode)
    254         {
    255             case MOVE_MODE_FALLING:
    256                 if (!timer_is_expired(game_state->jump_allowance_timer))
    257                 {
    258                     if (btn_was_just_pressed(game_input, BTN_JUMP))
    259                     {
    260                         player->move_mode = MOVE_MODE_JUMPING;
    261                     }
    262                 }
    263                 break;
    264             case MOVE_MODE_GROUNDED:
    265                 if(entity_is_adjacent_to_solid_tiles(game_state->world, player->pos, player_rect, DIR_DOWN))
    266                 {
    267                     if (btn_was_just_pressed(game_input, BTN_JUMP))
    268                     {
    269                         player->move_mode = MOVE_MODE_JUMPING;
    270                     }
    271                 }
    272                 else
    273                 {
    274                     timer_init(&game_state->jump_allowance_timer, JUMP_ALLOWANCE_MS);
    275                     player->move_mode = MOVE_MODE_FALLING;
    276                 }
    277                 break;
    278             case MOVE_MODE_CLIMBING:
    279                 if (player->facing == previous_facing_dir)
    280                 {
    281                     if (entity_is_adjacent_to_solid_tiles(game_state->world, player->pos, player_rect, player->facing))
    282                     {
    283                         if (btn_was_just_pressed(game_input, BTN_JUMP))
    284                         {
    285                             // nudge the player away from the wall
    286                             if (player->facing == DIR_RIGHT)
    287                             {
    288                                 player->velocity.x = -3.0f;
    289                             }
    290                             else if (player->facing == DIR_LEFT)
    291                             {
    292                                 player->velocity.x = 3.0f;
    293                             }
    294                             player->move_mode = MOVE_MODE_JUMPING;
    295                         }
    296                     }
    297                     else
    298                     {
    299                         if (player->velocity.y > 0.0f)
    300                         {
    301                             // nudge the player over the ledge
    302                             if (player->facing == DIR_RIGHT)
    303                             {
    304                                 player->velocity.x = 3.0f;
    305                             }
    306                             else if (player->facing == DIR_LEFT)
    307                             {
    308                                 player->velocity.x = -3.0f;
    309                             }
    310                             player->acceleration.y = 0.0f;
    311                             player->velocity.y = 0.0f;
    312                         }
    313                         player->move_mode = MOVE_MODE_FALLING;
    314                     }
    315                 }
    316                 else
    317                 {
    318                     if (btn_was_just_pressed(game_input, BTN_JUMP))
    319                     {
    320                         // nudge the player away from the wall
    321                         if (player->facing == DIR_RIGHT)
    322                         {
    323                             player->velocity.x = 3.0f;
    324                         }
    325                         else if (player->facing == DIR_LEFT)
    326                         {
    327                             player->velocity.x = -3.0f;
    328                         }
    329                         player->move_mode = MOVE_MODE_JUMPING;
    330                     }
    331                     else
    332                     {
    333                         player->acceleration.y = 0.0f;
    334                         player->velocity.y = 0.0f;
    335                         timer_init(&game_state->jump_allowance_timer, JUMP_ALLOWANCE_MS);
    336                         player->move_mode = MOVE_MODE_FALLING;
    337                     }
    338                 }
    339                 break;
    340             case MOVE_MODE_JUMPING:
    341                 if (btn_was_just_released(game_input, BTN_JUMP))
    342                 {
    343                     player->move_mode = MOVE_MODE_FALLING;
    344                 }
    345                 break;
    346             case MOVE_MODE_FLOATING:
    347                 break;
    348             default:
    349                 assert(false);
    350                 break;
    351         }
    352 
    353         //move_mode_print(player->move_mode);
    354 
    355         // simulate movement mode
    356         switch(player->move_mode)
    357         {
    358             case MOVE_MODE_FALLING:
    359                 player->acceleration.y = -gravity;
    360                 break;
    361             case MOVE_MODE_GROUNDED:
    362                 player->acceleration.y = 0.0f;
    363                 player->velocity.y = 0.0f;
    364                 break;
    365             case MOVE_MODE_CLIMBING:
    366                 if (btn_is_down(button_states, BTN_DOWN))
    367                 {
    368                     player->acceleration.y = -acceleration_rate;
    369                 }
    370                 else
    371                 {
    372                     if (player->velocity.y < 0.0f)
    373                     {
    374                         player->acceleration.y = 0.0f;
    375                         player->velocity.y = 0.0f;
    376                     }
    377 
    378                     if (btn_is_down(button_states, BTN_UP))
    379                     {
    380                         player->acceleration.y = acceleration_rate;
    381                     }
    382                     else
    383                     {
    384                         player->acceleration.y = 0.0f;
    385                         player->velocity.y = -1.0f;
    386                     }
    387                 }
    388                 break;
    389             case MOVE_MODE_JUMPING:
    390             {
    391                 f32 jump_height = 1.0f;
    392                 // v^2  = v0^2 - 2g(y - y0)
    393                 // 0    = v0^2 - 2g(h - 0)
    394                 // 0    = v0^2 - 2gh
    395                 // v0^2 = 2gh
    396                 // v0   = sqrt(2gh)
    397                 player->velocity.y = sqrtf(2.0f * gravity * jump_height);
    398                 player->move_mode = MOVE_MODE_FALLING;
    399                 break;
    400             }
    401             case MOVE_MODE_FLOATING:
    402                 if (btn_is_down(button_states, BTN_UP))
    403                 {
    404                     player->acceleration.y = acceleration_rate;
    405                 }
    406                 else if (btn_is_down(button_states, BTN_DOWN))
    407                 {
    408                     player->acceleration.y = -acceleration_rate;
    409                 }
    410                 else
    411                 {
    412                     player->acceleration.y = 0.0f;
    413                     player->velocity.y = player->velocity.y * friction;
    414                 }
    415                 break;
    416             default:
    417                 assert(false);
    418                 break;
    419         }
    420 
    421         // Semi implicit Euler integration: https://gafferongames.com/post/integration_basics/
    422         player->velocity = math_v2_a(player->velocity, math_v2f_m(player->acceleration, dt));
    423         // TODO: clamp the length of the velocity vector, not each of its components
    424         math_clamp(&player->velocity.x, -max_run_speed, max_run_speed);
    425         switch(player->move_mode)
    426         {
    427             case MOVE_MODE_CLIMBING:
    428                 math_clamp(&player->velocity.y, -max_fall_speed, max_climb_speed);
    429                 break;
    430             case MOVE_MODE_FALLING:
    431                 if (player->velocity.y < -max_fall_speed)
    432                 {
    433                     player->velocity.y = -max_fall_speed;
    434                 }
    435                 break;
    436             case MOVE_MODE_FLOATING:
    437                 math_clamp(&player->velocity.y, -max_run_speed, max_run_speed);
    438                 break;
    439             default:
    440                 break;
    441         }
    442 
    443         //printf("v: ");
    444         //math_print(player->velocity);
    445         //printf("a: ");
    446         //math_print(player->acceleration);
    447 
    448         // game_detect_collisions
    449         {
    450 #if 0
    451 #define RENDER_COLLISION_DEBUG_QUAD(r, c) renderer_debug_quad_draw(&game_state->renderer, (r), (c));
    452 #else
    453 #define RENDER_COLLISION_DEBUG_QUAD(r, c)
    454 #endif
    455             v2 player_delta = math_v2f_m(player->velocity, dt);
    456             v2 new_p = math_v2_a(player->pos.local, player_delta);
    457 
    458             rect player_traversal_bb = {
    459                 .min = {math_min(player->pos.local.x, new_p.x), math_min(player->pos.local.y, new_p.y)},
    460                 .max = {math_max(player->pos.local.x, new_p.x), math_max(player->pos.local.y, new_p.y)},
    461             };
    462 
    463             rect player_traversal_occupancy_bb = math_minkowski_sum_rect(player_traversal_bb, player->dimensions);
    464 
    465             rect tile_search_range = {
    466                 .min = {
    467                     math_floor(player_traversal_occupancy_bb.min.x - 1),
    468                     math_floor(player_traversal_occupancy_bb.min.y - 1),
    469                 },
    470                 .max = {
    471                     math_floor(player_traversal_occupancy_bb.max.x + 2),
    472                     math_floor(player_traversal_occupancy_bb.max.y + 2),
    473                 },
    474             };
    475 
    476             RENDER_COLLISION_DEBUG_QUAD(tile_search_range, ((v3) {0.8f, 0.8f, 0.8f}));
    477 
    478             f32 remaining_time_factor = 1.0f;
    479             for (u32 i = 0; i < 4 && remaining_time_factor > 0.0f; i++)
    480             {
    481                 v2 wall_normal = {0};
    482                 f32 smallest_distance_scale_factor = 1.0f;
    483                 bool collision_occurred = false;
    484                 bool edges_collided[MAX_RECT_EDGE] = {0};
    485                 for (i32 tile_y = tile_search_range.min.y; tile_y < tile_search_range.max.y; tile_y++)
    486                 {
    487                     for (i32 tile_x = tile_search_range.min.x; tile_x < tile_search_range.max.x; tile_x++)
    488                     {
    489                         struct AbsolutePos tile_pos = world_tile_pos_recanonicalize(
    490                             (struct AbsolutePos) {
    491                                 .room = current_room_i,
    492                                 .local = (v2) {tile_x, tile_y},
    493                             });
    494 
    495                         if (world_tile_is_solid(game_state->world, tile_pos))
    496                         {
    497                             rect tile_aabb = {
    498                                 .min = {tile_x, tile_y},
    499                                 .max = {tile_x + TILE_SIZE, tile_y + TILE_SIZE},
    500                             };
    501                             RENDER_COLLISION_DEBUG_QUAD(tile_aabb, ((v3) {0.8f, 0.4f, 0.4f}));
    502 
    503                             rect tile_player_sum = math_minkowski_sum_rect(tile_aabb, player->dimensions);
    504 
    505                             for (enum MathRectEdge edge = 0; edge < MAX_RECT_EDGE; edge++)
    506                             {
    507                                 segment player_sum_wall = math_rect_get_edge(tile_player_sum, edge);
    508                                 v2 normal = math_rect_get_normal(edge);
    509                                 segment tile_wall = math_rect_get_edge(tile_aabb, edge);
    510 
    511                                 if (!wall_is_internal(game_state->world, current_room_i, tile_wall))
    512                                 {
    513                                     struct WallCollision c = get_wall_collision(player->pos.local, new_p, player_sum_wall, normal);
    514                                     if (c.collision_occurred)
    515                                     {
    516                                         collision_occurred = true;
    517                                         edges_collided[edge] = true;
    518                                         if (smallest_distance_scale_factor > c.distance_scale_factor)
    519                                         {
    520                                             smallest_distance_scale_factor = c.distance_scale_factor;
    521                                         }
    522                                         wall_normal = normal;
    523                                         RENDER_COLLISION_DEBUG_QUAD(
    524                                             ((rect) {
    525                                                 .min = tile_wall.min,
    526                                                 .max = math_v2_a(tile_wall.max, math_v2f_m(normal, -0.2f)),
    527                                             }),
    528                                             ((v3) {0.0f, 0.8f, 0.0f}));
    529                                     }
    530                                 }
    531                             }
    532                         }
    533                     }
    534                 }
    535 
    536                 if (collision_occurred && player->move_mode != MOVE_MODE_FLOATING)
    537                 {
    538                     if (edges_collided[RECT_EDGE_TOP] && player->move_mode == MOVE_MODE_FALLING)
    539                     {
    540                         player->move_mode = MOVE_MODE_GROUNDED;
    541                     }
    542                     else if (edges_collided[RECT_EDGE_LEFT] || edges_collided[RECT_EDGE_RIGHT])
    543                     {
    544                         player->move_mode = MOVE_MODE_CLIMBING;
    545                     }
    546                 }
    547 
    548                 v2 delta_to_nearest_collision = math_v2f_m(player_delta, smallest_distance_scale_factor);
    549                 v2 nearest_collision_p = math_v2_a(player->pos.local, delta_to_nearest_collision);
    550 
    551                 f32 collision_epsilon = 0.0001f;
    552                 rect local_inhabitable_region = {
    553                     .min = {
    554                         math_min(player->pos.local.x, nearest_collision_p.x + collision_epsilon),
    555                         math_min(player->pos.local.y, nearest_collision_p.y + collision_epsilon),
    556                     },
    557                     .max = {
    558                         math_max(player->pos.local.x, nearest_collision_p.x - collision_epsilon),
    559                         math_max(player->pos.local.y, nearest_collision_p.y - collision_epsilon),
    560                     },
    561                 };
    562 
    563                 player->pos.local = nearest_collision_p;
    564                 math_clamp(&player->pos.local.x, local_inhabitable_region.min.x, local_inhabitable_region.max.x);
    565                 math_clamp(&player->pos.local.y, local_inhabitable_region.min.y, local_inhabitable_region.max.y);
    566 
    567                 // NOTE(amin):
    568                 //     n * dot(v, n)
    569                 //     = n * (|v| * |n| * cos(theta))
    570                 //     = n * (|v| * 1 * cos(theta))
    571                 //     = n * (|v| * cos(theta))
    572                 //     = n * |v_projected_onto_n|
    573                 //     = v_projected_onto_n
    574                 v2 colliding_velocity_component = math_v2f_m(wall_normal, math_v2_dot(player->velocity, wall_normal));
    575 
    576                 player->velocity = math_v2_s(player->velocity, colliding_velocity_component);
    577 
    578                 player_delta = math_v2f_m(player->velocity, dt);
    579                 new_p = math_v2_a(player->pos.local, player_delta);
    580                 remaining_time_factor -= smallest_distance_scale_factor;
    581             }
    582         }
    583 #undef RENDER_COLLISION_DEBUG_QUAD
    584 
    585         player->pos = world_pos_recanonicalize(player->pos);
    586     }
    587 
    588     // render_player
    589     {
    590         struct Entity player = game_state->player;
    591 
    592         m4 model = math_m4_init_id();
    593         model = math_translate(model, (v3) {player.pos.local.x, player.pos.local.y, 0.0f});
    594         model = math_scale(model, (v3) {player.dimensions.x, player.dimensions.y, 1.0f});
    595 
    596         v3 color = (v3) { 1.0f, 0.0f, 1.0f };
    597 
    598         renderer_job_enqueue(
    599             &game_state->renderer,
    600             (struct RenderJob) {
    601                 .ebo = game_state->renderer.quad_ebo,
    602                 .color = color,
    603                 .model = model,
    604                 .layer = RENDER_LAYER_PLAYER,
    605             });
    606     }
    607 
    608     // game_render_tiles
    609     {
    610         for (size_t y = 0; y < ROOM_DIM_Y; y++)
    611         {
    612             for (size_t x = 0; x < ROOM_DIM_X; x++)
    613             {
    614                 v2 tile_pos = {
    615                     x,
    616                     ROOM_DIM_Y - 1.0f - y,
    617                 };
    618 
    619                 m4 model = math_m4_init_id();
    620                 // our square verts are anchored around the center point of the
    621                 // square, so we want to offset by 0.5 to instead have our
    622                 // anchor in the min corner
    623                 model = math_translate(model, (v3) {TILE_SIZE * 0.5f, TILE_SIZE * 0.5f, 0.0f});
    624                 model = math_translate(model, (v3) {tile_pos.x, tile_pos.y, 0.0f});
    625                 model = math_scale(model, (v3) {TILE_SIZE, TILE_SIZE, 1.0f});
    626 
    627                 v3 color;
    628                 bool solid = world_tile_in_room_is_solid(current_room->tiles, tile_pos);
    629                 f32 tex_interp = 0.0f;
    630                 u8 tile_id = 0;
    631                 if (solid)
    632                 {
    633                     color = (v3) {0.4f, 0.4f, 0.4f};
    634                     tex_interp = 1.0f;
    635                     tile_id = get_tile_value(current_room->tiles, tile_pos);
    636                     assert(tile_id > 0);
    637                     tile_id -= 1;
    638                     tile_id += current_room->tileset_offset;
    639                 }
    640                 else
    641                 {
    642                     color = (math_v3_from_rgb(0x16161D));
    643                 }
    644 
    645                 renderer_job_enqueue(
    646                     &game_state->renderer,
    647                     (struct RenderJob) {
    648                         .ebo = game_state->renderer.quad_ebo,
    649                         .color = color,
    650                         .model = model,
    651                         .tex_interp = tex_interp,
    652                         .tile_id = tile_id,
    653                         .layer = RENDER_LAYER_TILES,
    654                     });
    655             }
    656         }
    657     }
    658 
    659     renderer_jobs_sort(&game_state->renderer, &game_state->world_allocator);
    660     renderer_jobs_draw(&game_state->renderer, framebuffer);
    661 }
    662 
    663 internal void game_cleanup(struct GameMemory *game_memory)
    664 {
    665     assert(sizeof(struct GameState) <= game_memory->buffer_size);
    666     struct GameState *game_state = (struct GameState *)game_memory->buffer;
    667     renderer_cleanup(&game_state->renderer);
    668 }
    669 
    670 #ifdef PLATFORM_HOTLOAD_GAME_CODE
    671 void game_load_opengl_symbols(void)
    672 {
    673 #if defined(PLATFORM_LINUX)
    674     am_gl_init();
    675 #endif
    676 }
    677 #endif