From 7223e9ce0783364f2fa99623c7ea50a6b040e0a2 Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Sun, 18 Oct 2026 13:34:30 +0200 Subject: [PATCH] feat: add moving platform support + fix jolt --- .../destrum/Physics/JoltPhysicsWorld.h | 2 +- .../include/destrum/Physics/PhysicsWorld.h | 4 +- .../destrum/Physics/SimplePhysicsWorld.h | 2 +- destrum/src/Physics/JoltPhysicsWorld.cpp | 10 +-- destrum/src/Physics/PhysicsSceneBridge.cpp | 2 +- destrum/src/Physics/PhysicsWorld.cpp | 2 +- destrum/src/Physics/SimplePhysicsWorld.cpp | 5 +- lightkeeper/CMakeLists.txt | 1 + .../components/FirstPersonController.h | 12 ++- .../include/components/GameComponentList.h | 2 + .../include/components/MovingPlatform.h | 38 +++++++++ .../src/components/FirstPersonController.cpp | 80 ++++++++++++++----- lightkeeper/src/components/MovingPlatform.cpp | 79 ++++++++++++++++++ lightkeeper/src/scenes/MainScene.cpp | 43 +++++++++- tests/destrum_tests.cpp | 43 ++++++++++ 15 files changed, 291 insertions(+), 34 deletions(-) create mode 100644 lightkeeper/include/components/MovingPlatform.h create mode 100644 lightkeeper/src/components/MovingPlatform.cpp diff --git a/destrum/include/destrum/Physics/JoltPhysicsWorld.h b/destrum/include/destrum/Physics/JoltPhysicsWorld.h index 27d6295..425270b 100644 --- a/destrum/include/destrum/Physics/JoltPhysicsWorld.h +++ b/destrum/include/destrum/Physics/JoltPhysicsWorld.h @@ -35,7 +35,7 @@ public: void Step(float fixedDt) override; - void SyncKinematicBodiesToPhysics() override; + void SyncKinematicBodiesToPhysics(float fixedDt) override; void SyncDynamicBodiesToTransforms() override; PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc) override; diff --git a/destrum/include/destrum/Physics/PhysicsWorld.h b/destrum/include/destrum/Physics/PhysicsWorld.h index 7f06063..d6dbab8 100644 --- a/destrum/include/destrum/Physics/PhysicsWorld.h +++ b/destrum/include/destrum/Physics/PhysicsWorld.h @@ -29,9 +29,9 @@ public: virtual void Step(float fixedDt) = 0; - // - Kinematic: Transform -> physics body before Step() + // - Kinematic: moved towards its Transform over the step, so it has a velocity // - Dynamic: physics body -> Transform after Step() - virtual void SyncKinematicBodiesToPhysics(); + virtual void SyncKinematicBodiesToPhysics(float fixedDt); virtual void SyncDynamicBodiesToTransforms(); virtual PhysicsBodyHandle CreateBody(const PhysicsBodyDesc& desc) = 0; diff --git a/destrum/include/destrum/Physics/SimplePhysicsWorld.h b/destrum/include/destrum/Physics/SimplePhysicsWorld.h index 3c6057f..26da7da 100644 --- a/destrum/include/destrum/Physics/SimplePhysicsWorld.h +++ b/destrum/include/destrum/Physics/SimplePhysicsWorld.h @@ -49,7 +49,7 @@ public: PhysicsRaycastHit& hit, PhysicsBodyHandle ignoreBody = {}) const override; - void SyncKinematicBodiesToPhysics(); + void SyncKinematicBodiesToPhysics(float fixedDt) override; void SyncDynamicBodiesToTransforms(); [[nodiscard]] const glm::vec3& GetGravity() const { return m_Gravity; } diff --git a/destrum/src/Physics/JoltPhysicsWorld.cpp b/destrum/src/Physics/JoltPhysicsWorld.cpp index 09d247c..966fea1 100644 --- a/destrum/src/Physics/JoltPhysicsWorld.cpp +++ b/destrum/src/Physics/JoltPhysicsWorld.cpp @@ -548,7 +548,7 @@ public: return events; } - void SyncKinematicBodiesToPhysics() + void SyncKinematicBodiesToPhysics(float fixedDt) { BodyInterface& bodyInterface = m_PhysicsSystem.GetBodyInterface(); @@ -562,11 +562,11 @@ public: Transform& transform = record.desc.owner->GetTransform(); - bodyInterface.SetPositionAndRotation( + bodyInterface.MoveKinematic( record.bodyID, ToJoltRVec3(transform.GetWorldPosition()), ToJoltQuat(transform.GetWorldRotation()), - EActivation::Activate); + fixedDt); } } @@ -819,9 +819,9 @@ void JoltPhysicsWorld::Step(float fixedDt) m_Impl->Step(fixedDt); } -void JoltPhysicsWorld::SyncKinematicBodiesToPhysics() +void JoltPhysicsWorld::SyncKinematicBodiesToPhysics(float fixedDt) { - m_Impl->SyncKinematicBodiesToPhysics(); + m_Impl->SyncKinematicBodiesToPhysics(fixedDt); } void JoltPhysicsWorld::SyncDynamicBodiesToTransforms() diff --git a/destrum/src/Physics/PhysicsSceneBridge.cpp b/destrum/src/Physics/PhysicsSceneBridge.cpp index 0a7ebf1..b66d8a8 100644 --- a/destrum/src/Physics/PhysicsSceneBridge.cpp +++ b/destrum/src/Physics/PhysicsSceneBridge.cpp @@ -35,7 +35,7 @@ void PhysicsSceneBridge::RefreshGameObject(GameObject& object) { void PhysicsSceneBridge::FixedUpdate(float fixedDt) { if (!m_World) return; - m_World->SyncKinematicBodiesToPhysics(); + m_World->SyncKinematicBodiesToPhysics(fixedDt); m_World->Step(fixedDt); for (const auto& event : m_World->ConsumeTriggerEvents()) { diff --git a/destrum/src/Physics/PhysicsWorld.cpp b/destrum/src/Physics/PhysicsWorld.cpp index fa78174..5121f9c 100644 --- a/destrum/src/Physics/PhysicsWorld.cpp +++ b/destrum/src/Physics/PhysicsWorld.cpp @@ -115,7 +115,7 @@ void PhysicsWorld::UnregisterRigidbody(Rigidbody& rigidbody) { rigidbody.DetachPhysicsBody(); } -void PhysicsWorld::SyncKinematicBodiesToPhysics() { +void PhysicsWorld::SyncKinematicBodiesToPhysics(float) { // Backend-independent sync is intentionally not possible here because // PhysicsWorld does not own the list of active rigidbodies. } diff --git a/destrum/src/Physics/SimplePhysicsWorld.cpp b/destrum/src/Physics/SimplePhysicsWorld.cpp index f6cfd74..0c9bd48 100644 --- a/destrum/src/Physics/SimplePhysicsWorld.cpp +++ b/destrum/src/Physics/SimplePhysicsWorld.cpp @@ -134,7 +134,7 @@ void SimplePhysicsWorld::Step(float fixedDt) { m_Bodies.end()); } -void SimplePhysicsWorld::SyncKinematicBodiesToPhysics() { +void SimplePhysicsWorld::SyncKinematicBodiesToPhysics(float fixedDt) { for (BodyRecord& body : m_Bodies) { if (!body.alive || body.desc.type != RigidbodyType::Kinematic || !body.desc.owner || body.desc.owner->IsBeingDestroyed() || !body.desc.owner->IsActiveInHierarchy()) { @@ -146,6 +146,9 @@ void SimplePhysicsWorld::SyncKinematicBodiesToPhysics() { body.previousTransform = body.currentTransform; body.currentTransform.position = transform.GetWorldPosition(); body.currentTransform.rotation = transform.GetWorldRotation(); + body.linearVelocity = fixedDt > 0.0f + ? (body.currentTransform.position - body.previousTransform.position) / fixedDt + : glm::vec3{0.0f}; } } diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index 5e123a6..a2d0e9d 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -18,6 +18,7 @@ set(GAME_SRC src/components/Interactable.cpp src/components/InteractableButton.cpp src/components/LightToggle.cpp + src/components/MovingPlatform.cpp src/components/PrintComponent.cpp src/components/TriggerLoggerComponent.cpp ) diff --git a/lightkeeper/include/components/FirstPersonController.h b/lightkeeper/include/components/FirstPersonController.h index 5a5fa09..4656ee4 100644 --- a/lightkeeper/include/components/FirstPersonController.h +++ b/lightkeeper/include/components/FirstPersonController.h @@ -1,6 +1,8 @@ #ifndef LIGHTKEEPER_FIRSTPERSONCONTROLLER_H #define LIGHTKEEPER_FIRSTPERSONCONTROLLER_H +#include + #include #include #include @@ -45,7 +47,12 @@ public: void SetPossessed(bool possessed); private: - [[nodiscard]] bool CheckGrounded() const; + struct GroundContact { + glm::vec3 normal; + glm::vec3 velocity; + }; + + [[nodiscard]] std::optional FindGround() const; [[nodiscard]] glm::vec3 GetWishDirection() const; [[nodiscard]] Interactable* FindFocusedInteractable() const; @@ -67,11 +74,14 @@ private: float m_CoyoteTime{0.1f}; float m_JumpBufferTime{0.12f}; float m_InteractDistance{3.0f}; + float m_MaxSlopeAngle{glm::radians(50.0f)}; + float m_GroundStickSpeed{2.0f}; Interactable* m_FocusedInteractable{nullptr}; bool m_Possessed{false}; bool m_Grounded{false}; + glm::vec3 m_GroundNormal{0.0f, 1.0f, 0.0f}; bool m_JumpHeld{false}; float m_CoyoteTimer{0.0f}; float m_JumpBufferTimer{0.0f}; diff --git a/lightkeeper/include/components/GameComponentList.h b/lightkeeper/include/components/GameComponentList.h index 75a44bb..8519d2c 100644 --- a/lightkeeper/include/components/GameComponentList.h +++ b/lightkeeper/include/components/GameComponentList.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -13,6 +14,7 @@ X(FirstPersonController) \ X(InteractableButton) \ X(LightToggle) \ + X(MovingPlatform) \ X(PrintComponent) \ X(TriggerLoggerComponent) diff --git a/lightkeeper/include/components/MovingPlatform.h b/lightkeeper/include/components/MovingPlatform.h new file mode 100644 index 0000000..2cc2354 --- /dev/null +++ b/lightkeeper/include/components/MovingPlatform.h @@ -0,0 +1,38 @@ +#ifndef LIGHTKEEPER_MOVINGPLATFORM_H +#define LIGHTKEEPER_MOVINGPLATFORM_H + +#include + +#include + +class MovingPlatform final : public Component { +public: + explicit MovingPlatform(GameObject& parent); + + std::string GetTypeName() const override { return "MovingPlatform"; } + + void Start() override; + void Update(float dt) override; + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; + + void SetTravelOffset(const glm::vec3& offset) { m_TravelOffset = offset; } + void SetTravelDuration(float seconds) { m_TravelDuration = seconds; } + void SetPauseDuration(float seconds) { m_PauseDuration = seconds; } + +private: + [[nodiscard]] float GetTravelProgress() const; + [[nodiscard]] glm::vec3 GetOffsetAt(float progress) const; + + glm::vec3 m_TravelOffset{8.0f, 0.0f, 0.0f}; + float m_TravelDuration{4.0f}; + float m_PauseDuration{1.0f}; + + float m_ElapsedTime{0.0f}; + glm::vec3 m_StartPosition{0.0f}; + bool m_Started{false}; +}; + +#endif // LIGHTKEEPER_MOVINGPLATFORM_H diff --git a/lightkeeper/src/components/FirstPersonController.cpp b/lightkeeper/src/components/FirstPersonController.cpp index c86e925..36af014 100644 --- a/lightkeeper/src/components/FirstPersonController.cpp +++ b/lightkeeper/src/components/FirstPersonController.cpp @@ -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 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::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); diff --git a/lightkeeper/src/components/MovingPlatform.cpp b/lightkeeper/src/components/MovingPlatform.cpp new file mode 100644 index 0000000..c3edcb1 --- /dev/null +++ b/lightkeeper/src/components/MovingPlatform.cpp @@ -0,0 +1,79 @@ +#include + +#include +#include + +#include + +#include + +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(), offset.at(1).get(), offset.at(2).get()}; + } + 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"); +} diff --git a/lightkeeper/src/scenes/MainScene.cpp b/lightkeeper/src/scenes/MainScene.cpp index dc556cc..0494be3 100644 --- a/lightkeeper/src/scenes/MainScene.cpp +++ b/lightkeeper/src/scenes/MainScene.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,7 @@ #include +#include #include #include #include @@ -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(); boxMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox(size))); @@ -221,6 +225,42 @@ namespace { button->AddComponent()->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()->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); diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index 835b84f..b32a859 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -540,6 +540,48 @@ namespace { "a frustum entirely behind the camera must not overlap"); } + void testKinematicBodiesMoveWithVelocity() + { + constexpr float fixedDt = 1.0f / 60.0f; + Scene& scene = SceneManager::GetInstance().CreateScene("kinematic-velocity"); + + GameObject* platform = scene.CreateGameObject("platform"); + platform->AddComponent(glm::vec3{2.0f, 0.25f, 2.0f}); + auto* platformBody = platform->AddComponent(); + platformBody->SetType(RigidbodyType::Kinematic); + + GameObject* crate = scene.CreateGameObject("crate"); + crate->GetTransform().SetWorldPosition({0.0f, 0.76f, 0.0f}); + crate->AddComponent(glm::vec3{0.5f}); + auto* crateBody = crate->AddComponent(); + crateBody->SetFriction(1.0f); + crateBody->SetAllowSleep(false); + + scene.CommitPendingAdditions(); + scene.GetPhysics().RefreshGameObject(*platform); + scene.GetPhysics().RefreshGameObject(*crate); + + for (int step = 0; step < 30; ++step) { + scene.FixedUpdate(fixedDt); + } + check(glm::length(platformBody->GetLinearVelocity()) < 0.001f, + "a kinematic body that is not moved must have no velocity"); + + constexpr float platformSpeed = 2.0f; + for (int step = 0; step < 60; ++step) { + platform->GetTransform().SetWorldPosition( + platform->GetTransform().GetWorldPosition() + glm::vec3{platformSpeed * fixedDt, 0.0f, 0.0f}); + scene.FixedUpdate(fixedDt); + } + + check(close(platformBody->GetLinearVelocity().x, platformSpeed), + "a kinematic body moved through its transform must get the matching velocity"); + check(crate->GetTransform().GetWorldPosition().x > 1.5f, + "a dynamic body resting on a moving kinematic platform must be carried along"); + + SceneManager::GetInstance().Destroy(); + } + void testRenderer2DShapes() { auto& r2d = Renderer2D::GetInstance(); @@ -1335,6 +1377,7 @@ int main() testColliderCenterOffset(); testCapsuleColliderDebugLines(); testFrustumFromViewProj(); + testKinematicBodiesMoveWithVelocity(); testRenderer2DShapes(); testFontAtlasAndLayout(); std::cout << "destrum tests passed\n";