feat: improve fps controller / add fog and ambient light to the scene
This commit is contained in:
@@ -32,6 +32,7 @@ private:
|
||||
void createEmptyScene();
|
||||
void submitBoxColliderDebugLines();
|
||||
[[nodiscard]] CameraComponent* getSceneCamera() const;
|
||||
[[nodiscard]] bool isPlayerViewActive() const;
|
||||
|
||||
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
class Rigidbody;
|
||||
class CapsuleCollider;
|
||||
|
||||
// Dynamic capsule character. Velocity is driven directly (no forces) so movement stays responsive:
|
||||
// acceleration towards the wished velocity, own gravity, coyote time and jump buffering.
|
||||
class FirstPersonController final : public Component {
|
||||
public:
|
||||
explicit FirstPersonController(GameObject& parent);
|
||||
@@ -22,27 +24,53 @@ public:
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
|
||||
[[nodiscard]] float GetYaw() const { return m_Yaw; }
|
||||
void SetYaw(float yawRadians) { m_Yaw = yawRadians; }
|
||||
[[nodiscard]] float GetPitch() const { return m_Pitch; }
|
||||
[[nodiscard]] float GetEyeHeight() const { return m_EyeHeight; }
|
||||
|
||||
void SetMovementSpeed(float speed) { m_MovementSpeed = speed; }
|
||||
[[nodiscard]] float GetMovementSpeed() const { return m_MovementSpeed; }
|
||||
// Eye position interpolated between the last two physics steps, so the camera moves smoothly at any framerate
|
||||
[[nodiscard]] glm::vec3 GetEyePosition() const;
|
||||
|
||||
void SetMovementSpeed(float speed) { m_WalkSpeed = speed; }
|
||||
[[nodiscard]] float GetMovementSpeed() const { return m_WalkSpeed; }
|
||||
|
||||
void SetMouseSensitivity(float sens) { m_MouseSensitivity = sens; }
|
||||
[[nodiscard]] float GetMouseSensitivity() const { return m_MouseSensitivity; }
|
||||
|
||||
[[nodiscard]] bool IsGrounded() const { return m_Grounded; }
|
||||
|
||||
[[nodiscard]] bool IsMouseCaptured() const { return InputManager::GetInstance().IsMouseCaptured(); }
|
||||
void SetMouseCaptured(bool captured);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool CheckGrounded() const;
|
||||
[[nodiscard]] glm::vec3 GetWishDirection() const;
|
||||
|
||||
Rigidbody* m_Rigidbody{nullptr};
|
||||
CapsuleCollider* m_Capsule{nullptr};
|
||||
|
||||
float m_Yaw{0.0f};
|
||||
float m_Pitch{0.0f};
|
||||
float m_MovementSpeed{5.0f};
|
||||
float m_MouseSensitivity{0.002f};
|
||||
float m_EyeHeight{1.6f};
|
||||
float m_EyeHeight{0.7f}; // above the capsule center
|
||||
|
||||
float m_WalkSpeed{5.0f};
|
||||
float m_SprintSpeed{8.5f};
|
||||
float m_GroundAcceleration{60.0f};
|
||||
float m_AirAcceleration{15.0f};
|
||||
float m_JumpHeight{1.3f};
|
||||
float m_Gravity{22.0f};
|
||||
float m_FallGravityMultiplier{1.6f};
|
||||
float m_CoyoteTime{0.1f};
|
||||
float m_JumpBufferTime{0.12f};
|
||||
|
||||
bool m_Grounded{false};
|
||||
bool m_JumpHeld{false};
|
||||
float m_CoyoteTimer{0.0f};
|
||||
float m_JumpBufferTimer{0.0f};
|
||||
|
||||
glm::vec3 m_PreviousPosition{0.0f};
|
||||
bool m_HasPreviousPosition{false};
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+166
-33
@@ -7,10 +7,12 @@
|
||||
#include <destrum/Graphics/Managers/LineRenderingManager.h>
|
||||
#include <destrum/Graphics/Managers/QuadRenderingManager.h>
|
||||
#include "glm/gtc/constants.hpp"
|
||||
#include "glm/gtc/quaternion.hpp"
|
||||
#include "glm/gtx/transform.hpp"
|
||||
#include "spdlog/spdlog.h"
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Components/Physics/BoxCollider.h>
|
||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
|
||||
@@ -53,6 +55,55 @@ namespace {
|
||||
const std::filesystem::path path{value};
|
||||
return path.is_absolute() ? path : exeDir / path;
|
||||
}
|
||||
|
||||
// Box centered on the origin. UVs are in meters, so tiling textures keep the same size on every box
|
||||
CPUMesh MakeBoxMesh(const glm::vec3& size)
|
||||
{
|
||||
struct Face {
|
||||
glm::vec3 normal;
|
||||
glm::vec3 tangent;
|
||||
};
|
||||
constexpr std::array<Face, 6> faces{{
|
||||
{{1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, -1.0f}},
|
||||
{{-1.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
|
||||
{{0.0f, 1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
|
||||
{{0.0f, -1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
|
||||
{{0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},
|
||||
{{0.0f, 0.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}},
|
||||
}};
|
||||
constexpr std::array<glm::vec2, 4> corners{{
|
||||
{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f},
|
||||
}};
|
||||
|
||||
CPUMesh mesh{};
|
||||
mesh.name = "Box";
|
||||
mesh.minPos = -size * 0.5f;
|
||||
mesh.maxPos = size * 0.5f;
|
||||
|
||||
for (const Face& face : faces) {
|
||||
const glm::vec3 bitangent = glm::cross(face.normal, face.tangent);
|
||||
const float width = glm::dot(glm::abs(face.tangent), size);
|
||||
const float height = glm::dot(glm::abs(bitangent), size);
|
||||
const auto first = static_cast<std::uint32_t>(mesh.vertices.size());
|
||||
|
||||
for (const glm::vec2& corner : corners) {
|
||||
// v runs against the bitangent so images are upright on the side faces (w = -1 to match)
|
||||
mesh.vertices.push_back(CPUMesh::Vertex{
|
||||
.position = (face.normal + face.tangent * corner.x + bitangent * corner.y) * size * 0.5f,
|
||||
.uv_x = (corner.x + 1.0f) * 0.5f * width,
|
||||
.normal = face.normal,
|
||||
.uv_y = (1.0f - corner.y) * 0.5f * height,
|
||||
.tangent = glm::vec4{face.tangent, -1.0f},
|
||||
});
|
||||
}
|
||||
|
||||
for (const std::uint32_t index : {0u, 1u, 2u, 0u, 2u, 3u}) {
|
||||
mesh.indices.push_back(first + index);
|
||||
}
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
|
||||
LightKeeper::LightKeeper() : App()
|
||||
@@ -88,12 +139,25 @@ void LightKeeper::customInit()
|
||||
camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f)));
|
||||
|
||||
auto& scene = SceneManager::GetInstance().CreateScene("Main");
|
||||
scene.GetEnvironment().fogDensity = 0.01f;
|
||||
|
||||
// Directional lights shine down their local -Z: tilt it 50 degrees down, then turn it around the up axis
|
||||
const auto sunObj = scene.CreateGameObject("Sun");
|
||||
sunObj->GetTransform().SetWorldPosition(glm::vec3(0.f, 10.f, 0.f));
|
||||
sunObj->GetTransform().SetWorldRotation(
|
||||
glm::angleAxis(glm::radians(35.f), glm::vec3{0.f, 1.f, 0.f}) *
|
||||
glm::angleAxis(glm::radians(-50.f), glm::vec3{1.f, 0.f, 0.f}));
|
||||
auto* sun = sunObj->AddComponent<LightComponent>(LightType::Directional);
|
||||
sun->SetColor(glm::vec3{1.0f, 0.95f, 0.85f});
|
||||
sun->SetIntensity(3.0f);
|
||||
sun->SetCastShadows(true);
|
||||
|
||||
// scene.Add(testCube);
|
||||
|
||||
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/skybox.jpg");
|
||||
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/mars.jpg");
|
||||
const auto skyboxID = AssetFS::GetInstance().GetCookedPathForFile("game://starmap_2020_4k.exr");
|
||||
// const auto skyboxID = AssetFS::GetInstance().GetCookedPathForFile("game://starmap_2020_4k.exr");
|
||||
const auto skyboxID = AssetFS::GetInstance().GetCookedPathForFile("engine://DJI_0001_stitch.jpg");
|
||||
//
|
||||
// const auto skyboxID = AssetFS::GetInstance().GetFullPath("engine://textures/test-skybox.png");
|
||||
//
|
||||
@@ -136,11 +200,71 @@ void LightKeeper::customInit()
|
||||
planeMeshComp->SetMaterialID(planeMaterialID);
|
||||
planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.0f, 0.f));
|
||||
planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f));
|
||||
planeObj->AddComponent<BoxCollider>(glm::vec3{10.0f, 0.5f, 10.0f});
|
||||
|
||||
auto* floorRb = planeObj->AddComponent<Rigidbody>();
|
||||
floorRb->SetType(RigidbodyType::Static);
|
||||
scene.GetPhysics().RegisterGameObject(*planeObj);
|
||||
// Kenney prototype textures, texture_01 is a 1 x 1 meter grid
|
||||
const auto addPrototypeMaterial = [&](const std::string& color, float roughness)
|
||||
{
|
||||
Material material = Material::SimpleColor(glm::vec3{1.0f}, "Prototype" + color);
|
||||
material.roughnessFactor = roughness;
|
||||
material.textureFilteringMode = TextureFilteringMode::Anisotropic;
|
||||
material.diffuseTexture = resources.loadImageFromFile(
|
||||
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://kenny/" + color + "/texture_01.png"));
|
||||
return resources.materials().addMaterial(std::move(material));
|
||||
};
|
||||
const auto createBox = [&](const std::string& name, const glm::vec3& position, const glm::vec3& size,
|
||||
MaterialID materialID, RigidbodyType type)
|
||||
{
|
||||
const auto box = scene.CreateGameObject(name);
|
||||
box->GetTransform().SetWorldPosition(position);
|
||||
|
||||
const auto boxMeshComp = box->AddComponent<MeshRendererComponent>();
|
||||
boxMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, MakeBoxMesh(size)));
|
||||
boxMeshComp->SetMaterialID(materialID);
|
||||
|
||||
box->AddComponent<BoxCollider>(size * 0.5f);
|
||||
auto* boxRb = box->AddComponent<Rigidbody>();
|
||||
boxRb->SetType(type);
|
||||
boxRb->SetMass(10.0f);
|
||||
scene.GetPhysics().RegisterGameObject(*box);
|
||||
return box;
|
||||
};
|
||||
|
||||
// The plane mesh is flat at y = 0, so the floor collider sits right below it
|
||||
const auto groundObj = scene.CreateGameObject("GroundCollider");
|
||||
groundObj->GetTransform().SetWorldPosition(glm::vec3(0.f, -1.5f, 0.f));
|
||||
groundObj->AddComponent<BoxCollider>(glm::vec3{10.0f, 0.5f, 10.0f});
|
||||
groundObj->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
|
||||
scene.GetPhysics().RegisterGameObject(*groundObj);
|
||||
|
||||
// Parkour course, every platform is reachable with a jump from the previous one
|
||||
const MaterialID platformMaterial = addPrototypeMaterial("Orange", 0.6f);
|
||||
const MaterialID goalMaterial = addPrototypeMaterial("Green", 0.6f);
|
||||
const MaterialID wallMaterial = addPrototypeMaterial("Dark", 0.9f);
|
||||
const MaterialID crateMaterial = addPrototypeMaterial("Purple", 0.35f);
|
||||
|
||||
const auto createPlatform = [&](const std::string& name, const glm::vec3& center, float top, const glm::vec3& size,
|
||||
MaterialID materialID)
|
||||
{
|
||||
const float height = top - (-1.0f);
|
||||
createBox(name, glm::vec3{center.x, top - height * 0.5f, center.z}, glm::vec3{size.x, height, size.z},
|
||||
materialID, RigidbodyType::Static);
|
||||
};
|
||||
createPlatform("Parkour_Step", {0.0f, 0.0f, 4.0f}, -0.5f, {2.0f, 0.0f, 2.0f}, platformMaterial);
|
||||
createPlatform("Parkour_Block", {0.0f, 0.0f, 7.0f}, 0.4f, {2.0f, 0.0f, 2.0f}, platformMaterial);
|
||||
createPlatform("Parkour_Pillar1", {3.5f, 0.0f, 8.5f}, 1.2f, {1.5f, 0.0f, 1.5f}, platformMaterial);
|
||||
createPlatform("Parkour_Pillar2", {6.5f, 0.0f, 7.5f}, 2.0f, {1.2f, 0.0f, 1.2f}, platformMaterial);
|
||||
createBox("Parkour_Beam", {6.5f, 1.85f, 4.0f}, {0.6f, 0.3f, 5.0f}, platformMaterial, RigidbodyType::Static);
|
||||
createPlatform("Parkour_Goal", {6.5f, 0.0f, -0.5f}, 2.8f, {2.5f, 0.0f, 2.5f}, goalMaterial);
|
||||
|
||||
createBox("Wall", {-5.0f, 0.5f, 0.0f}, {0.5f, 3.0f, 8.0f}, wallMaterial, RigidbodyType::Static);
|
||||
createBox("LowWall", {-2.5f, -0.6f, -4.0f}, {4.0f, 0.8f, 0.4f}, wallMaterial, RigidbodyType::Static);
|
||||
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
createBox("Crate" + std::to_string(i),
|
||||
{-2.5f, -0.6f + 0.81f * static_cast<float>(i), 2.0f},
|
||||
glm::vec3{0.8f}, crateMaterial, RigidbodyType::Dynamic);
|
||||
}
|
||||
|
||||
const auto helmetObj = scene.CreateGameObject("FlightHelmet");
|
||||
helmetObj->GetTransform().SetWorldPosition(glm::vec3(2.f, -1.f, 0.f));
|
||||
@@ -521,12 +645,15 @@ void LightKeeper::customInit()
|
||||
// orbit->SetPivotPosition(glm::vec3{0.0f, -0.25f, 0.0f});
|
||||
// }
|
||||
|
||||
// auto playerObj = scene.CreateGameObject("Player");
|
||||
// playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, -5.0f));
|
||||
// auto* fps = playerObj->AddComponent<FirstPersonController>();
|
||||
// m_Player = playerObj;
|
||||
// m_FPSController = fps;
|
||||
// scene.GetPhysics().RegisterGameObject(*playerObj);
|
||||
auto playerObj = scene.CreateGameObject("Player");
|
||||
playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 0.0f, -6.0f));
|
||||
playerObj->AddComponent<CapsuleCollider>(0.4f, 1.8f);
|
||||
playerObj->AddComponent<Rigidbody>();
|
||||
auto* fps = playerObj->AddComponent<FirstPersonController>();
|
||||
fps->SetYaw(glm::half_pi<float>());
|
||||
m_Player = playerObj;
|
||||
m_FPSController = fps;
|
||||
scene.GetPhysics().RegisterGameObject(*playerObj);
|
||||
|
||||
const auto texPath = AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png");
|
||||
texID = resources.loadImageFromFile(gfxDevice, texPath);
|
||||
@@ -540,16 +667,7 @@ void LightKeeper::customUpdate(float dt)
|
||||
{
|
||||
// LightKeeper owns a camera that hides App::camera; update this instance
|
||||
// after SDL events have been consumed.
|
||||
if (m_FPSController != nullptr && m_FPSController->IsMouseCaptured())
|
||||
{
|
||||
auto& playerTransform = m_Player->GetTransform();
|
||||
const glm::vec3 playerPos = playerTransform.GetWorldPosition();
|
||||
camera.SetRotation(m_FPSController->GetYaw(), m_FPSController->GetPitch());
|
||||
camera.m_position = playerPos + glm::vec3(0.0f, m_FPSController->GetEyeHeight(), 0.0f);
|
||||
camera.CalculateViewMatrix();
|
||||
camera.CalculateProjectionMatrix();
|
||||
}
|
||||
else if (getSceneCamera() == nullptr)
|
||||
if (!isPlayerViewActive() && getSceneCamera() == nullptr)
|
||||
{
|
||||
camera.Update(dt);
|
||||
}
|
||||
@@ -591,6 +709,11 @@ void LightKeeper::customUpdate(float dt)
|
||||
|
||||
}
|
||||
|
||||
bool LightKeeper::isPlayerViewActive() const
|
||||
{
|
||||
return m_FPSController != nullptr && m_FPSController->IsMouseCaptured();
|
||||
}
|
||||
|
||||
CameraComponent* LightKeeper::getSceneCamera() const
|
||||
{
|
||||
const auto& activeScenes = SceneManager::GetInstance().GetActiveScenes();
|
||||
@@ -809,19 +932,32 @@ void LightKeeper::customDraw()
|
||||
glm::vec2{100.0f, 100.0f}, glm::vec2{200.0f, 200.0f}, 0.0f,
|
||||
glm::vec4{1.0f}, texID);
|
||||
|
||||
CameraComponent* sceneCamera = getSceneCamera();
|
||||
// The player camera is placed here, after the fixed steps, so it can be interpolated between them
|
||||
if (isPlayerViewActive())
|
||||
{
|
||||
camera.SetRotation(m_FPSController->GetYaw(), m_FPSController->GetPitch());
|
||||
camera.m_position = m_FPSController->GetEyePosition();
|
||||
camera.CalculateViewMatrix();
|
||||
camera.CalculateProjectionMatrix();
|
||||
}
|
||||
|
||||
CameraComponent* sceneCamera = isPlayerViewActive() ? nullptr : getSceneCamera();
|
||||
Camera& viewCamera = sceneCamera != nullptr ? sceneCamera->GetCamera(camera.GetAspectRatio()) : camera;
|
||||
|
||||
const SceneEnvironment& environment = SceneManager::GetInstance().GetCurrentScene().GetEnvironment();
|
||||
const GameRenderer::SceneData sceneData{
|
||||
.camera = viewCamera,
|
||||
.ambientColor = environment.ambientColor,
|
||||
.ambientIntensity = environment.ambientIntensity,
|
||||
.fogColor = environment.fogColor,
|
||||
.fogDensity = environment.fogDensity,
|
||||
.skyboxRotation = environment.skyboxRotation,
|
||||
};
|
||||
|
||||
const RenderContext ctx{
|
||||
.renderer = renderer,
|
||||
.camera = viewCamera,
|
||||
.sceneData = {
|
||||
.camera = viewCamera,
|
||||
.ambientColor = glm::vec3(0.1f),
|
||||
.ambientIntensity = 0.5f,
|
||||
.fogColor = glm::vec3(0.5f),
|
||||
.fogDensity = 0.01f
|
||||
}
|
||||
.sceneData = sceneData,
|
||||
};
|
||||
|
||||
SceneManager::GetInstance().Render(ctx);
|
||||
@@ -829,10 +965,7 @@ void LightKeeper::customDraw()
|
||||
|
||||
const auto& drawImage = renderer.getDrawImage();
|
||||
|
||||
renderer.draw(
|
||||
cmd, gfxDevice, viewCamera, GameRenderer::SceneData{
|
||||
viewCamera, glm::vec3(0.1f), 0.5f, glm::vec3(0.5f), 0.01f
|
||||
});
|
||||
renderer.draw(cmd, gfxDevice, viewCamera, sceneData);
|
||||
|
||||
gfxDevice.endFrame(
|
||||
cmd, drawImage, {
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
#include <components/FirstPersonController.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <glm/gtx/norm.hpp>
|
||||
|
||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Input/InputManager.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/ObjectModel/Transform.h>
|
||||
#include <destrum/Physics/PhysicsWorld.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Util/DeltaTime.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace {
|
||||
glm::vec3 MoveTowards(const glm::vec3& current, const glm::vec3& target, float maxDelta)
|
||||
{
|
||||
const glm::vec3 delta = target - current;
|
||||
const float distance = glm::length(delta);
|
||||
if (distance <= maxDelta || distance < 1e-6f) {
|
||||
return target;
|
||||
}
|
||||
return current + delta / distance * maxDelta;
|
||||
}
|
||||
}
|
||||
|
||||
FirstPersonController::FirstPersonController(GameObject& parent)
|
||||
: Component(parent, "FirstPersonController")
|
||||
{
|
||||
@@ -26,14 +41,26 @@ void FirstPersonController::SetMouseCaptured(bool captured)
|
||||
void FirstPersonController::Start()
|
||||
{
|
||||
auto* obj = GetGameObject();
|
||||
m_Capsule = obj->AddComponent<CapsuleCollider>(0.5f, 2.0f);
|
||||
m_Rigidbody = obj->AddComponent<Rigidbody>();
|
||||
|
||||
m_Capsule = obj->GetComponent<CapsuleCollider>();
|
||||
if (m_Capsule == nullptr) {
|
||||
m_Capsule = obj->AddComponent<CapsuleCollider>(0.4f, 1.8f);
|
||||
}
|
||||
|
||||
m_Rigidbody = obj->GetComponent<Rigidbody>();
|
||||
if (m_Rigidbody == nullptr) {
|
||||
m_Rigidbody = obj->AddComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
// The controller owns gravity and friction, the physics body only resolves collisions
|
||||
m_Rigidbody->SetMass(80.0f);
|
||||
m_Rigidbody->SetFriction(0.0f);
|
||||
m_Rigidbody->SetAllowSleep(false);
|
||||
m_Rigidbody->SetUseGravity(true);
|
||||
m_Rigidbody->SetUseGravity(false);
|
||||
m_Rigidbody->SetFreezeRotation(true);
|
||||
}
|
||||
|
||||
void FirstPersonController::Update(float)
|
||||
void FirstPersonController::Update(float dt)
|
||||
{
|
||||
const auto& input = InputManager::GetInstance();
|
||||
|
||||
@@ -44,6 +71,7 @@ void FirstPersonController::Update(float)
|
||||
|
||||
if (!IsMouseCaptured())
|
||||
{
|
||||
m_JumpHeld = false;
|
||||
if (input.WasMousePressed(SDL_BUTTON_LEFT) && !ImGui::GetIO().WantCaptureMouse)
|
||||
{
|
||||
SetMouseCaptured(true);
|
||||
@@ -57,59 +85,144 @@ void FirstPersonController::Update(float)
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.WasKeyPressed(SDL_SCANCODE_SPACE) && m_Rigidbody->HasPhysicsBody())
|
||||
// Jump presses are remembered for a moment, so pressing just before landing still jumps
|
||||
m_JumpBufferTimer = std::max(m_JumpBufferTimer - dt, 0.0f);
|
||||
if (input.WasKeyPressed(SDL_SCANCODE_SPACE))
|
||||
{
|
||||
m_Rigidbody->AddImpulse(glm::vec3(0.0f, 400.0f, 0.0f));
|
||||
m_JumpBufferTimer = m_JumpBufferTime;
|
||||
}
|
||||
m_JumpHeld = input.IsKeyDown(SDL_SCANCODE_SPACE);
|
||||
|
||||
m_Yaw += static_cast<float>(input.MouseDeltaX()) * m_MouseSensitivity;
|
||||
m_Pitch -= static_cast<float>(input.MouseDeltaY()) * m_MouseSensitivity;
|
||||
m_Pitch = glm::clamp(m_Pitch, glm::radians(-89.0f), glm::radians(89.0f));
|
||||
}
|
||||
|
||||
void FirstPersonController::FixedUpdate(float)
|
||||
void FirstPersonController::FixedUpdate(float fixedDt)
|
||||
{
|
||||
if (!IsMouseCaptured() || m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody())
|
||||
if (m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody())
|
||||
return;
|
||||
|
||||
m_PreviousPosition = GetTransform().GetWorldPosition();
|
||||
m_HasPreviousPosition = true;
|
||||
|
||||
glm::vec3 velocity = m_Rigidbody->GetLinearVelocity();
|
||||
|
||||
m_Grounded = velocity.y <= 0.1f && CheckGrounded();
|
||||
m_CoyoteTimer = m_Grounded ? m_CoyoteTime : std::max(m_CoyoteTimer - fixedDt, 0.0f);
|
||||
|
||||
const auto& input = InputManager::GetInstance();
|
||||
const bool sprinting = IsMouseCaptured() && 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_JumpBufferTimer > 0.0f && m_CoyoteTimer > 0.0f)
|
||||
{
|
||||
velocity.y = std::sqrt(2.0f * m_Gravity * m_JumpHeight);
|
||||
m_JumpBufferTimer = 0.0f;
|
||||
m_CoyoteTimer = 0.0f;
|
||||
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)
|
||||
{
|
||||
gravity *= m_FallGravityMultiplier;
|
||||
}
|
||||
velocity.y -= gravity * fixedDt;
|
||||
|
||||
m_Rigidbody->SetLinearVelocity(velocity);
|
||||
}
|
||||
|
||||
bool FirstPersonController::CheckGrounded() const
|
||||
{
|
||||
const auto* world = m_Rigidbody->GetPhysicsWorld();
|
||||
if (world == nullptr || m_Capsule == nullptr)
|
||||
return false;
|
||||
|
||||
const glm::vec3 center = GetTransform().GetWorldPosition() + m_Capsule->GetCenterOffset();
|
||||
const float radius = m_Capsule->GetRadius();
|
||||
const float bottom = center.y - m_Capsule->GetHeight() * 0.5f;
|
||||
|
||||
// Rays start inside the capsule (ignoring it) so they are always above the floor, even when the solver
|
||||
// leaves the capsule sunk in a little. The outer rays catch standing on an edge.
|
||||
const float spread = radius * 0.7f;
|
||||
const std::array<glm::vec2, 5> offsets{
|
||||
glm::vec2{0.0f, 0.0f},
|
||||
glm::vec2{spread, 0.0f},
|
||||
glm::vec2{-spread, 0.0f},
|
||||
glm::vec2{0.0f, spread},
|
||||
glm::vec2{0.0f, -spread},
|
||||
};
|
||||
|
||||
constexpr float startHeight = 0.2f;
|
||||
constexpr float probeDistance = 0.1f;
|
||||
for (const glm::vec2& offset : offsets)
|
||||
{
|
||||
const glm::vec3 origin{center.x + offset.x, bottom + startHeight, center.z + offset.y};
|
||||
|
||||
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)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
glm::vec3 FirstPersonController::GetWishDirection() const
|
||||
{
|
||||
if (!IsMouseCaptured())
|
||||
return glm::vec3{0.0f};
|
||||
|
||||
const auto& input = InputManager::GetInstance();
|
||||
|
||||
const float cosY = glm::cos(m_Yaw);
|
||||
const float sinY = glm::sin(m_Yaw);
|
||||
const glm::vec3 forward{cosY, 0.0f, sinY};
|
||||
const glm::vec3 right{-sinY, 0.0f, cosY};
|
||||
const glm::vec3 forward{std::cos(m_Yaw), 0.0f, std::sin(m_Yaw)};
|
||||
const glm::vec3 right{-std::sin(m_Yaw), 0.0f, std::cos(m_Yaw)};
|
||||
|
||||
glm::vec3 moveDir(0.0f);
|
||||
if (input.IsKeyDown(SDL_SCANCODE_W)) moveDir += forward;
|
||||
if (input.IsKeyDown(SDL_SCANCODE_S)) moveDir -= forward;
|
||||
if (input.IsKeyDown(SDL_SCANCODE_D)) moveDir += right;
|
||||
if (input.IsKeyDown(SDL_SCANCODE_A)) moveDir -= right;
|
||||
glm::vec3 direction(0.0f);
|
||||
if (input.IsKeyDown(SDL_SCANCODE_W)) direction += forward;
|
||||
if (input.IsKeyDown(SDL_SCANCODE_S)) direction -= forward;
|
||||
if (input.IsKeyDown(SDL_SCANCODE_D)) direction += right;
|
||||
if (input.IsKeyDown(SDL_SCANCODE_A)) direction -= right;
|
||||
|
||||
glm::vec3 velocity(0.0f);
|
||||
if (glm::length2(moveDir) > 0.0f)
|
||||
{
|
||||
moveDir = glm::normalize(moveDir);
|
||||
velocity = moveDir * m_MovementSpeed;
|
||||
}
|
||||
return glm::length2(direction) > 0.0f ? glm::normalize(direction) : direction;
|
||||
}
|
||||
|
||||
velocity.y = m_Rigidbody->GetLinearVelocity().y;
|
||||
m_Rigidbody->SetLinearVelocity(velocity);
|
||||
|
||||
auto* world = m_Rigidbody->GetPhysicsWorld();
|
||||
if (world != nullptr)
|
||||
{
|
||||
PhysicsTransform xform = world->GetBodyTransform(m_Rigidbody->GetBody());
|
||||
xform.rotation = glm::quat(glm::vec3(0.0f, m_Yaw, 0.0f));
|
||||
world->SetBodyTransform(m_Rigidbody->GetBody(), xform);
|
||||
}
|
||||
glm::vec3 FirstPersonController::GetEyePosition() const
|
||||
{
|
||||
const glm::vec3 current = GetTransform().GetWorldPosition();
|
||||
const glm::vec3 position = m_HasPreviousPosition
|
||||
? glm::mix(m_PreviousPosition, current, Time::GetInstance().FixedAlpha())
|
||||
: current;
|
||||
return position + glm::vec3{0.0f, m_EyeHeight, 0.0f};
|
||||
}
|
||||
|
||||
void FirstPersonController::ImGuiInspector()
|
||||
{
|
||||
Component::ImGuiInspector();
|
||||
ImGui::DragFloat("Movement Speed", &m_MovementSpeed, 0.1f, 0.0f, 100.0f);
|
||||
ImGui::DragFloat("Walk Speed", &m_WalkSpeed, 0.1f, 0.0f, 100.0f);
|
||||
ImGui::DragFloat("Sprint Speed", &m_SprintSpeed, 0.1f, 0.0f, 100.0f);
|
||||
ImGui::DragFloat("Ground Acceleration", &m_GroundAcceleration, 0.5f, 0.0f, 500.0f);
|
||||
ImGui::DragFloat("Air Acceleration", &m_AirAcceleration, 0.5f, 0.0f, 500.0f);
|
||||
ImGui::DragFloat("Jump Height", &m_JumpHeight, 0.01f, 0.0f, 10.0f);
|
||||
ImGui::DragFloat("Gravity", &m_Gravity, 0.1f, 0.0f, 100.0f);
|
||||
ImGui::DragFloat("Fall Gravity Multiplier", &m_FallGravityMultiplier, 0.01f, 1.0f, 5.0f);
|
||||
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("Mouse Sensitivity", &m_MouseSensitivity, 0.0001f, 0.0f, 0.1f);
|
||||
ImGui::DragFloat("Eye Height", &m_EyeHeight, 0.01f, 0.0f, 5.0f);
|
||||
ImGui::DragFloat("Eye Height", &m_EyeHeight, 0.01f, -2.0f, 2.0f);
|
||||
ImGui::Text("Grounded: %s", m_Grounded ? "yes" : "no");
|
||||
ImGui::Text("Yaw: %.1f deg", glm::degrees(m_Yaw));
|
||||
ImGui::Text("Pitch: %.1f deg", glm::degrees(m_Pitch));
|
||||
bool captured = IsMouseCaptured();
|
||||
@@ -122,7 +235,15 @@ void FirstPersonController::ImGuiInspector()
|
||||
nlohmann::json FirstPersonController::Serialize() const
|
||||
{
|
||||
return {
|
||||
{"movementSpeed", m_MovementSpeed},
|
||||
{"walkSpeed", m_WalkSpeed},
|
||||
{"sprintSpeed", m_SprintSpeed},
|
||||
{"groundAcceleration", m_GroundAcceleration},
|
||||
{"airAcceleration", m_AirAcceleration},
|
||||
{"jumpHeight", m_JumpHeight},
|
||||
{"gravity", m_Gravity},
|
||||
{"fallGravityMultiplier", m_FallGravityMultiplier},
|
||||
{"coyoteTime", m_CoyoteTime},
|
||||
{"jumpBufferTime", m_JumpBufferTime},
|
||||
{"mouseSensitivity", m_MouseSensitivity},
|
||||
{"eyeHeight", m_EyeHeight},
|
||||
{"yaw", m_Yaw},
|
||||
@@ -132,9 +253,17 @@ nlohmann::json FirstPersonController::Serialize() const
|
||||
|
||||
void FirstPersonController::Deserialize(const nlohmann::json& data)
|
||||
{
|
||||
if (data.contains("movementSpeed")) m_MovementSpeed = data.at("movementSpeed").get<float>();
|
||||
if (data.contains("mouseSensitivity")) m_MouseSensitivity = data.at("mouseSensitivity").get<float>();
|
||||
if (data.contains("eyeHeight")) m_EyeHeight = data.at("eyeHeight").get<float>();
|
||||
if (data.contains("yaw")) m_Yaw = data.at("yaw").get<float>();
|
||||
if (data.contains("pitch")) m_Pitch = data.at("pitch").get<float>();
|
||||
}
|
||||
m_WalkSpeed = data.value("walkSpeed", data.value("movementSpeed", m_WalkSpeed));
|
||||
m_SprintSpeed = data.value("sprintSpeed", m_SprintSpeed);
|
||||
m_GroundAcceleration = data.value("groundAcceleration", m_GroundAcceleration);
|
||||
m_AirAcceleration = data.value("airAcceleration", m_AirAcceleration);
|
||||
m_JumpHeight = data.value("jumpHeight", m_JumpHeight);
|
||||
m_Gravity = data.value("gravity", m_Gravity);
|
||||
m_FallGravityMultiplier = data.value("fallGravityMultiplier", m_FallGravityMultiplier);
|
||||
m_CoyoteTime = data.value("coyoteTime", m_CoyoteTime);
|
||||
m_JumpBufferTime = data.value("jumpBufferTime", m_JumpBufferTime);
|
||||
m_MouseSensitivity = data.value("mouseSensitivity", m_MouseSensitivity);
|
||||
m_EyeHeight = data.value("eyeHeight", m_EyeHeight);
|
||||
m_Yaw = data.value("yaw", m_Yaw);
|
||||
m_Pitch = data.value("pitch", m_Pitch);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user