feat: add moving platform support + fix jolt

This commit is contained in:
2026-10-18 13:34:30 +02:00
parent 65a7310b56
commit 7223e9ce07
15 changed files with 291 additions and 34 deletions
@@ -27,6 +27,16 @@ namespace {
}
return current + delta / distance * maxDelta;
}
glm::vec3 ProjectOntoSlope(const glm::vec3& vector, const glm::vec3& slopeNormal)
{
const glm::vec3 projected = vector - slopeNormal * glm::dot(vector, slopeNormal);
const float projectedLength = glm::length(projected);
if (projectedLength < 1e-6f) {
return glm::vec3{0.0f};
}
return projected * (glm::length(vector) / projectedLength);
}
}
FirstPersonController::FirstPersonController(GameObject& parent)
@@ -130,22 +140,38 @@ void FirstPersonController::FixedUpdate(float fixedDt)
m_PreviousPosition = GetTransform().GetWorldPosition();
m_HasPreviousPosition = true;
glm::vec3 velocity = m_Rigidbody->GetLinearVelocity();
const std::optional<GroundContact> ground = FindGround();
const glm::vec3 groundVelocity = ground ? ground->velocity : glm::vec3{0.0f};
m_Grounded = velocity.y <= 0.1f && CheckGrounded();
// Movement happens relative to whatever is underneath, so moving platforms carry the player
glm::vec3 velocity = m_Rigidbody->GetLinearVelocity() - groundVelocity;
m_Grounded = ground.has_value() && glm::dot(velocity, ground->normal) <= 0.1f;
m_GroundNormal = m_Grounded ? ground->normal : glm::vec3{0.0f, 1.0f, 0.0f};
m_CoyoteTimer = m_Grounded ? m_CoyoteTime : std::max(m_CoyoteTimer - fixedDt, 0.0f);
const auto& input = InputManager::GetInstance();
const bool sprinting = m_Possessed && input.IsKeyDown(SDL_SCANCODE_LSHIFT);
const glm::vec3 wishVelocity = GetWishDirection() * (sprinting ? m_SprintSpeed : m_WalkSpeed);
const float acceleration = m_Grounded ? m_GroundAcceleration : m_AirAcceleration;
const glm::vec3 horizontal = MoveTowards(
glm::vec3{velocity.x, 0.0f, velocity.z},
wishVelocity,
acceleration * fixedDt);
velocity.x = horizontal.x;
velocity.z = horizontal.z;
if (m_Grounded)
{
const glm::vec3 velocityAlongSlope = velocity - m_GroundNormal * glm::dot(velocity, m_GroundNormal);
velocity = MoveTowards(
velocityAlongSlope,
ProjectOntoSlope(wishVelocity, m_GroundNormal),
m_GroundAcceleration * fixedDt);
velocity -= m_GroundNormal * m_GroundStickSpeed;
}
else
{
const glm::vec3 horizontal = MoveTowards(
glm::vec3{velocity.x, 0.0f, velocity.z},
wishVelocity,
m_AirAcceleration * fixedDt);
velocity.x = horizontal.x;
velocity.z = horizontal.z;
}
if (m_JumpBufferTimer > 0.0f && m_CoyoteTimer > 0.0f)
{
@@ -155,22 +181,25 @@ void FirstPersonController::FixedUpdate(float fixedDt)
m_Grounded = false;
}
// Falling faster than rising, and letting go of jump early cuts the jump short
float gravity = m_Gravity;
if (velocity.y < 0.0f || !m_JumpHeld)
if (!m_Grounded)
{
gravity *= m_FallGravityMultiplier;
// Falling faster than rising, and letting go of jump early cuts the jump short
float gravity = m_Gravity;
if (velocity.y < 0.0f || !m_JumpHeld)
{
gravity *= m_FallGravityMultiplier;
}
velocity.y -= gravity * fixedDt;
}
velocity.y -= gravity * fixedDt;
m_Rigidbody->SetLinearVelocity(velocity);
m_Rigidbody->SetLinearVelocity(velocity + groundVelocity);
}
bool FirstPersonController::CheckGrounded() const
std::optional<FirstPersonController::GroundContact> FirstPersonController::FindGround() const
{
const auto* world = m_Rigidbody->GetPhysicsWorld();
if (world == nullptr || m_Capsule == nullptr)
return false;
return std::nullopt;
const glm::vec3 center = GetTransform().GetWorldPosition() + m_Capsule->GetCenterOffset();
const float radius = m_Capsule->GetRadius();
@@ -187,6 +216,7 @@ bool FirstPersonController::CheckGrounded() const
constexpr float startHeight = 0.2f;
constexpr float probeDistance = 0.1f;
const float minWalkableNormalY = std::cos(m_MaxSlopeAngle);
for (const glm::vec2& offset : offsets)
{
const glm::vec3 origin{center.x + offset.x, bottom + startHeight, center.z + offset.y};
@@ -194,12 +224,15 @@ bool FirstPersonController::CheckGrounded() const
PhysicsRaycastHit hit{};
if (world->Raycast(origin, glm::vec3{0.0f, -1.0f, 0.0f}, startHeight + probeDistance, hit,
m_Rigidbody->GetBody()) &&
hit.normal.y > 0.6f)
hit.normal.y >= minWalkableNormalY)
{
return true;
return GroundContact{
.normal = glm::normalize(hit.normal),
.velocity = hit.body.IsValid() ? world->GetLinearVelocity(hit.body) : glm::vec3{0.0f},
};
}
}
return false;
return std::nullopt;
}
glm::vec3 FirstPersonController::GetWishDirection() const
@@ -243,9 +276,12 @@ void FirstPersonController::ImGuiInspector()
ImGui::DragFloat("Coyote Time", &m_CoyoteTime, 0.005f, 0.0f, 1.0f);
ImGui::DragFloat("Jump Buffer Time", &m_JumpBufferTime, 0.005f, 0.0f, 1.0f);
ImGui::DragFloat("Interact Distance", &m_InteractDistance, 0.05f, 0.0f, 20.0f);
ImGui::SliderAngle("Max Slope Angle", &m_MaxSlopeAngle, 0.0f, 89.0f);
ImGui::DragFloat("Ground Stick Speed", &m_GroundStickSpeed, 0.05f, 0.0f, 20.0f);
ImGui::DragFloat("Mouse Sensitivity", &m_MouseSensitivity, 0.0001f, 0.0f, 0.1f);
ImGui::DragFloat("Eye Height", &m_EyeHeight, 0.01f, -2.0f, 2.0f);
ImGui::Text("Grounded: %s", m_Grounded ? "yes" : "no");
ImGui::Text("Ground slope: %.1f deg", glm::degrees(std::acos(glm::clamp(m_GroundNormal.y, -1.0f, 1.0f))));
ImGui::Text("Yaw: %.1f deg", glm::degrees(m_Yaw));
ImGui::Text("Pitch: %.1f deg", glm::degrees(m_Pitch));
if (bool possessed = m_Possessed; ImGui::Checkbox("Possessed", &possessed)) {
@@ -266,6 +302,8 @@ nlohmann::json FirstPersonController::Serialize() const
{"coyoteTime", m_CoyoteTime},
{"jumpBufferTime", m_JumpBufferTime},
{"interactDistance", m_InteractDistance},
{"maxSlopeAngle", m_MaxSlopeAngle},
{"groundStickSpeed", m_GroundStickSpeed},
{"mouseSensitivity", m_MouseSensitivity},
{"eyeHeight", m_EyeHeight},
{"yaw", m_Yaw},
@@ -285,6 +323,8 @@ void FirstPersonController::Deserialize(const nlohmann::json& data)
m_CoyoteTime = data.value("coyoteTime", m_CoyoteTime);
m_JumpBufferTime = data.value("jumpBufferTime", m_JumpBufferTime);
m_InteractDistance = data.value("interactDistance", m_InteractDistance);
m_MaxSlopeAngle = data.value("maxSlopeAngle", m_MaxSlopeAngle);
m_GroundStickSpeed = data.value("groundStickSpeed", m_GroundStickSpeed);
m_MouseSensitivity = data.value("mouseSensitivity", m_MouseSensitivity);
m_EyeHeight = data.value("eyeHeight", m_EyeHeight);
m_Yaw = data.value("yaw", m_Yaw);
@@ -0,0 +1,79 @@
#include <components/MovingPlatform.h>
#include <algorithm>
#include <cmath>
#include <destrum/ObjectModel/GameObject.h>
#include <imgui.h>
MovingPlatform::MovingPlatform(GameObject& parent)
: Component(parent, "MovingPlatform")
{
}
void MovingPlatform::Start()
{
const glm::vec3 currentPosition = GetTransform().GetLocalPosition();
m_StartPosition = currentPosition - GetOffsetAt(GetTravelProgress());
m_Started = true;
}
void MovingPlatform::Update(float dt)
{
if (!m_Started) {
return;
}
m_ElapsedTime += dt;
GetTransform().SetLocalPosition(m_StartPosition + GetOffsetAt(GetTravelProgress()));
}
float MovingPlatform::GetTravelProgress() const
{
const float travelDuration = std::max(m_TravelDuration, 0.01f);
const float pauseDuration = std::max(m_PauseDuration, 0.0f);
const float legDuration = travelDuration + pauseDuration;
const float timeInCycle = std::fmod(m_ElapsedTime, legDuration * 2.0f);
const bool returning = timeInCycle >= legDuration;
const float timeInLeg = returning ? timeInCycle - legDuration : timeInCycle;
const float legProgress = std::clamp((timeInLeg - pauseDuration) / travelDuration, 0.0f, 1.0f);
return returning ? 1.0f - legProgress : legProgress;
}
glm::vec3 MovingPlatform::GetOffsetAt(float progress) const
{
const float eased = progress * progress * (3.0f - 2.0f * progress);
return m_TravelOffset * eased;
}
nlohmann::json MovingPlatform::Serialize() const
{
return {
{"travelOffset", {m_TravelOffset.x, m_TravelOffset.y, m_TravelOffset.z}},
{"travelDuration", m_TravelDuration},
{"pauseDuration", m_PauseDuration},
{"elapsedTime", m_ElapsedTime},
};
}
void MovingPlatform::Deserialize(const nlohmann::json& data)
{
if (data.contains("travelOffset")) {
const auto& offset = data.at("travelOffset");
m_TravelOffset = {offset.at(0).get<float>(), offset.at(1).get<float>(), offset.at(2).get<float>()};
}
m_TravelDuration = std::max(data.value("travelDuration", m_TravelDuration), 0.01f);
m_PauseDuration = std::max(data.value("pauseDuration", m_PauseDuration), 0.0f);
m_ElapsedTime = std::max(data.value("elapsedTime", m_ElapsedTime), 0.0f);
}
void MovingPlatform::ImGuiInspector()
{
ImGui::Text("Progress: %.0f%%", GetTravelProgress() * 100.0f);
ImGui::DragFloat3("Travel Offset", &m_TravelOffset.x, 0.05f);
ImGui::DragFloat("Travel Duration", &m_TravelDuration, 0.05f, 0.01f, 60.0f, "%.2f s");
ImGui::DragFloat("Pause Duration", &m_PauseDuration, 0.05f, 0.0f, 60.0f, "%.2f s");
}
+42 -1
View File
@@ -19,6 +19,7 @@
#include <components/Door.h>
#include <components/InteractableButton.h>
#include <components/LightToggle.h>
#include <components/MovingPlatform.h>
#include <components/TriggerLoggerComponent.h>
#include <glm/gtc/constants.hpp>
@@ -26,6 +27,7 @@
#include <spdlog/spdlog.h>
#include <cmath>
#include <memory>
#include <optional>
#include <string>
@@ -124,10 +126,12 @@ namespace {
GameObject* CreateBox(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources,
const std::string& name, const glm::vec3& position, const glm::vec3& size,
MaterialID materialID, RigidbodyType type)
MaterialID materialID, RigidbodyType type,
const glm::quat& rotation = glm::quat{1.0f, 0.0f, 0.0f, 0.0f})
{
GameObject* box = scene.CreateGameObject(name);
box->GetTransform().SetWorldPosition(position);
box->GetTransform().SetWorldRotation(rotation);
auto* boxMeshComp = box->AddComponent<MeshRendererComponent>();
boxMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox(size)));
@@ -221,6 +225,42 @@ namespace {
button->AddComponent<InteractableButton>()->AddTarget(target);
}
void CreateMovingPlatform(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID rampMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
const MaterialID platformMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
constexpr float laneZ = 2.0f;
constexpr float platformHeight = 1.0f;
constexpr float rampStartX = 9.0f;
constexpr float rampLength = 3.0f;
constexpr float rampThickness = 0.3f;
constexpr float travelDistance = 8.0f;
const glm::vec3 platformSize{3.0f, 0.3f, 3.0f};
const float platformTop = GroundHeight + platformHeight;
const glm::quat rampRotation =
glm::angleAxis(std::atan2(platformHeight, rampLength), glm::vec3{0.0f, 0.0f, 1.0f});
const glm::vec3 rampTopCenter{rampStartX + rampLength * 0.5f, GroundHeight + platformHeight * 0.5f, laneZ};
const glm::vec3 rampCenter = rampTopCenter - rampRotation * glm::vec3{0.0f, rampThickness * 0.5f, 0.0f};
const float rampSlopeLength = glm::length(glm::vec2{rampLength, platformHeight});
CreateBox(scene, gfxDevice, resources, "PlatformRamp", rampCenter,
{rampSlopeLength, rampThickness, platformSize.z}, rampMaterial, RigidbodyType::Static,
rampRotation);
const float platformStartX = rampStartX + rampLength + platformSize.x * 0.5f;
GameObject* platform = CreateBox(scene, gfxDevice, resources, "MovingPlatform",
{platformStartX, platformTop - platformSize.y * 0.5f, laneZ},
platformSize, platformMaterial, RigidbodyType::Kinematic);
platform->AddComponent<MovingPlatform>()->SetTravelOffset({travelDistance, 0.0f, 0.0f});
const float landingStartX = platformStartX + travelDistance + platformSize.x * 0.5f;
CreateBox(scene, gfxDevice, resources, "PlatformLanding",
{landingStartX + 1.0f, GroundHeight + platformHeight * 0.5f, laneZ},
{2.0f, platformHeight, platformSize.z}, rampMaterial, RigidbodyType::Static);
}
struct WallOpening {
float offset;
float width;
@@ -467,6 +507,7 @@ MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResour
const GameObject* door = CreateDoorway(scene, gfxDevice, resources);
CreateButton(scene, gfxDevice, resources, *door);
CreateHouse(scene, gfxDevice, resources);
CreateMovingPlatform(scene, gfxDevice, resources);
CreatePlayer(scene, objects);
LoadSpawnBallResources(gfxDevice, resources, objects);