Механика вскарабкивания для игры от первого лица (как в Thief)
В играх в том или ином виде можно встретить механику вскарабкивания на небольшие препятствия — это позволяет добиться лучшего ощущения контроля за персонажем и расширить механики перемещения.
Я реализовал систему, которая не требует предварительного расставления точек взаимодействия: игрок может залезть на любой валидный уступ, до которого сможет дотянуться.
Готовый прототип можно попробовать в билде 0.17.
Алгоритм
На схемах ниже отражены все этапы определения уступа и проверки коллизии. Код алгоритма целиком приведён в конце статьи.
1. Проверка условия для старта вскарабкивания
Во время прыжка ищем ближайший уступ перед игроком. Уступами являются грани коллайдеров.
Мы начинаем искать уступ только если:
- игрок в воздухе
- удерживает прыжок
- движется вверх
- находится не слишком низко над землёй
Если все условия соблюдены — считаем уступ валидным
2. Коррекция целевой точки
Без коррекции игрок оказался бы на самом краю и мог бы соскользнуть. Поэтому:
- берём направление от игрока к уступу
- убираем вертикальную составляющую
- сдвигаем точку немного вперёд (в моём коде — на половину радиуса капсулы игрока)
Это гарантирует, что новая точка находится на поверхности, куда можно безопасно переместить игрока.
3. Тест проникновения капсулы
Мы создаём виртуальную капсулу игрока в целевой точке, чтобы проверить, не блокирует ли что-то ей путь:
- если пересечений нет → путь свободен → начинаем анимацию
- если есть → вскарабкивание отменяется
4. Анимация
Отнимаем у игрока управление и начинаем анимацию, как он плавно перемещается из своей позиции в найденную точку.
Анимация разбита на 2 фазы:
- вертикальный подъём
- горизонтальный рывок на уступ
Это выглядит естественнее, чем линейный переход сразу по диагонали. После завершения анимации управление возвращается игроку.
Итоговый код
Полная реализация из моей игры: поиск уступа, проверка коллизии и анимация подъёма
static BOOL Player_DetectLedge(const Object *player, Vector forward_dir, OUT Point *out_point)
{
Capsule capsule = {
.start = Vector_Add(Player_GetHeadPosition(player), Vector_Mul(forward_dir, PLAYER_RADIUS)), // move forward a bit
.end = Vector_Add(Player_GetFootPosition(player), Vector_Mul(forward_dir, PLAYER_RADIUS)),
.radius = 0.4f // ledge check radius
};
return Collision_FindClosestEdgeToCapsule(&capsule, out_point);
}
static void Player_StartClimbing(const Object *player, const Point *ledge)
{
player_state.climb_start = (Vector){ player->x, player->y, player->z };
player_state.climb_end = *ledge;
player_state.climb_animation_start_time = game_time;
player_state.climb_animation_duration = Vector_Length(Vector_Sub(player_state.climb_end, player_state.climb_start));
player_state.is_climbing = TRUE;
// Reset player velocity at climb start to prevent jump momentum from launching the player
player_state.vertical_velocity = 0;
player_state.movement_velocity = (Vector){ 0.0f, 0.0f, 0.0f };
PlaySound(climbing_sounds[0]);
}
static void Player_StopClimbing(Object *player)
{
player->x = player_state.climb_end.x;
player->y = player_state.climb_end.y;
player->z = player_state.climb_end.z;
player_state.is_climbing = FALSE;
}
static void Player_AnimateClimbing(Object *player, float t)
{
// Climbing animation has two phases:
// 1. Move player up to the ledge (vertical movement)
// 2. Move player forward to the ledge (horizontal movement)
const float first_phase_duration = 0.5f; // the first phase takes 50% of the time, which gives a more natural climbing motion
float t1 = t;
float t2 = Remap(max(t, first_phase_duration), first_phase_duration, 1.0f, 0.0f, 1.0f); // scale second phase to [0, 1] range
player->x = Lerp(player_state.climb_start.x, player_state.climb_end.x, t2);
player->y = Lerp(player_state.climb_start.y, player_state.climb_end.y, t1);
player->z = Lerp(player_state.climb_start.z, player_state.climb_end.z, t2);
}
void Player_UpdateClimbing(Object *player)
{
// Start looking for a ledge if player in a jumping state and holding jump button
if ( !player_state.is_climbing
&& !player_state.is_grounded && keys[VK_SPACE]
&& player_state.vertical_velocity > 0
&& player_state.in_air_time > 0.15f) {
// Find valid ledge
const Vector forward_dir = (Vector){ sinf(player->yaw), 0.0f, cosf(player->yaw) };
Point ledge;
if (!Player_DetectLedge(player, forward_dir, &ledge)) {
return;
}
if (ledge.y > (player->y - PLAYER_STEP_HEIGHT)) {
return; // the ledge is below player step
}
// Move a bit further in a direction of ledge
Vector ledge_direction = Vector_Sub(ledge, (Vector){ player->x, player->y, player->z });
ledge_direction.y = 0; // remove vertical component
ledge_direction = Vector_Normalize(ledge_direction);
ledge = Vector_Add(ledge, Vector_Mul(ledge_direction, PLAYER_RADIUS / 2));
// Check path space at the edge is clear
Capsule capsule;
capsule.p1 = (Point){ ledge.x, ledge.y - PLAYER_STEP_HEIGHT, ledge.z };
capsule.p2 = (Point){ ledge.x, ledge.y - PLAYER_HEIGHT, ledge.z };
capsule.radius = PLAYER_RADIUS;
if (Collision_FindCapsulePenetration(&capsule).hit) {
return; // something is blocking the target position
}
// Start climbing
Player_StartClimbing(player, &ledge);
}
// Process climbing state
if (player_state.is_climbing) {
// Calculate animation time
float t = Clip((game_time - player_state.climb_animation_start_time) / player_state.climb_animation_duration, 0.0f, 1.0f);
t = Smoothstep(t);
player_state.climb_time += time_delta;
// Climbing is done
if (t >= 1.0f) {
Player_StopClimbing(player);
return;
}
// Cancel animation if jump button released during the animation
if (player_state.is_climbing && !keys[VK_SPACE] && t <= 0.5f) {
player_state.is_climbing = FALSE;
return;
}
// Animate climbing
Player_AnimateClimbing(player, t);
}
}