feat: add Activateable in Lightkeeper

This commit is contained in:
2026-10-17 17:00:00 +02:00
parent 89a168f76d
commit 70c940c476
13 changed files with 354 additions and 5 deletions
+2
View File
@@ -12,6 +12,8 @@ set(GAME_SRC
src/scenes/Experiments.cpp
src/components/GameComponentRegistry.cpp
src/components/Activatable.cpp
src/components/Door.cpp
src/components/FirstPersonController.cpp
src/components/Interactable.cpp
src/components/InteractableButton.cpp
@@ -0,0 +1,20 @@
#ifndef LIGHTKEEPER_ACTIVATABLE_H
#define LIGHTKEEPER_ACTIVATABLE_H
#include <destrum/Event.h>
#include <destrum/ObjectModel/Component.h>
class Activatable : public Component {
public:
void Activate(GameObject& instigator);
Event<GameObject&> OnActivated;
protected:
Activatable(GameObject& parent, const std::string& name)
: Component(parent, name) {}
virtual void OnActivate(GameObject& instigator) = 0;
};
#endif // LIGHTKEEPER_ACTIVATABLE_H
+50
View File
@@ -0,0 +1,50 @@
#ifndef LIGHTKEEPER_DOOR_H
#define LIGHTKEEPER_DOOR_H
#include <components/Activatable.h>
#include <glm/vec3.hpp>
class Door final : public Activatable {
public:
explicit Door(GameObject& parent);
std::string GetTypeName() const override { return "Door"; }
void Start() override;
void Update(float dt) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void Open() { m_IsOpen = true; }
void Close() { m_IsOpen = false; }
void Toggle() { m_IsOpen = !m_IsOpen; }
[[nodiscard]] bool IsOpen() const { return m_IsOpen; }
[[nodiscard]] float GetOpenAmount() const { return m_OpenAmount; }
void SetOpenOffset(const glm::vec3& offset) { m_OpenOffset = offset; }
void SetOpenDuration(float seconds) { m_OpenDuration = seconds; }
void SetToggleable(bool toggleable) { m_Toggleable = toggleable; }
protected:
void OnActivate(GameObject& instigator) override;
private:
[[nodiscard]] glm::vec3 GetOffsetFor(float openAmount) const;
void ApplyPosition();
glm::vec3 m_OpenOffset{0.0f, -2.4f, 0.0f};
float m_OpenDuration{1.0f};
bool m_Toggleable{true};
bool m_IsOpen{false};
float m_OpenAmount{0.0f};
glm::vec3 m_ClosedPosition{0.0f};
bool m_Started{false};
};
#endif // LIGHTKEEPER_DOOR_H
@@ -1,12 +1,14 @@
#ifndef LIGHTKEEPER_GAMECOMPONENTLIST_H
#define LIGHTKEEPER_GAMECOMPONENTLIST_H
#include <components/Door.h>
#include <components/FirstPersonController.h>
#include <components/InteractableButton.h>
#include <components/PrintComponent.h>
#include <components/TriggerLoggerComponent.h>
#define LIGHTKEEPER_GAME_COMPONENTS(X) \
X(Door) \
X(FirstPersonController) \
X(InteractableButton) \
X(PrintComponent) \
@@ -4,6 +4,9 @@
#include <components/Interactable.h>
#include <cstddef>
#include <vector>
#include <destrum/ObjectModel/ObjectId.h>
class InteractableButton final : public Interactable {
public:
@@ -21,11 +24,19 @@ public:
[[nodiscard]] std::size_t GetColorIndex() const { return m_ColorIndex; }
[[nodiscard]] int GetPressCount() const { return m_PressCount; }
// Every Activatable on a target object is activated when the button is pressed
void AddTarget(const GameObject& target);
void RemoveTarget(ObjectId target);
[[nodiscard]] const std::vector<ObjectId>& GetTargets() const { return m_Targets; }
private:
void ApplyColor();
void ActivateTargets(GameObject& interactor);
void DrawTargetsInspector();
std::size_t m_ColorIndex{0};
int m_PressCount{0};
std::vector<ObjectId> m_Targets;
};
#endif // LIGHTKEEPER_INTERACTABLEBUTTON_H
@@ -0,0 +1,11 @@
#include <components/Activatable.h>
void Activatable::Activate(GameObject& instigator)
{
if (!isEnabled() || IsBeingDestroyed()) {
return;
}
OnActivate(instigator);
OnActivated.Invoke(instigator);
}
+93
View File
@@ -0,0 +1,93 @@
#include <components/Door.h>
#include <algorithm>
#include <destrum/ObjectModel/GameObject.h>
#include <imgui.h>
Door::Door(GameObject& parent)
: Activatable(parent, "Door")
{
}
void Door::Start()
{
const glm::vec3 currentPosition = GetGameObject()->GetTransform().GetLocalPosition();
m_ClosedPosition = currentPosition - GetOffsetFor(m_OpenAmount);
m_Started = true;
}
void Door::Update(float dt)
{
if (!m_Started) {
return;
}
const float targetAmount = m_IsOpen ? 1.0f : 0.0f;
if (m_OpenAmount == targetAmount) {
return;
}
const float step = m_OpenDuration > 0.0f ? dt / m_OpenDuration : 1.0f;
m_OpenAmount = m_IsOpen
? std::min(m_OpenAmount + step, targetAmount)
: std::max(m_OpenAmount - step, targetAmount);
ApplyPosition();
}
void Door::OnActivate(GameObject&)
{
if (m_Toggleable) {
Toggle();
} else {
Open();
}
}
glm::vec3 Door::GetOffsetFor(float openAmount) const
{
const float eased = openAmount * openAmount * (3.0f - 2.0f * openAmount);
return m_OpenOffset * eased;
}
void Door::ApplyPosition()
{
GetGameObject()->GetTransform().SetLocalPosition(m_ClosedPosition + GetOffsetFor(m_OpenAmount));
}
nlohmann::json Door::Serialize() const
{
return {
{"openOffset", {m_OpenOffset.x, m_OpenOffset.y, m_OpenOffset.z}},
{"openDuration", m_OpenDuration},
{"toggleable", m_Toggleable},
{"open", m_IsOpen},
{"openAmount", m_OpenAmount},
};
}
void Door::Deserialize(const nlohmann::json& data)
{
if (data.contains("openOffset")) {
const auto& offset = data.at("openOffset");
m_OpenOffset = {offset.at(0).get<float>(), offset.at(1).get<float>(), offset.at(2).get<float>()};
}
m_OpenDuration = std::max(data.value("openDuration", m_OpenDuration), 0.0f);
m_Toggleable = data.value("toggleable", m_Toggleable);
m_IsOpen = data.value("open", m_IsOpen);
m_OpenAmount = std::clamp(data.value("openAmount", m_OpenAmount), 0.0f, 1.0f);
}
void Door::ImGuiInspector()
{
ImGui::Text("State: %s (%.0f%%)", m_IsOpen ? "Open" : "Closed", m_OpenAmount * 100.0f);
if (ImGui::Button(m_IsOpen ? "Close" : "Open")) {
Toggle();
}
ImGui::DragFloat3("Open Offset", &m_OpenOffset.x, 0.05f);
ImGui::DragFloat("Open Duration", &m_OpenDuration, 0.05f, 0.0f, 10.0f, "%.2f s");
ImGui::Checkbox("Toggleable", &m_Toggleable);
}
@@ -183,8 +183,6 @@ bool FirstPersonController::CheckGrounded() const
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},
@@ -1,13 +1,18 @@
#include <components/InteractableButton.h>
#include <components/Activatable.h>
#include <destrum/Components/MeshRendererComponent.h>
#include <destrum/Graphics/Renderer.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Util/GameState.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <array>
#include <optional>
#include <imgui.h>
@@ -38,6 +43,50 @@ void InteractableButton::Interact(GameObject& interactor)
ApplyColor();
spdlog::info("{} pressed {} by {}", GetGameObject()->GetName(), m_PressCount, interactor.GetName());
ActivateTargets(interactor);
}
void InteractableButton::AddTarget(const GameObject& target)
{
if (target.GetId() == InvalidObjectId || std::ranges::find(m_Targets, target.GetId()) != m_Targets.end()) {
return;
}
m_Targets.push_back(target.GetId());
}
void InteractableButton::RemoveTarget(ObjectId target)
{
std::erase(m_Targets, target);
}
void InteractableButton::ActivateTargets(GameObject& interactor)
{
const Scene* scene = GetGameObject()->GetScene();
if (scene == nullptr) {
return;
}
for (const ObjectId targetId : m_Targets) {
GameObject* target = scene->FindGameObject(targetId);
if (target == nullptr) {
spdlog::warn("{} targets object {} which no longer exists", GetGameObject()->GetName(), targetId);
continue;
}
std::vector<Activatable*> activatables;
for (const auto& component : target->GetComponents()) {
if (auto* activatable = dynamic_cast<Activatable*>(component.get())) {
activatables.push_back(activatable);
}
}
if (activatables.empty()) {
spdlog::warn("{} targets {} which has nothing to activate", GetGameObject()->GetName(), target->GetName());
}
for (Activatable* activatable : activatables) {
activatable->Activate(interactor);
}
}
}
void InteractableButton::ApplyColor()
@@ -59,6 +108,7 @@ nlohmann::json InteractableButton::Serialize() const
return {
{"colorIndex", m_ColorIndex},
{"pressCount", m_PressCount},
{"targets", m_Targets},
};
}
@@ -66,6 +116,7 @@ void InteractableButton::Deserialize(const nlohmann::json& data)
{
m_ColorIndex = data.value("colorIndex", m_ColorIndex) % ButtonColors.size();
m_PressCount = data.value("pressCount", m_PressCount);
m_Targets = data.value("targets", std::vector<ObjectId>{});
}
void InteractableButton::ImGuiInspector()
@@ -74,4 +125,53 @@ void InteractableButton::ImGuiInspector()
if (ImGui::Button("Press")) {
Interact(*GetGameObject());
}
DrawTargetsInspector();
}
void InteractableButton::DrawTargetsInspector()
{
ImGui::SeparatorText("Targets");
const Scene* scene = GetGameObject()->GetScene();
if (scene == nullptr) {
ImGui::TextDisabled("Not in a scene");
return;
}
std::optional<ObjectId> targetToRemove;
for (const ObjectId targetId : m_Targets) {
const GameObject* target = scene->FindGameObject(targetId);
ImGui::PushID(static_cast<int>(targetId));
if (ImGui::SmallButton("x")) {
targetToRemove = targetId;
}
ImGui::SameLine();
if (target != nullptr) {
ImGui::TextUnformatted(target->GetName().c_str());
} else {
ImGui::TextDisabled("Missing object %llu", static_cast<unsigned long long>(targetId));
}
ImGui::PopID();
}
if (targetToRemove) {
RemoveTarget(*targetToRemove);
}
if (ImGui::BeginCombo("##AddTarget", "Add target...")) {
for (const auto& object : scene->GetObjects()) {
const bool alreadyTargeted = object != nullptr &&
std::ranges::find(m_Targets, object->GetId()) != m_Targets.end();
if (object == nullptr || object.get() == GetGameObject() || alreadyTargeted ||
object->GetComponent<Activatable>() == nullptr) {
continue;
}
ImGui::PushID(static_cast<int>(object->GetId()));
if (ImGui::Selectable(object->GetName().c_str())) {
AddTarget(*object);
}
ImGui::PopID();
}
ImGui::EndCombo();
}
}
+30 -3
View File
@@ -16,6 +16,7 @@
#include <destrum/Util/ModelDocUtils.h>
#include <components/FirstPersonController.h>
#include <components/Door.h>
#include <components/InteractableButton.h>
#include <components/TriggerLoggerComponent.h>
@@ -159,7 +160,32 @@ namespace {
}
}
void CreateButton(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
GameObject* CreateDoorway(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID frameMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
const MaterialID doorMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
constexpr float x = 4.0f;
constexpr float z = -4.5f;
constexpr float frameHeight = 2.8f;
constexpr float doorWidth = 1.6f;
constexpr float doorHeight = 2.4f;
for (const float side : {-1.0f, 1.0f}) {
CreateBox(scene, gfxDevice, resources, side < 0.0f ? "DoorFrame_Left" : "DoorFrame_Right",
{x, GroundHeight + frameHeight * 0.5f, z + side * (doorWidth * 0.5f + 0.2f)},
{0.4f, frameHeight, 0.4f}, frameMaterial, RigidbodyType::Static);
}
CreateBox(scene, gfxDevice, resources, "DoorFrame_Top", {x, GroundHeight + frameHeight + 0.2f, z},
{0.4f, 0.4f, doorWidth + 0.8f}, frameMaterial, RigidbodyType::Static);
GameObject* door = CreateBox(scene, gfxDevice, resources, "Door", {x, GroundHeight + doorHeight * 0.5f, z},
{0.3f, doorHeight, doorWidth}, doorMaterial, RigidbodyType::Kinematic);
door->AddComponent<Door>()->SetOpenOffset({0.0f, 0.0f, -doorWidth - 0.1f});
return door;
}
void CreateButton(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const GameObject& target)
{
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},
@@ -172,7 +198,7 @@ namespace {
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>();
button->AddComponent<InteractableButton>()->AddTarget(target);
}
void CreateFlightHelmet(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
@@ -299,7 +325,8 @@ MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResour
CreateTree(scene, gfxDevice, resources);
CreateCapybara(scene, gfxDevice, resources);
CreateTriggerZone(scene);
CreateButton(scene, gfxDevice, resources);
const GameObject* door = CreateDoorway(scene, gfxDevice, resources);
CreateButton(scene, gfxDevice, resources, *door);
CreatePlayer(scene, objects);
LoadSpawnBallResources(gfxDevice, resources, objects);