diff --git a/destrum/include/destrum/Input/InputManager.h b/destrum/include/destrum/Input/InputManager.h index 90534e7..ceb8747 100644 --- a/destrum/include/destrum/Input/InputManager.h +++ b/destrum/include/destrum/Input/InputManager.h @@ -30,8 +30,8 @@ public: int MouseX() const { return m_mouseX; } int MouseY() const { return m_mouseY; } - int MouseDeltaX() const { return m_mouseDX; } - int MouseDeltaY() const { return m_mouseDY; } + int MouseDeltaX() const { return m_mouseRelDX; } + int MouseDeltaY() const { return m_mouseRelDY; } int WheelX() const { return m_wheelX; } int WheelY() const { return m_wheelY; } @@ -81,6 +81,9 @@ public: void SetAxisDeadzone(int deadzone) { m_axisDeadzone = deadzone; } // 0..32767 + void SetMouseCaptured(bool captured); + [[nodiscard]] bool IsMouseCaptured() const { return m_MouseCaptured; } + private: // Key states std::unordered_set m_keysDown; @@ -92,10 +95,9 @@ private: std::unordered_set m_mousePressed; std::unordered_set m_mouseReleased; - // Mouse position + delta + // Mouse position + relative delta (accumulated from SDL xrel/yrel) int m_mouseX = 0, m_mouseY = 0; - int m_prevMouseX = 0, m_prevMouseY = 0; - int m_mouseDX = 0, m_mouseDY = 0; + int m_mouseRelDX = 0, m_mouseRelDY = 0; int m_wheelX = 0, m_wheelY = 0; @@ -125,6 +127,8 @@ private: int m_axisDeadzone = 8000; // typical deadzone + bool m_MouseCaptured = false; + private: bool QueryBinding(const Binding& b, ButtonState state) const; diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index e084695..2aa36d5 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -131,6 +131,12 @@ void App::run() imguiPass.handleEvent(event); + const bool mouseEvent = + event.type == SDL_MOUSEBUTTONDOWN || + event.type == SDL_MOUSEBUTTONUP || + event.type == SDL_MOUSEMOTION || + event.type == SDL_MOUSEWHEEL; + if (event.type == SDL_QUIT) { isRunning = false; @@ -149,19 +155,13 @@ void App::run() } } - const bool mouseEvent = - event.type == SDL_MOUSEBUTTONDOWN || - event.type == SDL_MOUSEBUTTONUP || - event.type == SDL_MOUSEMOTION || - event.type == SDL_MOUSEWHEEL; - const bool keyboardEvent = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP || event.type == SDL_TEXTINPUT; const bool capturedByImgui = - (mouseEvent && imguiPass.wantsMouse()) || + (mouseEvent && !InputManager::GetInstance().IsMouseCaptured() && imguiPass.wantsMouse()) || (keyboardEvent && imguiPass.wantsKeyboard()); if (!capturedByImgui) @@ -176,7 +176,7 @@ void App::run() } } - if (!isRunning) break; +if (!isRunning) break; // Consume SDL events before updating the base camera so held and // newly pressed inputs are applied in the same frame. diff --git a/destrum/src/Graphics/Camera.cpp b/destrum/src/Graphics/Camera.cpp index 0153e62..fc2238b 100644 --- a/destrum/src/Graphics/Camera.cpp +++ b/destrum/src/Graphics/Camera.cpp @@ -114,7 +114,7 @@ void Camera::SetRotation(const glm::vec2& yawPitchRadians) { SetRotation(yawPitchRadians.x, yawPitchRadians.y); } -void Camera::SetTarget(const glm::vec3& target) { +void Camera::SetTarget(const glm::vec3&) { } void Camera::ClearTarget() { diff --git a/destrum/src/Graphics/Pipelines/ImguiPass.cpp b/destrum/src/Graphics/Pipelines/ImguiPass.cpp index 5a54a80..3977eb3 100644 --- a/destrum/src/Graphics/Pipelines/ImguiPass.cpp +++ b/destrum/src/Graphics/Pipelines/ImguiPass.cpp @@ -124,6 +124,11 @@ void ImguiPass::beginFrame() { ImGui_ImplVulkan_NewFrame(); ImGui_ImplSDL2_NewFrame(); + + if (SDL_GetRelativeMouseMode() == SDL_TRUE) { + ImGui::GetIO().MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + } + ImGui::NewFrame(); frameBegun = true; diff --git a/destrum/src/Input/InputManager.cpp b/destrum/src/Input/InputManager.cpp index 1323765..6eddae3 100644 --- a/destrum/src/Input/InputManager.cpp +++ b/destrum/src/Input/InputManager.cpp @@ -46,11 +46,11 @@ void InputManager::BeginFrame() { m_textInput.clear(); - // Update mouse delta based on last known position - m_mouseDX = m_mouseX - m_prevMouseX; - m_mouseDY = m_mouseY - m_prevMouseY; - m_prevMouseX = m_mouseX; - m_prevMouseY = m_mouseY; + // Relative mouse accumulators are filled during SDL event processing + // and consumed by game code each frame. Reset them here (before events + // are processed) so this frame accumulates fresh deltas. + m_mouseRelDX = 0; + m_mouseRelDY = 0; // Clear controller "edge" sets + snapshot axes for (auto& [id, pad]: m_pads) { @@ -60,6 +60,12 @@ void InputManager::BeginFrame() { } } +void InputManager::SetMouseCaptured(bool captured) { + if (captured == m_MouseCaptured) return; + m_MouseCaptured = captured; + SDL_SetRelativeMouseMode(captured ? SDL_TRUE : SDL_FALSE); +} + void InputManager::AddController(int deviceIndex) { if (!SDL_IsGameController(deviceIndex)) return; @@ -134,6 +140,8 @@ bool InputManager::ProcessEvent(const SDL_Event& e) { case SDL_MOUSEMOTION: { m_mouseX = e.motion.x; m_mouseY = e.motion.y; + m_mouseRelDX += e.motion.xrel; + m_mouseRelDY += e.motion.yrel; } break; diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index 83f02be..e6505f8 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -8,6 +8,7 @@ set(GAME_SRC src/Lightkeeper.cpp src/components/GameComponentRegistry.cpp + src/components/FirstPersonController.cpp src/components/PrintComponent.cpp src/components/TriggerLoggerComponent.cpp ) diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index f6324cd..b6725e6 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -10,6 +10,7 @@ #include "destrum/Graphics/Resources/Cubemap.h" #include "destrum/ObjectModel/GameObject.h" +class FirstPersonController; class LightKeeper final : public App { public: LightKeeper(); @@ -39,6 +40,8 @@ private: std::unique_ptr skyboxCubemap; GameObject* capybara = nullptr; + GameObject* m_Player = nullptr; + FirstPersonController* m_FPSController = nullptr; char scenePath[512]{"scenes/debug_scene.json"}; char newSceneName[128]{"EmptyScene"}; diff --git a/lightkeeper/include/components/FirstPersonController.h b/lightkeeper/include/components/FirstPersonController.h new file mode 100644 index 0000000..044eb25 --- /dev/null +++ b/lightkeeper/include/components/FirstPersonController.h @@ -0,0 +1,48 @@ +#ifndef LIGHTKEEPER_FIRSTPERSONCONTROLLER_H +#define LIGHTKEEPER_FIRSTPERSONCONTROLLER_H + +#include +#include +#include + +class Rigidbody; +class CapsuleCollider; + +class FirstPersonController final : public Component { +public: + explicit FirstPersonController(GameObject& parent); + + std::string GetTypeName() const override { return "FirstPersonController"; } + + void Start() override; + void Update(float dt) override; + void FixedUpdate(float fixedDt) override; + void ImGuiInspector() override; + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + + [[nodiscard]] float GetYaw() const { return m_Yaw; } + [[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; } + + void SetMouseSensitivity(float sens) { m_MouseSensitivity = sens; } + [[nodiscard]] float GetMouseSensitivity() const { return m_MouseSensitivity; } + + [[nodiscard]] bool IsMouseCaptured() const { return InputManager::GetInstance().IsMouseCaptured(); } + void SetMouseCaptured(bool captured); + +private: + 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}; +}; + +#endif \ No newline at end of file diff --git a/lightkeeper/include/components/GameComponentList.h b/lightkeeper/include/components/GameComponentList.h index ab6de98..57cfadc 100644 --- a/lightkeeper/include/components/GameComponentList.h +++ b/lightkeeper/include/components/GameComponentList.h @@ -1,10 +1,12 @@ #ifndef LIGHTKEEPER_GAMECOMPONENTLIST_H #define LIGHTKEEPER_GAMECOMPONENTLIST_H +#include #include #include #define LIGHTKEEPER_GAME_COMPONENTS(X) \ + X(FirstPersonController) \ X(PrintComponent) \ X(TriggerLoggerComponent) diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index b4d82d3..575b526 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -20,6 +20,7 @@ #include "destrum/ObjectModel/GameObject.h" #include "destrum/Util/ModelDoc.h" #include "destrum/Components/Animator.h" +#include #include @@ -446,6 +447,13 @@ void LightKeeper::customInit() triggerObj->AddComponent(); triggerObj->GetTransform().SetWorldPosition({0.0f, 0.0f, 0.0f}); + auto playerObj = scene.CreateGameObject("Player"); + playerObj->GetTransform().SetWorldPosition(glm::vec3(0.0f, 2.0f, -5.0f)); + auto* fps = playerObj->AddComponent(); + 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); @@ -455,7 +463,19 @@ void LightKeeper::customUpdate(float dt) { // LightKeeper owns a camera that hides App::camera; update this instance // after SDL events have been consumed. - camera.Update(dt); + 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 + { + camera.Update(dt); + } SceneManager::GetInstance().Update(dt); SceneManager::GetInstance().LateUpdate(dt); diff --git a/lightkeeper/src/components/FirstPersonController.cpp b/lightkeeper/src/components/FirstPersonController.cpp new file mode 100644 index 0000000..df1cb89 --- /dev/null +++ b/lightkeeper/src/components/FirstPersonController.cpp @@ -0,0 +1,140 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +FirstPersonController::FirstPersonController(GameObject& parent) + : Component(parent, "FirstPersonController") +{ +} + +void FirstPersonController::SetMouseCaptured(bool captured) +{ + InputManager::GetInstance().SetMouseCaptured(captured); +} + +void FirstPersonController::Start() +{ + auto* obj = GetGameObject(); + m_Capsule = obj->AddComponent(0.5f, 2.0f); + m_Rigidbody = obj->AddComponent(); + m_Rigidbody->SetMass(80.0f); + m_Rigidbody->SetAllowSleep(false); + m_Rigidbody->SetUseGravity(true); +} + +void FirstPersonController::Update(float) +{ + const auto& input = InputManager::GetInstance(); + + if (input.WasKeyPressed(SDL_SCANCODE_F5)) + { + SetMouseCaptured(!IsMouseCaptured()); + } + + if (!IsMouseCaptured()) + { + if (input.WasMousePressed(SDL_BUTTON_LEFT) && !ImGui::GetIO().WantCaptureMouse) + { + SetMouseCaptured(true); + } + return; + } + + if (input.WasKeyPressed(SDL_SCANCODE_ESCAPE)) + { + SetMouseCaptured(false); + return; + } + + if (input.WasKeyPressed(SDL_SCANCODE_SPACE) && m_Rigidbody->HasPhysicsBody()) + { + m_Rigidbody->AddImpulse(glm::vec3(0.0f, 400.0f, 0.0f)); + } + + m_Yaw += static_cast(input.MouseDeltaX()) * m_MouseSensitivity; + m_Pitch -= static_cast(input.MouseDeltaY()) * m_MouseSensitivity; + m_Pitch = glm::clamp(m_Pitch, glm::radians(-89.0f), glm::radians(89.0f)); +} + +void FirstPersonController::FixedUpdate(float) +{ + if (!IsMouseCaptured() || m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody()) + return; + + 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}; + + 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 velocity(0.0f); + if (glm::length2(moveDir) > 0.0f) + { + moveDir = glm::normalize(moveDir); + velocity = moveDir * m_MovementSpeed; + } + + 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); + } +} + +void FirstPersonController::ImGuiInspector() +{ + Component::ImGuiInspector(); + ImGui::DragFloat("Movement Speed", &m_MovementSpeed, 0.1f, 0.0f, 100.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::Text("Yaw: %.1f deg", glm::degrees(m_Yaw)); + ImGui::Text("Pitch: %.1f deg", glm::degrees(m_Pitch)); + bool captured = IsMouseCaptured(); + ImGui::Checkbox("Mouse Captured", &captured); + if (captured != IsMouseCaptured()) { + SetMouseCaptured(captured); + } +} + +nlohmann::json FirstPersonController::Serialize() const +{ + return { + {"movementSpeed", m_MovementSpeed}, + {"mouseSensitivity", m_MouseSensitivity}, + {"eyeHeight", m_EyeHeight}, + {"yaw", m_Yaw}, + {"pitch", m_Pitch}, + }; +} + +void FirstPersonController::Deserialize(const nlohmann::json& data) +{ + if (data.contains("movementSpeed")) m_MovementSpeed = data.at("movementSpeed").get(); + if (data.contains("mouseSensitivity")) m_MouseSensitivity = data.at("mouseSensitivity").get(); + if (data.contains("eyeHeight")) m_EyeHeight = data.at("eyeHeight").get(); + if (data.contains("yaw")) m_Yaw = data.at("yaw").get(); + if (data.contains("pitch")) m_Pitch = data.at("pitch").get(); +} \ No newline at end of file