feat: add interactable component and test button

This commit is contained in:
2026-10-16 23:25:11 +02:00
parent faba4171ec
commit 7dd5ee7903
14 changed files with 236 additions and 6 deletions
+2
View File
@@ -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
)
+1
View File
@@ -29,6 +29,7 @@ public:
private:
void setupSkybox();
void drawCrosshair();
[[nodiscard]] CameraComponent* getSceneCamera() const;
[[nodiscard]] bool isPlayerViewActive() const;
@@ -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};
@@ -2,11 +2,13 @@
#define LIGHTKEEPER_GAMECOMPONENTLIST_H
#include <components/FirstPersonController.h>
#include <components/InteractableButton.h>
#include <components/PrintComponent.h>
#include <components/TriggerLoggerComponent.h>
#define LIGHTKEEPER_GAME_COMPONENTS(X) \
X(FirstPersonController) \
X(InteractableButton) \
X(PrintComponent) \
X(TriggerLoggerComponent)
@@ -0,0 +1,17 @@
#ifndef LIGHTKEEPER_INTERACTABLE_H
#define LIGHTKEEPER_INTERACTABLE_H
#include <destrum/ObjectModel/Component.h>
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
@@ -0,0 +1,31 @@
#ifndef LIGHTKEEPER_INTERACTABLEBUTTON_H
#define LIGHTKEEPER_INTERACTABLEBUTTON_H
#include <components/Interactable.h>
#include <cstddef>
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
+26 -1
View File
@@ -2,9 +2,11 @@
#include <destrum/Components/CameraComponent.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Scene/Scene.h>
#include <components/FirstPersonController.h>
#include <components/Interactable.h>
#include <components/GameComponentRegistry.h>
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<float>(extent.width) * 0.5f, static_cast<float>(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());
@@ -1,4 +1,5 @@
#include <components/FirstPersonController.h>
#include <components/Interactable.h>
#include <glm/glm.hpp>
#include <glm/gtx/norm.hpp>
@@ -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<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));
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);
@@ -0,0 +1,17 @@
#include <components/Interactable.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
Interactable* Interactable::FindOnObjectOrParents(GameObject* object)
{
while (object != nullptr) {
if (auto* interactable = object->GetComponent<Interactable>();
interactable != nullptr && interactable->isEnabled()) {
return interactable;
}
Transform* parent = object->GetTransform().GetParent();
object = parent != nullptr ? parent->GetOwner() : nullptr;
}
return nullptr;
}
@@ -0,0 +1,78 @@
#include <components/InteractableButton.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Graphics/Renderer.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Util/GameState.h>
#include <spdlog/spdlog.h>
#include <array>
#include <imgui.h>
namespace {
constexpr std::array<glm::vec3, 5> 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<MeshRendererComponent>();
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());
}
}
+18
View File
@@ -16,6 +16,7 @@
#include <destrum/Util/ModelDocUtils.h>
#include <components/FirstPersonController.h>
#include <components/InteractableButton.h>
#include <components/TriggerLoggerComponent.h>
#include <glm/gtc/constants.hpp>
@@ -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<InteractableButton>();
}
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);