diff --git a/destrum/assets_src/Mayan.glb b/destrum/assets_src/Mayan.glb new file mode 100644 index 0000000..d52657a Binary files /dev/null and b/destrum/assets_src/Mayan.glb differ diff --git a/destrum/assets_src/grass.jpg b/destrum/assets_src/grass.jpg new file mode 100644 index 0000000..aed4ff1 Binary files /dev/null and b/destrum/assets_src/grass.jpg differ diff --git a/destrum/include/destrum/Util/DeltaTime.h b/destrum/include/destrum/Util/DeltaTime.h index 9caf2f2..76cb86a 100644 --- a/destrum/include/destrum/Util/DeltaTime.h +++ b/destrum/include/destrum/Util/DeltaTime.h @@ -14,7 +14,6 @@ public: [[nodiscard]] double FixedDeltaTime() const; [[nodiscard]] double DeltaTime() const; - // How far the current frame is between the last two fixed steps (0..1), used to interpolate physics objects [[nodiscard]] float FixedAlpha() const { return m_FixedAlpha; } void SetFixedAlpha(float alpha) { m_FixedAlpha = alpha; } diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index 0f9efbd..914b623 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -13,6 +13,8 @@ set(GAME_SRC src/components/GameComponentRegistry.cpp src/components/FirstPersonController.cpp + src/components/Interactable.cpp + src/components/InteractableButton.cpp src/components/PrintComponent.cpp src/components/TriggerLoggerComponent.cpp ) diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index af29e91..2319efd 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -29,6 +29,7 @@ public: private: void setupSkybox(); + void drawCrosshair(); [[nodiscard]] CameraComponent* getSceneCamera() const; [[nodiscard]] bool isPlayerViewActive() const; diff --git a/lightkeeper/include/components/FirstPersonController.h b/lightkeeper/include/components/FirstPersonController.h index d2c0c67..72464f5 100644 --- a/lightkeeper/include/components/FirstPersonController.h +++ b/lightkeeper/include/components/FirstPersonController.h @@ -7,9 +7,8 @@ class Rigidbody; class CapsuleCollider; +class Interactable; -// 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); @@ -26,9 +25,9 @@ public: [[nodiscard]] float GetYaw() const { return m_Yaw; } void SetYaw(float yawRadians) { m_Yaw = yawRadians; } [[nodiscard]] float GetPitch() const { return m_Pitch; } + void SetPitch(float pitchRadians) { m_Pitch = pitchRadians; } [[nodiscard]] float GetEyeHeight() const { return m_EyeHeight; } - // 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; } @@ -39,12 +38,16 @@ public: [[nodiscard]] bool IsGrounded() const { return m_Grounded; } + [[nodiscard]] Interactable* GetFocusedInteractable() const { return m_FocusedInteractable; } + [[nodiscard]] glm::vec3 GetLookDirection() const; + [[nodiscard]] bool IsMouseCaptured() const { return InputManager::GetInstance().IsMouseCaptured(); } void SetMouseCaptured(bool captured); private: [[nodiscard]] bool CheckGrounded() const; [[nodiscard]] glm::vec3 GetWishDirection() const; + [[nodiscard]] Interactable* FindFocusedInteractable() const; Rigidbody* m_Rigidbody{nullptr}; CapsuleCollider* m_Capsule{nullptr}; @@ -63,6 +66,9 @@ private: float m_FallGravityMultiplier{1.6f}; float m_CoyoteTime{0.1f}; float m_JumpBufferTime{0.12f}; + float m_InteractDistance{3.0f}; + + Interactable* m_FocusedInteractable{nullptr}; bool m_Grounded{false}; bool m_JumpHeld{false}; diff --git a/lightkeeper/include/components/GameComponentList.h b/lightkeeper/include/components/GameComponentList.h index 57cfadc..5b8015c 100644 --- a/lightkeeper/include/components/GameComponentList.h +++ b/lightkeeper/include/components/GameComponentList.h @@ -2,11 +2,13 @@ #define LIGHTKEEPER_GAMECOMPONENTLIST_H #include +#include #include #include #define LIGHTKEEPER_GAME_COMPONENTS(X) \ X(FirstPersonController) \ + X(InteractableButton) \ X(PrintComponent) \ X(TriggerLoggerComponent) diff --git a/lightkeeper/include/components/Interactable.h b/lightkeeper/include/components/Interactable.h new file mode 100644 index 0000000..2b3c634 --- /dev/null +++ b/lightkeeper/include/components/Interactable.h @@ -0,0 +1,17 @@ +#ifndef LIGHTKEEPER_INTERACTABLE_H +#define LIGHTKEEPER_INTERACTABLE_H + +#include + +class Interactable : public Component { +public: + virtual void Interact(GameObject& interactor) = 0; + + [[nodiscard]] static Interactable* FindOnObjectOrParents(GameObject* object); + +protected: + Interactable(GameObject& parent, const std::string& name) + : Component(parent, name) {} +}; + +#endif // LIGHTKEEPER_INTERACTABLE_H diff --git a/lightkeeper/include/components/InteractableButton.h b/lightkeeper/include/components/InteractableButton.h new file mode 100644 index 0000000..cea6ad5 --- /dev/null +++ b/lightkeeper/include/components/InteractableButton.h @@ -0,0 +1,31 @@ +#ifndef LIGHTKEEPER_INTERACTABLEBUTTON_H +#define LIGHTKEEPER_INTERACTABLEBUTTON_H + +#include + +#include + +class InteractableButton final : public Interactable { +public: + explicit InteractableButton(GameObject& parent); + + std::string GetTypeName() const override { return "InteractableButton"; } + + void Start() override; + void Interact(GameObject& interactor) override; + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; + + [[nodiscard]] std::size_t GetColorIndex() const { return m_ColorIndex; } + [[nodiscard]] int GetPressCount() const { return m_PressCount; } + +private: + void ApplyColor(); + + std::size_t m_ColorIndex{0}; + int m_PressCount{0}; +}; + +#endif // LIGHTKEEPER_INTERACTABLEBUTTON_H diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 0669f83..a3ba5b5 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -2,9 +2,11 @@ #include #include +#include #include #include +#include #include LightKeeper::LightKeeper() : App() @@ -69,6 +71,29 @@ void LightKeeper::customUpdate(float dt) updateDebugUi(); } +void LightKeeper::drawCrosshair() +{ + if (!isPlayerViewActive()) { + return; + } + + const bool focused = mainScene.playerController->GetFocusedInteractable() != nullptr; + const glm::vec4 color = focused ? glm::vec4{1.0f, 0.8f, 0.25f, 1.0f} : glm::vec4{1.0f, 1.0f, 1.0f, 0.8f}; + const float armLength = focused ? 16.0f : 10.0f; + constexpr float thickness = 2.0f; + + const VkExtent3D extent = renderer.getDrawImage().extent; + const glm::vec2 center{static_cast(extent.width) * 0.5f, static_cast(extent.height) * 0.5f}; + + auto& renderer2D = Renderer2D::GetInstance(); + const float halfArm = armLength * 0.5f; + renderer2D.DrawLine(center - glm::vec2{halfArm, 0.0f}, center + glm::vec2{halfArm, 0.0f}, thickness, color); + renderer2D.DrawLine(center - glm::vec2{0.0f, halfArm}, center + glm::vec2{0.0f, halfArm}, thickness, color); + if (focused) { + renderer2D.DrawEllipseOutline(center, glm::vec2{14.0f}, thickness, color); + } +} + bool LightKeeper::isPlayerViewActive() const { return mainScene.playerController != nullptr && mainScene.playerController->IsMouseCaptured(); @@ -92,8 +117,8 @@ void LightKeeper::customDraw() renderer.beginDrawing(gfxDevice); submitDebugDraws(); + drawCrosshair(); - // The player camera is placed here, after the fixed steps, so it can be interpolated between them if (isPlayerViewActive()) { camera.SetRotation(mainScene.playerController->GetYaw(), mainScene.playerController->GetPitch()); diff --git a/lightkeeper/src/components/FirstPersonController.cpp b/lightkeeper/src/components/FirstPersonController.cpp index 79b6b60..94f1d2e 100644 --- a/lightkeeper/src/components/FirstPersonController.cpp +++ b/lightkeeper/src/components/FirstPersonController.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -69,6 +70,8 @@ void FirstPersonController::Update(float dt) SetMouseCaptured(!IsMouseCaptured()); } + m_FocusedInteractable = nullptr; + if (!IsMouseCaptured()) { m_JumpHeld = false; @@ -85,7 +88,6 @@ void FirstPersonController::Update(float dt) return; } - // 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)) { @@ -96,6 +98,35 @@ void FirstPersonController::Update(float dt) 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)); + + m_FocusedInteractable = FindFocusedInteractable(); + if (m_FocusedInteractable != nullptr && input.WasKeyPressed(SDL_SCANCODE_E)) + { + m_FocusedInteractable->Interact(*GetGameObject()); + } +} + +glm::vec3 FirstPersonController::GetLookDirection() const +{ + return glm::vec3{ + std::cos(m_Yaw) * std::cos(m_Pitch), + std::sin(m_Pitch), + std::sin(m_Yaw) * std::cos(m_Pitch), + }; +} + +Interactable* FirstPersonController::FindFocusedInteractable() const +{ + if (m_Rigidbody == nullptr || m_Rigidbody->GetPhysicsWorld() == nullptr) + return nullptr; + + PhysicsRaycastHit hit{}; + if (!m_Rigidbody->GetPhysicsWorld()->Raycast( + GetEyePosition(), GetLookDirection(), m_InteractDistance, hit, m_Rigidbody->GetBody())) + { + return nullptr; + } + return Interactable::FindOnObjectOrParents(hit.object); } void FirstPersonController::FixedUpdate(float fixedDt) @@ -220,6 +251,7 @@ void FirstPersonController::ImGuiInspector() 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("Interact Distance", &m_InteractDistance, 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"); @@ -244,6 +276,7 @@ nlohmann::json FirstPersonController::Serialize() const {"fallGravityMultiplier", m_FallGravityMultiplier}, {"coyoteTime", m_CoyoteTime}, {"jumpBufferTime", m_JumpBufferTime}, + {"interactDistance", m_InteractDistance}, {"mouseSensitivity", m_MouseSensitivity}, {"eyeHeight", m_EyeHeight}, {"yaw", m_Yaw}, @@ -262,6 +295,7 @@ void FirstPersonController::Deserialize(const nlohmann::json& data) m_FallGravityMultiplier = data.value("fallGravityMultiplier", m_FallGravityMultiplier); m_CoyoteTime = data.value("coyoteTime", m_CoyoteTime); m_JumpBufferTime = data.value("jumpBufferTime", m_JumpBufferTime); + m_InteractDistance = data.value("interactDistance", m_InteractDistance); 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/Interactable.cpp b/lightkeeper/src/components/Interactable.cpp new file mode 100644 index 0000000..cccfe76 --- /dev/null +++ b/lightkeeper/src/components/Interactable.cpp @@ -0,0 +1,17 @@ +#include + +#include +#include + +Interactable* Interactable::FindOnObjectOrParents(GameObject* object) +{ + while (object != nullptr) { + if (auto* interactable = object->GetComponent(); + interactable != nullptr && interactable->isEnabled()) { + return interactable; + } + Transform* parent = object->GetTransform().GetParent(); + object = parent != nullptr ? parent->GetOwner() : nullptr; + } + return nullptr; +} diff --git a/lightkeeper/src/components/InteractableButton.cpp b/lightkeeper/src/components/InteractableButton.cpp new file mode 100644 index 0000000..390d057 --- /dev/null +++ b/lightkeeper/src/components/InteractableButton.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include +#include +#include + +#include + +#include + +#include + +namespace { + constexpr std::array ButtonColors{{ + {0.9f, 0.1f, 0.1f}, + {0.1f, 0.8f, 0.2f}, + {0.1f, 0.3f, 0.9f}, + {0.9f, 0.8f, 0.1f}, + {0.8f, 0.2f, 0.9f}, + }}; +} + +InteractableButton::InteractableButton(GameObject& parent) + : Interactable(parent, "InteractableButton") +{ +} + +void InteractableButton::Start() +{ + ApplyColor(); +} + +void InteractableButton::Interact(GameObject& interactor) +{ + m_ColorIndex = (m_ColorIndex + 1) % ButtonColors.size(); + ++m_PressCount; + ApplyColor(); + + spdlog::info("{} pressed {} by {}", GetGameObject()->GetName(), m_PressCount, interactor.GetName()); +} + +void InteractableButton::ApplyColor() +{ + const auto* meshRenderer = GetGameObject()->GetComponent(); + if (meshRenderer == nullptr || meshRenderer->GetMaterialID() == NULL_MATERIAL_ID || + !GameState::GetInstance().HasRenderer()) { + return; + } + + GameRenderer& renderer = GameState::GetInstance().Renderer(); + Material& material = renderer.getMaterialMutable(meshRenderer->GetMaterialID()); + material.baseColor = ButtonColors[m_ColorIndex]; + material.emissiveColor = ButtonColors[m_ColorIndex] * 0.4f; + renderer.updateMaterialGPU(meshRenderer->GetMaterialID()); +} + +nlohmann::json InteractableButton::Serialize() const +{ + return { + {"colorIndex", m_ColorIndex}, + {"pressCount", m_PressCount}, + }; +} + +void InteractableButton::Deserialize(const nlohmann::json& data) +{ + m_ColorIndex = data.value("colorIndex", m_ColorIndex) % ButtonColors.size(); + m_PressCount = data.value("pressCount", m_PressCount); +} + +void InteractableButton::ImGuiInspector() +{ + ImGui::Text("Presses: %d", m_PressCount); + if (ImGui::Button("Press")) { + Interact(*GetGameObject()); + } +} diff --git a/lightkeeper/src/scenes/MainScene.cpp b/lightkeeper/src/scenes/MainScene.cpp index 819bd1b..9cf939b 100644 --- a/lightkeeper/src/scenes/MainScene.cpp +++ b/lightkeeper/src/scenes/MainScene.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -158,6 +159,22 @@ namespace { } } + void CreateButton(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources) + { + const MaterialID pedestalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f); + CreateBox(scene, gfxDevice, resources, "ButtonPedestal", {1.5f, -0.45f, -4.5f}, {0.4f, 1.1f, 0.4f}, + pedestalMaterial, RigidbodyType::Static); + + // Every button needs its own material, pressing it recolors the material + Material buttonMaterial = Material::SimpleColor(glm::vec3{1.0f}, "ButtonMaterial"); + buttonMaterial.roughnessFactor = 0.4f; + const MaterialID buttonMaterialID = resources.materials().addMaterial(std::move(buttonMaterial)); + + GameObject* button = CreateBox(scene, gfxDevice, resources, "Button", {1.5f, 0.15f, -4.5f}, {0.25f, 0.1f, 0.25f}, + buttonMaterialID, RigidbodyType::Static); + button->AddComponent(); + } + void CreateFlightHelmet(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources) { GameObject* helmetObj = ModelSpawner::SpawnStaticModel( @@ -282,6 +299,7 @@ MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResour CreateTree(scene, gfxDevice, resources); CreateCapybara(scene, gfxDevice, resources); CreateTriggerZone(scene); + CreateButton(scene, gfxDevice, resources); CreatePlayer(scene, objects); LoadSpawnBallResources(gfxDevice, resources, objects);