#include "player.h" #include "world.h" #include "twn_game_api.h" #include #include #include #include static void update_timers(Player *player) { tick_timer(&player->jump_air_timer); tick_timer(&player->jump_coyote_timer); tick_timer(&player->jump_buffer_timer); } static void input_move(InputState *input, Player *player) { /* apply horizontal damping when the player stops moving */ /* in other words, make it decelerate to a standstill */ if (!input_is_action_pressed(input, "player_left") && !input_is_action_pressed(input, "player_right")) { player->dx *= player->horizontal_damping; } int input_dir = 0; if (input_is_action_pressed(input, "player_left")) input_dir = -1; if (input_is_action_pressed(input, "player_right")) input_dir = 1; if (input_is_action_pressed(input, "player_left") && input_is_action_pressed(input, "player_right")) input_dir = 0; player->dx += (float)input_dir * player->run_horizontal_speed; player->dx = SDL_clamp(player->dx, -player->max_dx, player->max_dx); if (fabs(player->dx) < player->run_horizontal_speed) { player->dx = 0; } } static void jump(Player *player) { player->jump_coyote_timer = 0; player->jump_buffer_timer = 0; player->dy = player->jump_force_initial; player->action = PLAYER_ACTION_JUMP; player->jump_air_timer = player->jump_air_ticks; } static void input_jump(InputState *input, Player *player) { player->current_gravity_multiplier = player->jump_default_multiplier; if (input_is_action_just_pressed(input, "player_jump")) { player->jump_air_timer = 0; player->jump_buffer_timer = player->jump_buffer_ticks; if (player->action == PLAYER_ACTION_GROUND || player->jump_coyote_timer > 0) { jump(player); } } if (input_is_action_pressed(input, "player_jump")) { if (player->action != PLAYER_ACTION_GROUND && player->jump_air_timer > 0) { player->current_gravity_multiplier = player->jump_boosted_multiplier; player->dy += player->jump_force_increase; } } } static void update_collider_x(Player *player) { player->collider_x.w = player->rect.w; player->collider_x.h = player->rect.h - 8; player->collider_x.x = player->rect.x; player->collider_x.y = player->rect.y + ((player->rect.h - player->collider_x.h) / 2); } static void update_collider_y(Player *player) { player->collider_y.w = player->rect.w; player->collider_y.h = player->rect.h; player->collider_y.x = player->rect.x + ((player->rect.w - player->collider_y.w) / 2); player->collider_y.y = player->rect.y; } static void apply_gravity(Player *player, float gravity) { player->dy -= gravity * player->current_gravity_multiplier; player->dy = fmax(player->dy, -player->terminal_velocity); if (player->dy < 0) { /* just started falling */ if (player->action == PLAYER_ACTION_GROUND) { player->jump_coyote_timer = player->jump_coyote_ticks; } player->action = PLAYER_ACTION_FALL; } } /* returns whether or not a correction was applied */ static bool corner_correct(Player *player, Rect collision) { /* * somewhat of a hack here. we only want to do corner correction * if the corner in question really is the corner of a "platform," * and not simply the edge of a single tile in a row of many * tiles, as that would briefly clip the player into the ceiling, * halting movement. the dumbest way i could think of to fix this * is to simply ensure that there's no tile to possibly clip into * before correcting, by checking if there's a tile right above * the center of the player */ if (world_is_tile_at(player->world, player->rect.x + (player->rect.w / 2), player->rect.y - 1)) { return false; } float player_center_x = player->collider_x.x + (player->collider_x.w / 2); float collision_center_x = collision.x + (collision.w / 2); float abs_difference = fabs(player_center_x - collision_center_x); /* we're good, no correction needed */ if (abs_difference < player->jump_corner_correction_offset) return false; /* collision was on player's right side */ if (player_center_x < collision_center_x) { player->rect.x -= collision.w / 2; } /* collision was on player's left side */ else if (player_center_x > collision_center_x) { player->rect.x += collision.w / 2; } return true; } static void calc_collisions_x(Player *player) { Rect collision; bool is_colliding = world_find_intersect_frect(player->world, player->collider_x, &collision); if (!is_colliding) return; float player_center_x = player->collider_x.x + (player->collider_x.w / 2); float collision_center_x = collision.x + (collision.w / 2); typedef enum CollisionDirection { COLLISION_LEFT = -1, COLLISION_RIGHT = 1 } CollisionDirection; CollisionDirection dir_x = player_center_x > collision_center_x ? COLLISION_LEFT : COLLISION_RIGHT; player->rect.x -= collision.w * (float)dir_x; player->dx = 0; } static void calc_collisions_y(Player *player) { Rect collision; bool is_colliding = world_find_intersect_frect(player->world, player->collider_y, &collision); if (!is_colliding) return; float player_center_y = player->collider_y.y + (player->collider_y.h / 2); float collision_center_y = collision.y + (collision.h / 2); typedef enum CollisionDirection { COLLISION_ABOVE = -1, COLLISION_BELOW = 1 } CollisionDirection; CollisionDirection dir_y = player_center_y > collision_center_y ? COLLISION_ABOVE : COLLISION_BELOW; /* before the resolution */ if (dir_y == COLLISION_ABOVE) { if (corner_correct(player, collision)) { return; } } /* resolution */ player->rect.y -= collision.h * (float)dir_y; player->dy = 0; /* after the resolution */ if (dir_y == COLLISION_BELOW) { /* bandaid fix for float precision-related jittering */ player->rect.y = ceilf(player->rect.y); player->action = PLAYER_ACTION_GROUND; if (player->jump_buffer_timer > 0) { jump(player); apply_gravity(player, player->world->gravity); } } else if (dir_y == COLLISION_ABOVE) { player->jump_air_timer = 0; } } Player *player_create(World *world) { Player *player = cmalloc(sizeof *player); *player = (Player) { .world = world, .sprite_w = 48, .sprite_h = 48, .rect = (Rect) { .x = 92, .y = 200, .w = 16, .h = 32, }, .action = PLAYER_ACTION_GROUND, .collider_thickness = 42, .max_dx = 8, .run_horizontal_speed = 0.5f, .horizontal_damping = 0.9f, .current_gravity_multiplier = 1, .terminal_velocity = 30, .jump_force_initial = 10, .jump_force_increase = 1, .jump_air_ticks = 18, .jump_coyote_ticks = 8, .jump_buffer_ticks = 8, .jump_default_multiplier = 1.4f, .jump_boosted_multiplier = 1, .jump_corner_correction_offset = 16.0f, }; return player; } static void drawdef(Player *player) { m_sprite("/assets/player/baron-walk.png", (Rect) { .x = player->rect.x + ((player->rect.w - player->sprite_w) / 2), .y = player->rect.y - 8, .w = player->sprite_w, .h = player->sprite_h, }); push_circle((Vec2) { 256, 128 }, 24, (Color) { 255, 0, 0, 255 }); } static void drawdef_debug(Player *player) { if (!ctx.debug) return; /* const int info_separation = 24; */ /* const struct RectPrimitive info_theme = { */ /* .x = 8, */ /* .r = 0, .g = 0, .b = 0, .a = 255, */ /* }; */ push_rectangle(player->collider_x, (Color){ 0, 0, 255, 128 }); push_rectangle(player->collider_y, (Color){ 0, 0, 255, 128 }); } void player_destroy(Player *player) { free(player); } void player_calc(Player *player) { update_timers(player); input_move(&ctx.input, player); input_jump(&ctx.input, player); player->rect.x += player->dx; update_collider_x(player); calc_collisions_x(player); apply_gravity(player, player->world->gravity); player->rect.y -= player->dy; update_collider_y(player); calc_collisions_y(player); update_collider_x(player); update_collider_y(player); drawdef(player); drawdef_debug(player); }