Дивжения по наклонным поверхностяи в Godot (KinematicBody)
Знакомлюсь с игровым движком Godot. Пробую делать своего игрока на основе KinematicBody. Сцена самого игрока выглядит примерно так:
Далее решил описать движения игрока. В целом пока все достаточно просто - есть вектор направления, который зависит от того какие кнопки зажаты и от положения камеры. Определяется он примерно вот так в методе process:
void PlayerController::process(float delta)
{
(void)delta;
// Сбросить состояние
state_ = PlayerState::eWaiting;
// Вектор направления задаваемого кнопками
Vector3 movementDir = {0.0f, 0.0f, 0.0f};
// Изменить локальный вектор направления в зависимости от нажатых клавиш
auto input = Input::get_singleton();
if(input->is_action_pressed("forward")) movementDir += Vector3(0.0f, 0.0f, -1.0f);
if(input->is_action_pressed("backward")) movementDir += Vector3(0.0f, 0.0f, 1.0f);
if(input->is_action_pressed("left")) movementDir += Vector3(-1.0f, 0.0f, 0.0f);
if(input->is_action_pressed("right")) movementDir += Vector3(1.0f, 0.0f, 0.0f);
// Если направление движения изменяется
if(movementDir.length_squared() > 0.0f && pCamHor_ != nullptr)
{
// Установить направление (направление устанавливается относительно камеры)
direction_ = Transform(pCamHor_->get_global_transform().basis) * movementDir.normalized();
// Установить состояние
state_ = PlayerState::eRunning;
}
}
Положение камеры при этом меняется в physics_process, и там же осуществляется само движение. Итоговый вектор скорости по сути получается при помощи умножения вектора направления на скалярное значение скорости. Все в целом хорошо, но как насчет наклонных поверхностей? Я решил проверить, и как ожидалось получилось вот так:
То есть при спуске вниз по наклонной возникают небольшие "подпрыгивания". Я так понял это происходит потому, что вектор скорости не направлен вдоль этой плоскости, и в итоге на небольшое время игрок оказывается оторван от нее, после чего в ход вступает вектор силы тяжести.
Я решил попробовать исправить это путем получения нужного вектора движения, который будет направлен вдоль. Я это сделал следующим образом:
- Получить нормаль поверхности
- Векторно умножить вектор текущего направления на эту нормаль, получив некий вектор t
- Векторно умножить нормаль на вектор t, получив итоговое направление движения
В коде выглядит как-то так:
Vector3 n = get_floor_normal();
Vector3 t = direction_.cross(n);
movementDirection = n.cross(t);
Но в итоге ничего не изменилось. Подпрыгивания остались, хотя я даже выводил в консоль этот вектор - и вроде как с ним все впорядке (то есть при движении вниз Y меньше 0, при движении вверх больше 0).
Весь код physics_process выглядит вот так:
void PlayerController::physicsProcess(float delta)
{
// Управление камерой
if(pCamHor_ != nullptr && pCamVert_ != nullptr)
{
// Ограничить угол по вертикали
camVertAngle_ = Math::clamp(camVertAngle_,camMinVAngle,camMaxVAngle);
// Новые значения углов зависят от времени между кадрами (интерполяция между старым и новым значением)
float newHAngle = Math::lerp(pCamHor_->get_rotation_degrees().y,camHorAngle_,delta * camAcceleration);
float newVAngle = Math::lerp(pCamVert_->get_rotation_degrees().x,camVertAngle_,delta * camAcceleration);
// Установить новое значение
pCamHor_->set_rotation_degrees(Vector3(0.0f,newHAngle,0.0f));
pCamVert_->set_rotation_degrees(Vector3(newVAngle,0.0f,0.0f));
}
// Вектор "вверх", определяет пол и стены, задается гравитацией
Vector3 up = -plrGravityVector.normalized();
// Направление движения
Vector3 movementDirection = direction_;
// Вектор скорости движения
Vector3 desiredVelocity = {0.0f, 0.0f, 0.0f};
// Скорость падения растет если игрок в воздухе
freeFallVelocity_ = this->is_on_floor() ? Vector3(0.0f, 0.0f, 0.0f) : freeFallVelocity_ + (plrGravityVector * plrGravityAcceleration * delta);
// Если игрок двигается
if(state_ == PlayerState::eRunning)
{
// Если игрок находится на полу и пол является наклонным
if(is_on_floor() && up.dot(get_floor_normal()) < 1.0f)
{
// Вычислить вектор движения вдоль наклонной поверхности
Vector3 n = get_floor_normal();
Vector3 t = direction_.cross(n);
movementDirection = n.cross(t);
}
// Желаемая скорость
desiredVelocity = movementDirection * plrSpeed;
}
velocity_ = desiredVelocity;
// Двигать объект
this->move_and_slide(velocity_ + freeFallVelocity_,up);
}
Что я делаю не так? Почему вектор скорости направленный вдоль наклонной поверхности не решает эту проблему? Может это какой-то баг движка? А может подобное в godot реализовывается как-то иначе?
Ответы (1 шт):
Данную проблему удалось решить лишь при помощи своего метода isOnFloor, поскольку метод is_on_floor у KinematicBody, предлагаемый движком, работает довольно странно и судя по всему это и является причиной данной проблемы.
Выкладываю код своего метода isOnFloor использующего рей-каст, может быть кому-то пригодится.
/**
* \brief Находится ли на полу/земле игрок
* \param up Вектор вверх
* \param contactNormal Нормаль в точке соприкосновения с полом
* \param maxSlopeAngle Максимальный угол поверхности (в радианах)
* \return Если игрок на поверхности и угол наклона не превышает maxSlopeAngle будет отдано true
*/
bool Player::isOnFloor(const Vector3 &up, Vector3 *contactNormal, float maxSlopeAngle)
{
// Если есть collision shape
if(pCollisionShape_ != nullptr)
{
// Указатель на форму для коллизии
auto pCapsuleShape = Object::cast_to<CapsuleShape>(pCollisionShape_->get_shape().ptr());
// Если capsule shape задан
if(pCapsuleShape != nullptr)
{
// Высота капсулы
auto capsuleHeight = pCapsuleShape->get_height() + (pCapsuleShape->get_radius() * 2);
// Исходная и конечная точка для рей-каста (луч направлен вниз из центра collision shape)
Vector3 srcPoint = pCollisionShape_->get_global_transform().origin;
Vector3 dstPoint = srcPoint + (-up * ((capsuleHeight / 2.0f) + 0.05f));
// В исключения для луча необходимо добавить самого игрока и его коллизию
auto exclude = Array();
exclude.push_back(this);
exclude.push_back(pCollisionShape_);
// Трассировка сцены лучом
auto spaceState = this->get_world()->get_direct_space_state();
auto intersection = spaceState->intersect_ray(srcPoint,dstPoint,exclude);
// Если пересечение было
if(!intersection.empty())
{
// Получить нормаль в точке пересечения
auto normal = intersection["normal"].operator Vector3();
// Если угол наклона поверхности в допустимых пределах - считать что игрок на полу
if(normal.dot(up) >= Math::cos(maxSlopeAngle)){
if(contactNormal != nullptr) *contactNormal = normal;
return true;
}
}
}
}
return false;
}
P.S. Необходимо подключить следующие заголовочные файлы:
#include <CollisionShape.hpp>
#include <Shape.hpp>
#include <CapsuleShape.hpp>
#include <World.hpp>
#include <PhysicsDirectSpaceState.hpp>

