feat: add moving platform support + fix jolt
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef LIGHTKEEPER_FIRSTPERSONCONTROLLER_H
|
||||
#define LIGHTKEEPER_FIRSTPERSONCONTROLLER_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <destrum/Input/InputManager.h>
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
@@ -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<GroundContact> 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};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <components/FirstPersonController.h>
|
||||
#include <components/InteractableButton.h>
|
||||
#include <components/LightToggle.h>
|
||||
#include <components/MovingPlatform.h>
|
||||
#include <components/PrintComponent.h>
|
||||
#include <components/TriggerLoggerComponent.h>
|
||||
|
||||
@@ -13,6 +14,7 @@
|
||||
X(FirstPersonController) \
|
||||
X(InteractableButton) \
|
||||
X(LightToggle) \
|
||||
X(MovingPlatform) \
|
||||
X(PrintComponent) \
|
||||
X(TriggerLoggerComponent)
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef LIGHTKEEPER_MOVINGPLATFORM_H
|
||||
#define LIGHTKEEPER_MOVINGPLATFORM_H
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user