feat: add basic canvas / improve inspector

This commit is contained in:
2026-10-18 20:42:04 +02:00
parent 7df65f52b5
commit ad16daea7c
47 changed files with 3142 additions and 405 deletions
+5
View File
@@ -13,14 +13,19 @@ set(GAME_SRC
src/components/GameComponentRegistry.cpp
src/components/Activatable.cpp
src/components/Checkpoint.cpp
src/components/CrosshairUI.cpp
src/components/Door.cpp
src/components/FirstPersonController.cpp
src/components/Interactable.cpp
src/components/InteractableButton.cpp
src/components/LightToggle.cpp
src/components/MovingPlatform.cpp
src/components/PlayerRespawn.cpp
src/components/PrintComponent.cpp
src/components/TriggerLoggerComponent.cpp
src/components/WinGoal.cpp
src/components/WinScreenUI.cpp
)
add_executable(lightkeeper ${GAME_SRC})
+2 -3
View File
@@ -6,10 +6,10 @@
#include <destrum/App.h>
#include <destrum/Scene/SceneManager.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Util/RectGizmo.h>
#include <destrum/Util/TransformGizmo.h>
#include "destrum/Graphics/Resources/Cubemap.h"
#include "destrum/Graphics/Resources/Font.h"
#include "destrum/ObjectModel/GameObject.h"
#include <scenes/MainScene.h>
@@ -31,7 +31,6 @@ public:
private:
void setupSkybox();
void drawCrosshair();
void updateViewMode();
[[nodiscard]] CameraComponent* getSceneCamera() const;
[[nodiscard]] Camera& getViewCamera();
@@ -59,8 +58,8 @@ private:
MainSceneObjects mainScene{};
ImageID debug2DTexture{NULL_IMAGE_ID};
Font hudFont;
TransformGizmo transformGizmo;
RectGizmo rectGizmo;
char scenePath[512]{"scenes/debug_scene.json"};
char newSceneName[128]{"EmptyScene"};
@@ -0,0 +1,27 @@
#ifndef LIGHTKEEPER_CHECKPOINT_H
#define LIGHTKEEPER_CHECKPOINT_H
#include <glm/vec3.hpp>
#include <destrum/ObjectModel/Component.h>
class Checkpoint final : public Component {
public:
explicit Checkpoint(GameObject& parent);
std::string GetTypeName() const override { return "Checkpoint"; }
void OnTriggerEnter(GameObject* other) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetSpawnOffset(const glm::vec3& offset) { m_SpawnOffset = offset; }
[[nodiscard]] glm::vec3 GetSpawnPoint() const;
private:
glm::vec3 m_SpawnOffset{0.0f};
};
#endif // LIGHTKEEPER_CHECKPOINT_H
@@ -0,0 +1,43 @@
#ifndef LIGHTKEEPER_CROSSHAIRUI_H
#define LIGHTKEEPER_CROSSHAIRUI_H
#include <glm/glm.hpp>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/ObjectRef.h>
// Shows the crosshair while the player is possessed, tints and grows it and shows the focus
// indicator while the player looks at an Interactable
class CrosshairUI final : public Component {
public:
explicit CrosshairUI(GameObject& parent);
std::string GetTypeName() const override { return "CrosshairUI"; }
void LateUpdate(float dt) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ResolveReferences(const ObjectMap& objects) override;
void ImGuiInspector() override;
void SetPlayer(const GameObject& player) { m_Player = ObjectRef(player); }
void SetCrosshair(const GameObject& crosshair) { m_Crosshair = ObjectRef(crosshair); }
void SetFocusIndicator(const GameObject& indicator) { m_FocusIndicator = ObjectRef(indicator); }
[[nodiscard]] const ObjectRef& GetPlayer() const { return m_Player; }
[[nodiscard]] const ObjectRef& GetCrosshair() const { return m_Crosshair; }
[[nodiscard]] const ObjectRef& GetFocusIndicator() const { return m_FocusIndicator; }
private:
ObjectRef m_Player;
ObjectRef m_Crosshair;
ObjectRef m_FocusIndicator;
glm::vec4 m_Color{1.0f, 1.0f, 1.0f, 0.8f};
glm::vec4 m_FocusedColor{1.0f, 0.8f, 0.25f, 1.0f};
float m_Size{10.0f};
float m_FocusedSize{16.0f};
};
#endif // LIGHTKEEPER_CROSSHAIRUI_H
@@ -39,6 +39,7 @@ public:
[[nodiscard]] float GetMouseSensitivity() const { return m_MouseSensitivity; }
[[nodiscard]] bool IsGrounded() const { return m_Grounded; }
void Teleport(const glm::vec3& worldPosition);
[[nodiscard]] Interactable* GetFocusedInteractable() const { return m_FocusedInteractable; }
[[nodiscard]] glm::vec3 GetLookDirection() const;
@@ -1,21 +1,31 @@
#ifndef LIGHTKEEPER_GAMECOMPONENTLIST_H
#define LIGHTKEEPER_GAMECOMPONENTLIST_H
#include <components/Checkpoint.h>
#include <components/CrosshairUI.h>
#include <components/Door.h>
#include <components/FirstPersonController.h>
#include <components/InteractableButton.h>
#include <components/LightToggle.h>
#include <components/MovingPlatform.h>
#include <components/PlayerRespawn.h>
#include <components/PrintComponent.h>
#include <components/TriggerLoggerComponent.h>
#include <components/WinGoal.h>
#include <components/WinScreenUI.h>
#define LIGHTKEEPER_GAME_COMPONENTS(X) \
X(Checkpoint) \
X(CrosshairUI) \
X(Door) \
X(FirstPersonController) \
X(InteractableButton) \
X(LightToggle) \
X(MovingPlatform) \
X(PlayerRespawn) \
X(PrintComponent) \
X(TriggerLoggerComponent)
X(TriggerLoggerComponent) \
X(WinGoal) \
X(WinScreenUI)
#endif // LIGHTKEEPER_GAMECOMPONENTLIST_H
@@ -0,0 +1,35 @@
#ifndef LIGHTKEEPER_PLAYERRESPAWN_H
#define LIGHTKEEPER_PLAYERRESPAWN_H
#include <glm/vec3.hpp>
#include <destrum/ObjectModel/Component.h>
class PlayerRespawn final : public Component {
public:
explicit PlayerRespawn(GameObject& parent);
std::string GetTypeName() const override { return "PlayerRespawn"; }
void Start() override;
void FixedUpdate(float fixedDt) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetRespawnPoint(const glm::vec3& worldPosition);
[[nodiscard]] const glm::vec3& GetRespawnPoint() const { return m_RespawnPoint; }
void SetKillHeight(float height) { m_KillHeight = height; }
[[nodiscard]] float GetKillHeight() const { return m_KillHeight; }
void Respawn();
private:
glm::vec3 m_RespawnPoint{0.0f};
bool m_HasRespawnPoint{false};
float m_KillHeight{-15.0f};
};
#endif // LIGHTKEEPER_PLAYERRESPAWN_H
+26
View File
@@ -0,0 +1,26 @@
#ifndef LIGHTKEEPER_WINGOAL_H
#define LIGHTKEEPER_WINGOAL_H
#include <components/Activatable.h>
class WinGoal final : public Activatable {
public:
explicit WinGoal(GameObject& parent);
std::string GetTypeName() const override { return "WinGoal"; }
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
[[nodiscard]] bool IsReached() const { return m_Reached; }
void Reset() { m_Reached = false; }
protected:
void OnActivate(GameObject& instigator) override;
private:
bool m_Reached{false};
};
#endif // LIGHTKEEPER_WINGOAL_H
@@ -0,0 +1,32 @@
#ifndef LIGHTKEEPER_WINSCREENUI_H
#define LIGHTKEEPER_WINSCREENUI_H
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/ObjectRef.h>
// Keeps the screen object active while the goal's WinGoal is reached
class WinScreenUI final : public Component {
public:
explicit WinScreenUI(GameObject& parent);
std::string GetTypeName() const override { return "WinScreenUI"; }
void LateUpdate(float dt) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ResolveReferences(const ObjectMap& objects) override;
void ImGuiInspector() override;
void SetGoal(const GameObject& goal) { m_Goal = ObjectRef(goal); }
void SetScreen(const GameObject& screen) { m_Screen = ObjectRef(screen); }
[[nodiscard]] const ObjectRef& GetGoal() const { return m_Goal; }
[[nodiscard]] const ObjectRef& GetScreen() const { return m_Screen; }
private:
ObjectRef m_Goal;
ObjectRef m_Screen;
};
#endif // LIGHTKEEPER_WINSCREENUI_H
+1 -1
View File
@@ -19,7 +19,7 @@ struct MainSceneObjects {
MaterialID sphereMaterial{};
};
// Sun, ground, parkour course, models, trigger zone and the player
// Sun, ground, parkour course, models, trigger zone, the player and the HUD
MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources);
#endif // LIGHTKEEPER_MAINSCENE_H
-39
View File
@@ -2,11 +2,9 @@
#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()
@@ -43,9 +41,6 @@ void LightKeeper::customInit()
debug2DTexture = resources.loadImageFromFile(
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png"));
hudFont = Font::LoadFromFile(AssetFS::GetInstance().GetFullPath("engine://fonts/Roboto-Medium.ttf"));
hudFont.Upload(gfxDevice, resources);
}
void LightKeeper::setupSkybox()
@@ -103,39 +98,6 @@ void LightKeeper::updateViewMode()
}
}
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);
constexpr std::string_view prompt = "[E] Interact";
constexpr float promptSize = 22.0f;
const glm::vec2 promptExtent = hudFont.MeasureText(prompt, promptSize);
renderer2D.DrawText(
hudFont,
prompt,
center + glm::vec2{-promptExtent.x * 0.5f, 28.0f},
promptSize,
TextStyle{.color = color, .outline = 2.0f, .outlineColor = {0.0f, 0.0f, 0.0f, 0.75f}});
}
}
bool LightKeeper::isPlayerViewActive() const
{
return mainScene.playerController != nullptr && mainScene.playerController->IsPossessed();
@@ -165,7 +127,6 @@ void LightKeeper::customDraw()
renderer.beginDrawing(gfxDevice);
submitDebugDraws();
drawCrosshair();
if (isPlayerViewActive())
{
+6
View File
@@ -190,6 +190,12 @@ void LightKeeper::drawSelectionGizmo()
return;
}
if (RectGizmo::CanManipulate(*selected)) {
const VkExtent3D extent = renderer.getDrawImage().extent;
rectGizmo.Manipulate(*selected, glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)});
return;
}
transformGizmo.HandleShortcuts();
transformGizmo.DrawToolbar();
transformGizmo.Manipulate(*selected, getViewCamera());
+51
View File
@@ -0,0 +1,51 @@
#include <components/Checkpoint.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Serialization/GlmJson.h>
#include <components/PlayerRespawn.h>
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
#include <spdlog/spdlog.h>
Checkpoint::Checkpoint(GameObject& parent)
: Component(parent, "Checkpoint")
{
}
glm::vec3 Checkpoint::GetSpawnPoint() const
{
return GetTransform().GetWorldPosition() + m_SpawnOffset;
}
void Checkpoint::OnTriggerEnter(GameObject* other)
{
if (other == nullptr) {
return;
}
auto* respawn = other->GetComponent<PlayerRespawn>();
const glm::vec3 spawnPoint = GetSpawnPoint();
if (respawn == nullptr || respawn->GetRespawnPoint() == spawnPoint) {
return;
}
respawn->SetRespawnPoint(spawnPoint);
spdlog::info("{} reached checkpoint {}", other->GetName(), GetGameObject()->GetName());
}
nlohmann::json Checkpoint::Serialize() const
{
return {{"spawnOffset", m_SpawnOffset}};
}
void Checkpoint::Deserialize(const nlohmann::json& data)
{
m_SpawnOffset = data.value("spawnOffset", m_SpawnOffset);
}
void Checkpoint::ImGuiInspector()
{
ImGui::DragFloat3("Spawn offset", glm::value_ptr(m_SpawnOffset), 0.05f);
}
+107
View File
@@ -0,0 +1,107 @@
#include <components/CrosshairUI.h>
#include <components/FirstPersonController.h>
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/Components/UI/UIGraphic.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/GlmJson.h>
#include <destrum/Util/ImGuiUtils.h>
#include <imgui.h>
namespace {
void TintHierarchy(GameObject& object, const glm::vec4& color)
{
for (const auto& component : object.GetComponents()) {
if (auto* graphic = dynamic_cast<UIGraphic*>(component.get())) {
graphic->SetColor(color);
}
}
for (Transform* child : object.GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
TintHierarchy(*child->GetOwner(), color);
}
}
}
}
CrosshairUI::CrosshairUI(GameObject& parent)
: Component(parent, "CrosshairUI")
{
}
void CrosshairUI::LateUpdate(float)
{
const Scene* scene = GetGameObject()->GetScene();
if (scene == nullptr) {
return;
}
GameObject* player = m_Player.Get(*scene);
const auto* controller = player != nullptr ? player->GetComponent<FirstPersonController>() : nullptr;
const bool possessed = controller != nullptr && controller->IsPossessed();
const bool focused = possessed && controller->GetFocusedInteractable() != nullptr;
if (GameObject* indicator = m_FocusIndicator.Get(*scene)) {
indicator->SetActive(focused);
}
GameObject* crosshair = m_Crosshair.Get(*scene);
if (crosshair == nullptr) {
return;
}
crosshair->SetActive(possessed);
if (auto* rectTransform = crosshair->GetComponent<RectTransform>()) {
rectTransform->SetSize(glm::vec2{focused ? m_FocusedSize : m_Size});
}
TintHierarchy(*crosshair, focused ? m_FocusedColor : m_Color);
}
nlohmann::json CrosshairUI::Serialize() const
{
return {
{"player", m_Player},
{"crosshair", m_Crosshair},
{"focusIndicator", m_FocusIndicator},
{"color", m_Color},
{"focusedColor", m_FocusedColor},
{"size", m_Size},
{"focusedSize", m_FocusedSize},
};
}
void CrosshairUI::Deserialize(const nlohmann::json& data)
{
m_Player = data.value("player", ObjectRef{});
m_Crosshair = data.value("crosshair", ObjectRef{});
m_FocusIndicator = data.value("focusIndicator", ObjectRef{});
m_Color = data.value("color", m_Color);
m_FocusedColor = data.value("focusedColor", m_FocusedColor);
m_Size = data.value("size", m_Size);
m_FocusedSize = data.value("focusedSize", m_FocusedSize);
}
void CrosshairUI::ResolveReferences(const ObjectMap& objects)
{
m_Player.Resolve(objects);
m_Crosshair.Resolve(objects);
m_FocusIndicator.Resolve(objects);
}
void CrosshairUI::ImGuiInspector()
{
if (const Scene* scene = GetGameObject()->GetScene()) {
ImGuiUtils::DrawObjectRefPicker("Player", m_Player, *scene, [](const GameObject& object) {
return object.GetComponent<FirstPersonController>() != nullptr;
});
ImGuiUtils::DrawObjectRefPicker("Crosshair", m_Crosshair, *scene);
ImGuiUtils::DrawObjectRefPicker("Focus Indicator", m_FocusIndicator, *scene);
}
ImGui::ColorEdit4("Color", &m_Color.x);
ImGui::ColorEdit4("Focused Color", &m_FocusedColor.x);
ImGui::DragFloat("Size", &m_Size, 0.5f, 0.0f, 500.0f);
ImGui::DragFloat("Focused Size", &m_FocusedSize, 0.5f, 0.0f, 500.0f);
}
@@ -132,6 +132,17 @@ Interactable* FirstPersonController::FindFocusedInteractable() const
return Interactable::FindOnObjectOrParents(hit.object);
}
void FirstPersonController::Teleport(const glm::vec3& worldPosition)
{
if (m_Rigidbody != nullptr) {
m_Rigidbody->Teleport(worldPosition);
} else {
GetTransform().SetWorldPosition(worldPosition);
}
m_PreviousPosition = worldPosition;
m_HasPreviousPosition = true;
}
void FirstPersonController::FixedUpdate(float fixedDt)
{
if (m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody())
@@ -0,0 +1,73 @@
#include <components/PlayerRespawn.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Serialization/GlmJson.h>
#include <components/FirstPersonController.h>
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
PlayerRespawn::PlayerRespawn(GameObject& parent)
: Component(parent, "PlayerRespawn")
{
}
void PlayerRespawn::Start()
{
if (!m_HasRespawnPoint) {
SetRespawnPoint(GetTransform().GetWorldPosition());
}
}
void PlayerRespawn::FixedUpdate(float)
{
if (GetTransform().GetWorldPosition().y < m_KillHeight) {
Respawn();
}
}
void PlayerRespawn::SetRespawnPoint(const glm::vec3& worldPosition)
{
m_RespawnPoint = worldPosition;
m_HasRespawnPoint = true;
}
void PlayerRespawn::Respawn()
{
GameObject* object = GetGameObject();
if (auto* controller = object->GetComponent<FirstPersonController>()) {
controller->Teleport(m_RespawnPoint);
} else if (auto* rigidbody = object->GetComponent<Rigidbody>()) {
rigidbody->Teleport(m_RespawnPoint);
} else {
GetTransform().SetWorldPosition(m_RespawnPoint);
}
}
nlohmann::json PlayerRespawn::Serialize() const
{
nlohmann::json data{{"killHeight", m_KillHeight}};
if (m_HasRespawnPoint) {
data["respawnPoint"] = m_RespawnPoint;
}
return data;
}
void PlayerRespawn::Deserialize(const nlohmann::json& data)
{
m_KillHeight = data.value("killHeight", m_KillHeight);
if (data.contains("respawnPoint")) {
SetRespawnPoint(data.at("respawnPoint").get<glm::vec3>());
}
}
void PlayerRespawn::ImGuiInspector()
{
ImGui::DragFloat("Kill height", &m_KillHeight, 0.1f);
ImGui::DragFloat3("Respawn point", glm::value_ptr(m_RespawnPoint), 0.1f);
if (ImGui::Button("Respawn now")) {
Respawn();
}
}
+34
View File
@@ -0,0 +1,34 @@
#include <components/WinGoal.h>
#include <destrum/ObjectModel/GameObject.h>
#include <imgui.h>
#include <spdlog/spdlog.h>
WinGoal::WinGoal(GameObject& parent)
: Activatable(parent, "WinGoal")
{
}
void WinGoal::OnActivate(GameObject& instigator)
{
if (!m_Reached) {
spdlog::info("{} reached the goal", instigator.GetName());
}
m_Reached = true;
}
nlohmann::json WinGoal::Serialize() const
{
return {{"reached", m_Reached}};
}
void WinGoal::Deserialize(const nlohmann::json& data)
{
m_Reached = data.value("reached", m_Reached);
}
void WinGoal::ImGuiInspector()
{
ImGui::Checkbox("Reached", &m_Reached);
}
@@ -0,0 +1,62 @@
#include <components/WinScreenUI.h>
#include <components/WinGoal.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Util/ImGuiUtils.h>
WinScreenUI::WinScreenUI(GameObject& parent)
: Component(parent, "WinScreenUI")
{
}
void WinScreenUI::LateUpdate(float)
{
const Scene* scene = GetGameObject()->GetScene();
if (scene == nullptr) {
return;
}
GameObject* screen = m_Screen.Get(*scene);
if (screen == nullptr) {
return;
}
GameObject* goal = m_Goal.Get(*scene);
const auto* winGoal = goal != nullptr ? goal->GetComponent<WinGoal>() : nullptr;
screen->SetActive(winGoal != nullptr && winGoal->IsReached());
}
nlohmann::json WinScreenUI::Serialize() const
{
return {
{"goal", m_Goal},
{"screen", m_Screen},
};
}
void WinScreenUI::Deserialize(const nlohmann::json& data)
{
m_Goal = data.value("goal", ObjectRef{});
m_Screen = data.value("screen", ObjectRef{});
}
void WinScreenUI::ResolveReferences(const ObjectMap& objects)
{
m_Goal.Resolve(objects);
m_Screen.Resolve(objects);
}
void WinScreenUI::ImGuiInspector()
{
const Scene* scene = GetGameObject()->GetScene();
if (scene == nullptr) {
return;
}
ImGuiUtils::DrawObjectRefPicker("Goal", m_Goal, *scene, [](const GameObject& object) {
return object.GetComponent<WinGoal>() != nullptr;
});
ImGuiUtils::DrawObjectRefPicker("Screen", m_Screen, *scene);
}
+257
View File
@@ -7,6 +7,10 @@
#include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Components/UI/Canvas.h>
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/Components/UI/UIImage.h>
#include <destrum/Components/UI/UIText.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
@@ -16,11 +20,16 @@
#include <destrum/Util/ModelDocUtils.h>
#include <components/FirstPersonController.h>
#include <components/Checkpoint.h>
#include <components/CrosshairUI.h>
#include <components/Door.h>
#include <components/InteractableButton.h>
#include <components/LightToggle.h>
#include <components/MovingPlatform.h>
#include <components/PlayerRespawn.h>
#include <components/TriggerLoggerComponent.h>
#include <components/WinGoal.h>
#include <components/WinScreenUI.h>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/quaternion.hpp>
@@ -262,6 +271,167 @@ namespace {
{2.0f, platformHeight, platformSize.z}, rampMaterial, RigidbodyType::Static);
}
// Floats over the void past the west edge of the ground. The controller jumps 1.3 m and sprints 8.5 m/s,
// which carries about 5 m on a level jump, the hardest gaps stay a little under that
WinGoal* CreateWinCourse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
{
const MaterialID checkpointMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
const MaterialID pillarMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.6f);
const MaterialID stoneMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
const MaterialID beamMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
const MaterialID movingMaterial = AddPrototypeMaterial(gfxDevice, resources, "Light", 0.5f);
const MaterialID goalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Red", 0.6f);
constexpr float slabThickness = 0.5f;
GameObject* course = scene.CreateGameObject("WinCourse");
course->GetTransform().SetWorldPosition({-GroundHalfExtent, GroundHeight, 0.0f});
const auto addToCourse = [&](GameObject* object)
{
object->GetTransform().SetParent(&course->GetTransform(), true);
return object;
};
// Top center and half size of the last placed platform, the course runs along -X
glm::vec3 top{-GroundHalfExtent - 2.5f, GroundHeight, 0.0f};
glm::vec2 half{2.5f, 2.5f};
int platformCount = 0;
int checkpointCount = 0;
const auto slab = [&](const std::string& name, const glm::vec3& topCenter, const glm::vec2& size,
MaterialID materialID, RigidbodyType type)
{
return addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_" + name,
topCenter - glm::vec3{0.0f, slabThickness * 0.5f, 0.0f},
{size.x, slabThickness, size.y}, materialID, type));
};
// gap is the edge to edge distance along X, rise and dz move the top center
const auto next = [&](float gap, float rise, float dz, const glm::vec2& size, MaterialID materialID,
RigidbodyType type = RigidbodyType::Static)
{
top = glm::vec3{top.x - half.x - gap - size.x * 0.5f, top.y + rise, top.z + dz};
half = size * 0.5f;
return slab("Platform" + std::to_string(++platformCount), top, size, materialID, type);
};
const auto mover = [&](float gap, float rise, float dz, const glm::vec2& size, const glm::vec3& travel,
float travelDuration, float pauseDuration)
{
auto* platform = next(gap, rise, dz, size, movingMaterial, RigidbodyType::Kinematic)
->AddComponent<MovingPlatform>();
platform->SetTravelOffset(travel);
platform->SetTravelDuration(travelDuration);
platform->SetPauseDuration(pauseDuration);
};
const auto addCheckpoint = [&]()
{
GameObject* trigger = addToCourse(
scene.CreateGameObject("WinCourse_Checkpoint" + std::to_string(++checkpointCount)));
trigger->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 1.0f, 0.0f});
trigger->AddComponent<BoxCollider>(glm::vec3{half.x, 1.0f, half.y})->SetTrigger(true);
trigger->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
trigger->AddComponent<Checkpoint>()->SetSpawnOffset({0.0f, 0.2f, 0.0f});
scene.GetPhysics().RegisterGameObject(*trigger);
};
// The start pad touches the ground edge so it can be walked onto
slab("Start", top, {5.0f, 5.0f}, checkpointMaterial, RigidbodyType::Static);
addCheckpoint();
// Warm-up hops
next(2.0f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
next(2.4f, 0.5f, 1.5f, {1.5f, 1.5f}, pillarMaterial);
next(2.6f, 0.5f, -3.0f, {1.5f, 1.5f}, pillarMaterial);
next(2.8f, 0.0f, 1.5f, {1.2f, 1.2f}, pillarMaterial);
next(2.2f, -0.5f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// Zig-zag over shrinking stones
next(2.6f, 0.0f, 1.0f, {1.0f, 1.0f}, stoneMaterial);
next(2.8f, 0.4f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
next(2.8f, 0.4f, 2.0f, {0.7f, 0.7f}, stoneMaterial);
next(3.0f, 0.4f, -2.0f, {0.7f, 0.7f}, stoneMaterial);
next(3.2f, -0.8f, 1.0f, {0.7f, 0.7f}, stoneMaterial);
next(2.5f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// Offset balance beams, then long sprint jumps
next(1.2f, 0.0f, 0.0f, {6.0f, 0.4f}, beamMaterial);
next(1.4f, 0.3f, 0.9f, {5.0f, 0.35f}, beamMaterial);
next(1.4f, 0.3f, -1.8f, {4.0f, 0.3f}, beamMaterial);
next(3.6f, 0.0f, 0.9f, {1.5f, 1.5f}, pillarMaterial);
next(4.0f, -0.6f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
next(3.8f, 0.0f, 0.0f, {1.2f, 1.2f}, pillarMaterial);
next(3.0f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// A ferry over a 9 m hole, then sideways movers that drift out of sync
constexpr float ferryTravel = 9.0f;
mover(0.6f, 0.0f, 0.0f, {2.5f, 2.5f}, {-ferryTravel, 0.0f, 0.0f}, 3.5f, 1.0f);
top.x -= ferryTravel;
next(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
constexpr float sway = 3.0f;
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 2.0f, 0.3f);
top.z += sway;
mover(2.2f, 0.0f, sway, {1.5f, 1.5f}, {0.0f, 0.0f, -sway * 2.0f}, 2.6f, 0.3f);
top.z -= sway;
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 1.7f, 0.3f);
top.z += sway;
next(2.2f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// An elevator up to the last stretch
constexpr float liftHeight = 5.0f;
mover(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, {0.0f, liftHeight, 0.0f}, 3.0f, 1.5f);
top.y += liftHeight;
next(1.2f, 0.0f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
next(2.8f, 0.5f, 1.2f, {0.8f, 0.8f}, stoneMaterial);
next(3.0f, 0.5f, -2.4f, {0.7f, 0.7f}, stoneMaterial);
next(3.2f, 0.0f, 2.4f, {0.6f, 0.6f}, stoneMaterial);
next(3.4f, -0.4f, -2.4f, {0.6f, 0.6f}, stoneMaterial);
next(2.5f, 0.0f, 1.2f, {3.0f, 3.0f}, checkpointMaterial);
addCheckpoint();
// Final gauntlet: a fast sideways mover and three tiny stones
constexpr float fastSway = 2.5f;
mover(2.4f, 0.0f, -fastSway, {1.2f, 1.2f}, {0.0f, 0.0f, fastSway * 2.0f}, 1.6f, 0.2f);
top.z += fastSway;
next(2.6f, 0.8f, 0.0f, {1.0f, 1.0f}, stoneMaterial);
next(3.4f, 0.0f, 1.0f, {0.8f, 0.8f}, stoneMaterial);
next(3.6f, -0.3f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
GameObject* goal = next(2.5f, 0.5f, 1.0f, {4.0f, 4.0f}, goalMaterial);
goal->SetName("WinCourse_Goal");
auto* winGoal = goal->AddComponent<WinGoal>();
const glm::vec3 buttonBase = top + glm::vec3{-1.0f, 0.0f, 0.0f};
addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_ButtonPedestal",
buttonBase + glm::vec3{0.0f, 0.55f, 0.0f}, {0.4f, 1.1f, 0.4f},
beamMaterial, RigidbodyType::Static));
// Every button needs its own material, pressing it recolors the material
Material buttonMaterial = Material::SimpleColor(glm::vec3{1.0f, 0.85f, 0.2f}, "WinButtonMaterial");
buttonMaterial.roughnessFactor = 0.4f;
GameObject* button = addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_Button",
buttonBase + glm::vec3{0.0f, 1.15f, 0.0f}, {0.25f, 0.1f, 0.25f},
resources.materials().addMaterial(std::move(buttonMaterial)),
RigidbodyType::Static));
button->AddComponent<InteractableButton>()->AddTarget(*goal);
GameObject* goalLight = addToCourse(scene.CreateGameObject("WinCourse_GoalLight"));
goalLight->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 3.0f, 0.0f});
auto* light = goalLight->AddComponent<LightComponent>(LightType::Point);
light->SetColor(glm::vec3{1.0f, 0.8f, 0.3f});
light->SetIntensity(6.0f);
light->SetRange(8.0f);
return winGoal;
}
struct WallOpening {
float offset;
float width;
@@ -493,12 +663,97 @@ namespace {
// Facing the parkour course
auto* controller = playerObj->AddComponent<FirstPersonController>();
controller->SetYaw(glm::half_pi<float>());
playerObj->AddComponent<PlayerRespawn>();
scene.GetPhysics().RegisterGameObject(*playerObj);
objects.player = playerObj;
objects.playerController = controller;
}
// Fills its parent until it is given anchors or a size
RectTransform* CreateUIObject(Scene& scene, const std::string& name, GameObject& parent)
{
GameObject* object = scene.CreateGameObject(name);
object->GetTransform().SetParent(&parent.GetTransform(), false);
auto* rect = object->AddComponent<RectTransform>();
rect->Stretch();
return rect;
}
GameObject* CreateCrosshair(Scene& scene, GameObject& hud)
{
auto* crosshair = CreateUIObject(scene, "Crosshair", hud);
crosshair->SetAnchor({0.5f, 0.5f});
crosshair->SetSize({10.0f, 10.0f});
constexpr float thickness = 2.0f;
auto* horizontal = CreateUIObject(scene, "Crosshair_Horizontal", *crosshair->GetGameObject());
horizontal->SetAnchors({0.0f, 0.5f}, {1.0f, 0.5f});
horizontal->SetSize({0.0f, thickness});
horizontal->GetGameObject()->AddComponent<UIImage>();
auto* vertical = CreateUIObject(scene, "Crosshair_Vertical", *crosshair->GetGameObject());
vertical->SetAnchors({0.5f, 0.0f}, {0.5f, 1.0f});
vertical->SetSize({thickness, 0.0f});
vertical->GetGameObject()->AddComponent<UIImage>();
return crosshair->GetGameObject();
}
GameObject* CreateFocusIndicator(Scene& scene, GameObject& crosshair)
{
GameObject* indicator = CreateUIObject(scene, "FocusIndicator", crosshair)->GetGameObject();
auto* ring = CreateUIObject(scene, "FocusRing", *indicator);
ring->SetAnchor({0.5f, 0.5f});
ring->SetSize({28.0f, 28.0f});
auto* ringImage = ring->GetGameObject()->AddComponent<UIImage>();
ringImage->SetShape(UIImageShape::Ellipse);
ringImage->SetOutline(2.0f);
auto* prompt = CreateUIObject(scene, "InteractPrompt", *indicator);
prompt->SetAnchor({0.5f, 0.5f});
prompt->SetPivot({0.5f, 0.0f});
prompt->SetPosition({0.0f, 28.0f});
prompt->SetSize({400.0f, 30.0f});
auto* promptText = prompt->GetGameObject()->AddComponent<UIText>("[E] Interact", 22.0f);
promptText->SetOutline(2.0f, {0.0f, 0.0f, 0.0f, 0.75f});
promptText->SetAlignment(UITextAlign::Center, UITextAlign::Start);
return indicator;
}
GameObject* CreateWinScreen(Scene& scene, GameObject& hud)
{
GameObject* screen = CreateUIObject(scene, "WinScreen", hud)->GetGameObject();
screen->AddComponent<UIImage>(glm::vec4{0.0f, 0.0f, 0.0f, 0.35f});
auto* titleText = CreateUIObject(scene, "WinScreen_Title", *screen)->GetGameObject()->AddComponent<UIText>("YOU WIN", 195.0f);
titleText->SetColor({1.0f, 0.85f, 0.2f, 1.0f});
titleText->SetOutline(4.0f, {0.0f, 0.0f, 0.0f, 0.9f});
screen->SetActive(false);
return screen;
}
// Sizes are in 1080p pixels, the canvas scales them with the screen height
void CreateHud(Scene& scene, const GameObject& player, const GameObject& winGoal)
{
GameObject* hud = scene.CreateGameObject("HUD");
hud->AddComponent<Canvas>();
GameObject* crosshair = CreateCrosshair(scene, *hud);
const GameObject* focusIndicator = CreateFocusIndicator(scene, *crosshair);
auto* crosshairUi = hud->AddComponent<CrosshairUI>();
crosshairUi->SetPlayer(player);
crosshairUi->SetCrosshair(*crosshair);
crosshairUi->SetFocusIndicator(*focusIndicator);
auto* winScreenUi = hud->AddComponent<WinScreenUI>();
winScreenUi->SetGoal(winGoal);
winScreenUi->SetScreen(*CreateWinScreen(scene, *hud));
}
void LoadSpawnBallResources(GfxDevice& gfxDevice, RenderResources& resources, MainSceneObjects& objects)
{
const auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", StaticModelOptions());
@@ -528,7 +783,9 @@ MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResour
CreateButton(scene, gfxDevice, resources, *door);
CreateHouse(scene, gfxDevice, resources);
CreateMovingPlatform(scene, gfxDevice, resources);
const WinGoal* winGoal = CreateWinCourse(scene, gfxDevice, resources);
CreatePlayer(scene, objects);
CreateHud(scene, *objects.player, *winGoal->GetGameObject());
LoadSpawnBallResources(gfxDevice, resources, objects);
return objects;