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
+8
View File
@@ -50,6 +50,11 @@ set(SRC_FILES
"src/Components/Physics/Collider.cpp" "src/Components/Physics/Collider.cpp"
"src/Components/Physics/SphereCollider.cpp" "src/Components/Physics/SphereCollider.cpp"
"src/Components/UI/Canvas.cpp"
"src/Components/UI/RectTransform.cpp"
"src/Components/UI/UIImage.cpp"
"src/Components/UI/UIText.cpp"
"src/Graphics/BindlessSetManager.cpp" "src/Graphics/BindlessSetManager.cpp"
"src/Graphics/Camera.cpp" "src/Graphics/Camera.cpp"
"src/Graphics/DebugDraw.cpp" "src/Graphics/DebugDraw.cpp"
@@ -80,6 +85,7 @@ set(SRC_FILES
"src/Graphics/Resources/NBuffer.cpp" "src/Graphics/Resources/NBuffer.cpp"
"src/Graphics/Resources/Cubemap.cpp" "src/Graphics/Resources/Cubemap.cpp"
"src/Graphics/Resources/Font.cpp" "src/Graphics/Resources/Font.cpp"
"src/Graphics/Resources/FontCache.cpp"
"src/Graphics/Resources/PrimitiveMeshes.cpp" "src/Graphics/Resources/PrimitiveMeshes.cpp"
"src/Graphics/Pipelines/MeshPipeline.cpp" "src/Graphics/Pipelines/MeshPipeline.cpp"
@@ -117,6 +123,8 @@ set(SRC_FILES
"src/Util/DeltaTime.cpp" "src/Util/DeltaTime.cpp"
"src/Util/ImGuiUtils.cpp" "src/Util/ImGuiUtils.cpp"
"src/Util/TransformGizmo.cpp" "src/Util/TransformGizmo.cpp"
"src/Util/RectGizmo.cpp"
"src/Util/SceneInspector.cpp"
"src/Physics/PhysicsWorld.cpp" "src/Physics/PhysicsWorld.cpp"
"src/Physics/SimplePhysicsWorld.cpp" "src/Physics/SimplePhysicsWorld.cpp"
@@ -40,6 +40,7 @@ public:
void AddImpulse(const glm::vec3& impulse); void AddImpulse(const glm::vec3& impulse);
void SetLinearVelocity(const glm::vec3& velocity); void SetLinearVelocity(const glm::vec3& velocity);
void Teleport(const glm::vec3& worldPosition);
[[nodiscard]] glm::vec3 GetLinearVelocity() const; [[nodiscard]] glm::vec3 GetLinearVelocity() const;
[[nodiscard]] RigidbodyType GetType() const { return m_Type; } [[nodiscard]] RigidbodyType GetType() const { return m_Type; }
@@ -0,0 +1,52 @@
#ifndef DESTRUM_CANVAS_H
#define DESTRUM_CANVAS_H
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
enum class CanvasScaleMode {
ConstantPixelSize,
ScaleWithScreenHeight,
};
// Draws the UIGraphics on its object and every active child, depth first, so later siblings draw on top.
// A child with its own Canvas is skipped and draws itself.
class Canvas final : public Component {
public:
explicit Canvas(GameObject& parent);
std::string GetTypeName() const override { return "Canvas"; }
void Start() override;
void Render(const RenderContext& ctx) override;
void Draw(const glm::vec2& screenSize);
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
[[nodiscard]] float GetScale(const glm::vec2& screenSize) const;
[[nodiscard]] static Canvas* FindFor(GameObject& object);
void SetScaleMode(CanvasScaleMode mode) { m_ScaleMode = mode; }
void SetReferenceHeight(float height) { m_ReferenceHeight = height; }
void SetScaleFactor(float factor) { m_ScaleFactor = factor; }
void SetDrawDebugRects(bool draw) { m_DrawDebugRects = draw; }
[[nodiscard]] CanvasScaleMode GetScaleMode() const { return m_ScaleMode; }
[[nodiscard]] float GetReferenceHeight() const { return m_ReferenceHeight; }
[[nodiscard]] float GetScaleFactor() const { return m_ScaleFactor; }
private:
void DrawObject(GameObject& object, const UIRect& rect, float scale, bool visible) const;
CanvasScaleMode m_ScaleMode{CanvasScaleMode::ScaleWithScreenHeight};
float m_ReferenceHeight{1080.0f};
float m_ScaleFactor{1.0f};
bool m_DrawDebugRects{false};
};
#endif // DESTRUM_CANVAS_H
@@ -0,0 +1,68 @@
#ifndef DESTRUM_RECTTRANSFORM_H
#define DESTRUM_RECTTRANSFORM_H
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
// Screen space rectangle in draw image pixels, origin top left, y down
struct UIRect {
glm::vec2 min{0.0f};
glm::vec2 size{0.0f};
[[nodiscard]] glm::vec2 Max() const { return min + size; }
[[nodiscard]] glm::vec2 Center() const { return min + size * 0.5f; }
[[nodiscard]] bool Contains(const glm::vec2& point) const;
};
// Anchors and pivot are normalized (0,0 top left), position and size are in canvas units
class RectTransform final : public Component {
public:
explicit RectTransform(GameObject& parent);
std::string GetTypeName() const override { return "RectTransform"; }
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
[[nodiscard]] UIRect ComputeRect(const UIRect& parentRect, float scale) const;
void SetAnchors(const glm::vec2& anchorMin, const glm::vec2& anchorMax);
void SetAnchor(const glm::vec2& anchor) { SetAnchors(anchor, anchor); }
void SetPivot(const glm::vec2& pivot) { m_Pivot = pivot; }
void SetPosition(const glm::vec2& position) { m_Position = position; }
void SetSize(const glm::vec2& size) { m_Size = size; }
void Stretch();
[[nodiscard]] const glm::vec2& GetAnchorMin() const { return m_AnchorMin; }
[[nodiscard]] const glm::vec2& GetAnchorMax() const { return m_AnchorMax; }
[[nodiscard]] const glm::vec2& GetPivot() const { return m_Pivot; }
[[nodiscard]] const glm::vec2& GetPosition() const { return m_Position; }
[[nodiscard]] const glm::vec2& GetSize() const { return m_Size; }
// Where the canvas last drew this object
[[nodiscard]] const UIRect& GetScreenRect() const { return m_ScreenRect; }
[[nodiscard]] const UIRect& GetParentRect() const { return m_ParentRect; }
void SetScreenRect(const UIRect& rect, const UIRect& parentRect, float scale)
{
m_ScreenRect = rect;
m_ParentRect = parentRect;
m_LastScale = scale;
}
void ApplyAnchorPreset(const glm::vec2& preset);
private:
glm::vec2 m_AnchorMin{0.5f};
glm::vec2 m_AnchorMax{0.5f};
glm::vec2 m_Pivot{0.5f};
glm::vec2 m_Position{0.0f};
glm::vec2 m_Size{100.0f};
UIRect m_ScreenRect{};
UIRect m_ParentRect{};
float m_LastScale{1.0f};
};
#endif // DESTRUM_RECTTRANSFORM_H
@@ -0,0 +1,23 @@
#ifndef DESTRUM_UIGRAPHIC_H
#define DESTRUM_UIGRAPHIC_H
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/ObjectModel/Component.h>
#include <glm/glm.hpp>
// Anything a Canvas draws into the rect of the object it sits on
class UIGraphic : public Component {
public:
virtual void DrawUI(const UIRect& rect, float scale) = 0;
[[nodiscard]] const glm::vec4& GetColor() const { return m_Color; }
void SetColor(const glm::vec4& color) { m_Color = color; }
protected:
UIGraphic(GameObject& parent, const std::string& name) : Component(parent, name) {}
glm::vec4 m_Color{1.0f};
};
#endif // DESTRUM_UIGRAPHIC_H
@@ -0,0 +1,51 @@
#ifndef DESTRUM_UIIMAGE_H
#define DESTRUM_UIIMAGE_H
#include <string>
#include <destrum/Components/UI/UIGraphic.h>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Graphics/ids.h>
enum class UIImageShape {
Rect,
Ellipse,
};
class UIImage final : public UIGraphic {
public:
explicit UIImage(GameObject& parent, const glm::vec4& color = glm::vec4{1.0f});
std::string GetTypeName() const override { return "UIImage"; }
void DrawUI(const UIRect& rect, float scale) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetShape(UIImageShape shape) { m_Shape = shape; }
void SetOutline(float thickness) { m_Outline = thickness; }
void SetCornerRadius(float radius) { m_CornerRadius = radius; }
void SetTexture(const std::string& assetPath) { m_TexturePath = assetPath; }
void SetFilter(TextureFilter filter) { m_Filter = filter; }
[[nodiscard]] UIImageShape GetShape() const { return m_Shape; }
[[nodiscard]] float GetOutline() const { return m_Outline; }
[[nodiscard]] float GetCornerRadius() const { return m_CornerRadius; }
[[nodiscard]] const std::string& GetTexture() const { return m_TexturePath; }
private:
ImageID ResolveTexture();
UIImageShape m_Shape{UIImageShape::Rect};
float m_Outline{0.0f};
float m_CornerRadius{0.0f};
std::string m_TexturePath;
TextureFilter m_Filter{TextureFilter::Linear};
std::string m_LoadedTexturePath;
ImageID m_Texture{NULL_IMAGE_ID};
};
#endif // DESTRUM_UIIMAGE_H
@@ -0,0 +1,52 @@
#ifndef DESTRUM_UITEXT_H
#define DESTRUM_UITEXT_H
#include <string>
#include <destrum/Components/UI/UIGraphic.h>
class Font;
enum class UITextAlign {
Start,
Center,
End,
};
class UIText final : public UIGraphic {
public:
explicit UIText(GameObject& parent, std::string text = {}, float fontSize = 24.0f);
std::string GetTypeName() const override { return "UIText"; }
void DrawUI(const UIRect& rect, float scale) override;
nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetText(std::string text) { m_Text = std::move(text); }
void SetFont(std::string assetPath) { m_FontPath = std::move(assetPath); }
void SetFontSize(float size) { m_FontSize = size; }
void SetOutline(float thickness, const glm::vec4& color);
void SetAlignment(UITextAlign horizontal, UITextAlign vertical);
[[nodiscard]] const std::string& GetText() const { return m_Text; }
[[nodiscard]] const std::string& GetFont() const { return m_FontPath; }
[[nodiscard]] float GetFontSize() const { return m_FontSize; }
[[nodiscard]] UITextAlign GetHorizontalAlign() const { return m_HorizontalAlign; }
[[nodiscard]] UITextAlign GetVerticalAlign() const { return m_VerticalAlign; }
[[nodiscard]] glm::vec2 PlaceText(const UIRect& rect, const glm::vec2& textSize) const;
private:
std::string m_Text;
std::string m_FontPath;
float m_FontSize{24.0f};
float m_Outline{0.0f};
glm::vec4 m_OutlineColor{0.0f, 0.0f, 0.0f, 1.0f};
UITextAlign m_HorizontalAlign{UITextAlign::Center};
UITextAlign m_VerticalAlign{UITextAlign::Center};
};
#endif // DESTRUM_UITEXT_H
@@ -0,0 +1,29 @@
#ifndef DESTRUM_FONTCACHE_H
#define DESTRUM_FONTCACHE_H
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <destrum/Graphics/Resources/Font.h>
#include <destrum/Singleton.h>
class FontCache final : public Singleton<FontCache> {
public:
friend class Singleton<FontCache>;
static constexpr std::string_view DefaultFont = "engine://fonts/Roboto-Medium.ttf";
[[nodiscard]] const Font* Get(std::string_view assetPath);
// The atlases belong to RenderResources, call before it is cleaned up
void Clear() { fonts.clear(); }
private:
FontCache() = default;
std::unordered_map<std::string, std::unique_ptr<Font>> fonts;
};
#endif // DESTRUM_FONTCACHE_H
@@ -59,6 +59,9 @@ public:
[[nodiscard]] const std::vector<Transform*>& GetChildren() const; [[nodiscard]] const std::vector<Transform*>& GetChildren() const;
[[nodiscard]] GameObject* GetOwner() const; [[nodiscard]] GameObject* GetOwner() const;
[[nodiscard]] int GetSiblingIndex() const;
void SetSiblingIndex(int index);
private: private:
void AddChild(Transform* transform); void AddChild(Transform* transform);
void RemoveChild(Transform* transform); void RemoveChild(Transform* transform);
+10
View File
@@ -8,6 +8,7 @@
#include <destrum/Event.h> #include <destrum/Event.h>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
#include <destrum/Util/SceneInspector.h>
#include "destrum/Physics/JoltPhysicsWorld.h" #include "destrum/Physics/JoltPhysicsWorld.h"
#include "destrum/Physics/PhysicsSceneBridge.h" #include "destrum/Physics/PhysicsSceneBridge.h"
@@ -66,7 +67,15 @@ public:
[[nodiscard]] const std::vector<std::shared_ptr<GameObject>>& GetObjects() const { [[nodiscard]] const std::vector<std::shared_ptr<GameObject>>& GetObjects() const {
return m_objects; return m_objects;
} }
[[nodiscard]] const std::vector<std::shared_ptr<GameObject>>& GetPendingAdditions() const {
return m_pendingAdditions;
}
[[nodiscard]] GameObject* FindGameObject(ObjectId id) const; [[nodiscard]] GameObject* FindGameObject(ObjectId id) const;
// Reparents object (nullptr is the scene root) and puts it right before insertBefore, or last when that is
// nullptr. Also reorders the object list so the order survives a save. False if the move isn't allowed.
bool MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore = nullptr);
void SetSelectedObject(ObjectId id) { m_SelectedObjectId = id; }
[[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); } [[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); }
void SetRegisterBindings(std::function<void()> registerBindings) { void SetRegisterBindings(std::function<void()> registerBindings) {
@@ -150,6 +159,7 @@ private:
//Imgui vars //Imgui vars
bool m_ShowDemoWindow{false}; bool m_ShowDemoWindow{false};
ObjectId m_SelectedObjectId{InvalidObjectId}; ObjectId m_SelectedObjectId{InvalidObjectId};
SceneInspector m_Inspector;
ObjectId m_CurrentCameraId{InvalidObjectId}; ObjectId m_CurrentCameraId{InvalidObjectId};
@@ -12,6 +12,10 @@
#include <destrum/Components/Physics/CapsuleCollider.h> #include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.h> #include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Components/Physics/SphereCollider.h> #include <destrum/Components/Physics/SphereCollider.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>
#define DESTRUM_ENGINE_COMPONENTS(X) \ #define DESTRUM_ENGINE_COMPONENTS(X) \
X(MeshRendererComponent) \ X(MeshRendererComponent) \
@@ -25,5 +29,9 @@
X(BoxCollider) \ X(BoxCollider) \
X(SphereCollider) \ X(SphereCollider) \
X(CapsuleCollider) \ X(CapsuleCollider) \
X(Canvas) \
X(RectTransform) \
X(UIImage) \
X(UIText) \
#endif // DESTRUM_ENGINECOMPONENTLIST_H #endif // DESTRUM_ENGINECOMPONENTLIST_H
+16 -8
View File
@@ -3,14 +3,19 @@
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string>
#include <string_view> #include <string_view>
#include <vector> #include <vector>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <destrum/ObjectModel/ObjectId.h> #include <destrum/ObjectModel/ObjectId.h>
#include <destrum/ObjectModel/ObjectRef.h>
class GameObject; class GameObject;
class Scene;
using ImGuiInputTextFlags = int;
namespace ImGuiUtils { namespace ImGuiUtils {
[[nodiscard]] bool DrawVec3Control( [[nodiscard]] bool DrawVec3Control(
@@ -18,14 +23,17 @@ namespace ImGuiUtils {
glm::vec3& value, glm::vec3& value,
float speed = 0.1f); float speed = 0.1f);
void RenderSceneInspector( inline constexpr const char* GameObjectPayload = "DESTRUM_GAMEOBJECT";
std::string_view sceneName,
const std::vector<std::shared_ptr<GameObject>>& objects, bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags = 0, const char* hint = nullptr);
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions, bool InputStringMultiline(const char* label, std::string& value);
ObjectId& selectedObjectId,
const std::function<GameObject*()>& createGameObject, // Combo over the scene's objects, filter can hide objects that can't be picked
const std::function<void()>& drawSceneSettings, bool DrawObjectRefPicker(
bool* open = nullptr); const char* label,
ObjectRef& ref,
const Scene& scene,
const std::function<bool(const GameObject&)>& filter = {});
} }
#endif // DESTRUM_IMGUIUTILS_H #endif // DESTRUM_IMGUIUTILS_H
+27
View File
@@ -0,0 +1,27 @@
#ifndef DESTRUM_RECTGIZMO_H
#define DESTRUM_RECTGIZMO_H
#include <glm/glm.hpp>
class GameObject;
class RectGizmo final {
public:
[[nodiscard]] static bool CanManipulate(GameObject& object);
// drawImageSize is what the canvas renders into, it is stretched over the main ImGui viewport
bool Manipulate(GameObject& object, const glm::vec2& drawImageSize);
[[nodiscard]] bool IsUsing() const { return m_ActiveHandle >= 0; }
bool snap{false};
float snapStep{5.0f};
private:
int m_ActiveHandle{-1};
glm::vec2 m_DragStartMouse{0.0f};
glm::vec2 m_DragStartPosition{0.0f};
glm::vec2 m_DragStartSize{0.0f};
};
#endif // DESTRUM_RECTGIZMO_H
@@ -0,0 +1,60 @@
#ifndef DESTRUM_SCENEINSPECTOR_H
#define DESTRUM_SCENEINSPECTOR_H
#include <functional>
#include <string>
#include <unordered_set>
#include <nlohmann/json.hpp>
#include <destrum/ObjectModel/ObjectId.h>
class Component;
class GameObject;
class Scene;
class SceneInspector final {
public:
void Draw(Scene& scene, ObjectId& selectedObjectId, const std::function<void()>& drawSceneSettings, bool* open);
private:
struct PendingMove {
ObjectId object{InvalidObjectId};
ObjectId parent{InvalidObjectId};
ObjectId anchor{InvalidObjectId};
bool after{false};
};
void DrawHierarchy(Scene& scene, ObjectId& selectedObjectId);
void DrawNode(Scene& scene, GameObject& object, ObjectId& selectedObjectId, std::unordered_set<const GameObject*>& visited);
void DrawRenameField(GameObject& object);
void DrawNodeDragDrop(GameObject& object);
void DrawObjectMenu(Scene& scene, GameObject* object, ObjectId& selectedObjectId);
void HandleShortcuts(Scene& scene, ObjectId& selectedObjectId);
void ApplyPendingActions(Scene& scene, ObjectId& selectedObjectId);
void DrawObject(GameObject& object);
void DrawTransform(GameObject& object);
void DrawComponents(GameObject& object);
void DrawComponentMenu(GameObject& object, Component& component, std::size_t index, bool removable);
void DrawAddComponent(GameObject& object);
void BeginRename(const GameObject& object);
std::string m_Filter;
std::string m_ComponentFilter;
ObjectId m_RenamingId{InvalidObjectId};
std::string m_RenameBuffer;
bool m_FocusRename{false};
ObjectId m_OpenNextFrame{InvalidObjectId};
PendingMove m_PendingMove;
ObjectId m_PendingDelete{InvalidObjectId};
ObjectId m_PendingDuplicate{InvalidObjectId};
std::string m_ClipboardType;
nlohmann::json m_Clipboard;
};
#endif // DESTRUM_SCENEINSPECTOR_H
+2
View File
@@ -6,6 +6,7 @@
#include <destrum/App.h> #include <destrum/App.h>
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
#include <destrum/Util/DeltaTime.h> #include <destrum/Util/DeltaTime.h>
@@ -358,6 +359,7 @@ void App::cleanup()
attempt([this] { customCleanup(); }); attempt([this] { customCleanup(); });
} }
attempt([] { SceneManager::GetInstance().Destroy(); }); attempt([] { SceneManager::GetInstance().Destroy(); });
attempt([] { FontCache::GetInstance().Clear(); });
attempt([this] { renderer.cleanup(gfxDevice); }); attempt([this] { renderer.cleanup(gfxDevice); });
attempt([this] { imguiPass.cleanup(); }); attempt([this] { imguiPass.cleanup(); });
attempt([this] { resources.cleanup(gfxDevice); }); attempt([this] { resources.cleanup(gfxDevice); });
@@ -91,6 +91,17 @@ void Rigidbody::SetLinearVelocity(const glm::vec3& velocity) {
m_World->SetLinearVelocity(m_Body, velocity); m_World->SetLinearVelocity(m_Body, velocity);
} }
void Rigidbody::Teleport(const glm::vec3& worldPosition) {
Transform& transform = GetTransform();
transform.SetWorldPosition(worldPosition);
if (!HasPhysicsBody()) {
return;
}
m_World->SetBodyTransform(m_Body, PhysicsTransform{worldPosition, transform.GetWorldRotation()});
m_World->SetLinearVelocity(m_Body, glm::vec3{0.0f});
}
glm::vec3 Rigidbody::GetLinearVelocity() const { glm::vec3 Rigidbody::GetLinearVelocity() const {
if (!HasPhysicsBody()) { if (!HasPhysicsBody()) {
return glm::vec3{0.0f}; return glm::vec3{0.0f};
+147
View File
@@ -0,0 +1,147 @@
#include <destrum/Components/UI/Canvas.h>
#include <array>
#include <destrum/Components/UI/UIGraphic.h>
#include <destrum/Components/UI/UIText.h>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/ObjectModel/GameObject.h>
#include <imgui.h>
#include "tracy/Tracy.hpp"
Canvas::Canvas(GameObject& parent)
: Component(parent, "Canvas")
{
}
namespace {
void WarmFonts(const GameObject& object)
{
for (const auto& component : object.GetComponents()) {
if (const auto* text = dynamic_cast<const UIText*>(component.get())) {
(void)FontCache::GetInstance().Get(text->GetFont());
}
}
for (const Transform* child : object.GetTransform().GetChildren()) {
if (child != nullptr && child->GetOwner() != nullptr) {
WarmFonts(*child->GetOwner());
}
}
}
}
// Building a font atlas takes seconds in Debug, so do it now instead of the first time hidden text shows up
void Canvas::Start()
{
WarmFonts(*GetGameObject());
}
Canvas* Canvas::FindFor(GameObject& object)
{
for (Transform* transform = &object.GetTransform(); transform != nullptr; transform = transform->GetParent()) {
GameObject* owner = transform->GetOwner();
if (owner == nullptr) {
continue;
}
if (auto* canvas = owner->GetComponent<Canvas>()) {
return canvas;
}
}
return nullptr;
}
void Canvas::Render(const RenderContext& ctx)
{
const VkExtent3D extent = ctx.renderer.getDrawImage().extent;
Draw(glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)});
}
void Canvas::Draw(const glm::vec2& screenSize)
{
ZoneScopedN("Canvas::Draw");
const UIRect screenRect{.min = glm::vec2{0.0f}, .size = screenSize};
if (auto* rectTransform = GetGameObject()->GetComponent<RectTransform>()) {
rectTransform->SetScreenRect(screenRect, screenRect, GetScale(screenSize));
}
DrawObject(*GetGameObject(), screenRect, GetScale(screenSize), true);
}
float Canvas::GetScale(const glm::vec2& screenSize) const
{
if (m_ScaleMode == CanvasScaleMode::ScaleWithScreenHeight && m_ReferenceHeight > 0.0f) {
return screenSize.y / m_ReferenceHeight;
}
return m_ScaleFactor;
}
// Hidden objects are still laid out so the editor can show and move them
void Canvas::DrawObject(GameObject& object, const UIRect& rect, float scale, bool visible) const
{
if (visible) {
for (const auto& component : object.GetComponents()) {
auto* graphic = dynamic_cast<UIGraphic*>(component.get());
if (graphic != nullptr && graphic->isEnabled() && !graphic->IsBeingDestroyed()) {
graphic->DrawUI(rect, scale);
}
}
if (m_DrawDebugRects) {
Renderer2D::GetInstance().DrawRectOutline(rect.min, rect.size, 1.0f, glm::vec4{1.0f, 0.0f, 1.0f, 0.8f});
}
}
for (Transform* childTransform : object.GetTransform().GetChildren()) {
GameObject* child = childTransform != nullptr ? childTransform->GetOwner() : nullptr;
if (child == nullptr || child->IsBeingDestroyed() || child->HasComponent<Canvas>()) {
continue;
}
UIRect childRect = rect;
if (auto* rectTransform = child->GetComponent<RectTransform>()) {
childRect = rectTransform->ComputeRect(rect, scale);
rectTransform->SetScreenRect(childRect, rect, scale);
}
DrawObject(*child, childRect, scale, visible && child->IsActive());
}
}
nlohmann::json Canvas::Serialize() const
{
return {
{"scaleMode", m_ScaleMode == CanvasScaleMode::ConstantPixelSize ? "constantPixelSize" : "scaleWithScreenHeight"},
{"referenceHeight", m_ReferenceHeight},
{"scaleFactor", m_ScaleFactor},
{"drawDebugRects", m_DrawDebugRects},
};
}
void Canvas::Deserialize(const nlohmann::json& data)
{
m_ScaleMode = data.value("scaleMode", std::string{"scaleWithScreenHeight"}) == "constantPixelSize"
? CanvasScaleMode::ConstantPixelSize
: CanvasScaleMode::ScaleWithScreenHeight;
m_ReferenceHeight = data.value("referenceHeight", m_ReferenceHeight);
m_ScaleFactor = data.value("scaleFactor", m_ScaleFactor);
m_DrawDebugRects = data.value("drawDebugRects", m_DrawDebugRects);
}
void Canvas::ImGuiInspector()
{
constexpr std::array modeNames{"Constant Pixel Size", "Scale With Screen Height"};
int mode = static_cast<int>(m_ScaleMode);
if (ImGui::Combo("Scale Mode", &mode, modeNames.data(), static_cast<int>(modeNames.size()))) {
m_ScaleMode = static_cast<CanvasScaleMode>(mode);
}
if (m_ScaleMode == CanvasScaleMode::ScaleWithScreenHeight) {
ImGui::DragFloat("Reference Height", &m_ReferenceHeight, 1.0f, 1.0f, 8192.0f);
} else {
ImGui::DragFloat("Scale Factor", &m_ScaleFactor, 0.01f, 0.01f, 10.0f);
}
ImGui::Checkbox("Draw Debug Rects", &m_DrawDebugRects);
}
+129
View File
@@ -0,0 +1,129 @@
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/Serialization/GlmJson.h>
#include <array>
#include <imgui.h>
bool UIRect::Contains(const glm::vec2& point) const
{
const glm::vec2 max = Max();
return point.x >= min.x && point.y >= min.y && point.x <= max.x && point.y <= max.y;
}
RectTransform::RectTransform(GameObject& parent)
: Component(parent, "RectTransform")
{
}
UIRect RectTransform::ComputeRect(const UIRect& parentRect, float scale) const
{
const glm::vec2 anchorMin = parentRect.min + parentRect.size * m_AnchorMin;
const glm::vec2 anchorMax = parentRect.min + parentRect.size * m_AnchorMax;
const glm::vec2 anchorSize = anchorMax - anchorMin;
const glm::vec2 size = glm::max(anchorSize + m_Size * scale, glm::vec2{0.0f});
const glm::vec2 pivotPosition = anchorMin + anchorSize * m_Pivot + m_Position * scale;
return UIRect{.min = pivotPosition - size * m_Pivot, .size = size};
}
void RectTransform::SetAnchors(const glm::vec2& anchorMin, const glm::vec2& anchorMax)
{
m_AnchorMin = anchorMin;
m_AnchorMax = anchorMax;
}
void RectTransform::ApplyAnchorPreset(const glm::vec2& preset)
{
for (int axis = 0; axis < 2; ++axis) {
const float value = preset[axis];
if (value == 0.0f || value == 0.5f || value == 1.0f) {
m_Size[axis] = m_LastScale > 0.0f ? m_ScreenRect.size[axis] / m_LastScale : m_Size[axis];
m_AnchorMin[axis] = value;
m_AnchorMax[axis] = value;
m_Pivot[axis] = value;
} else {
m_Size[axis] = 0.0f;
m_AnchorMin[axis] = 0.0f;
m_AnchorMax[axis] = 1.0f;
m_Pivot[axis] = 0.5f;
}
m_Position[axis] = 0.0f;
}
}
void RectTransform::Stretch()
{
SetAnchors(glm::vec2{0.0f}, glm::vec2{1.0f});
m_Pivot = glm::vec2{0.5f};
m_Position = glm::vec2{0.0f};
m_Size = glm::vec2{0.0f};
}
nlohmann::json RectTransform::Serialize() const
{
return {
{"anchorMin", m_AnchorMin},
{"anchorMax", m_AnchorMax},
{"pivot", m_Pivot},
{"position", m_Position},
{"size", m_Size},
};
}
void RectTransform::Deserialize(const nlohmann::json& data)
{
m_AnchorMin = data.value("anchorMin", m_AnchorMin);
m_AnchorMax = data.value("anchorMax", m_AnchorMax);
m_Pivot = data.value("pivot", m_Pivot);
m_Position = data.value("position", m_Position);
m_Size = data.value("size", m_Size);
}
void RectTransform::ImGuiInspector()
{
constexpr float StretchPreset = -1.0f;
constexpr std::array<float, 4> presetValues{0.0f, 0.5f, 1.0f, StretchPreset};
constexpr std::array<const char*, 4> rowNames{"Top", "Middle", "Bottom", "Stretch"};
constexpr std::array<const char*, 4> columnNames{"Left", "Center", "Right", "Stretch"};
constexpr std::array<char, 4> rowLetters{'T', 'M', 'B', 'S'};
constexpr std::array<char, 4> columnLetters{'L', 'C', 'R', 'S'};
const auto matches = [this](int axis, float value) {
return value == StretchPreset ? m_AnchorMin[axis] == 0.0f && m_AnchorMax[axis] == 1.0f
: m_AnchorMin[axis] == value && m_AnchorMax[axis] == value;
};
ImGui::TextDisabled("Anchor presets");
for (std::size_t row = 0; row < presetValues.size(); ++row) {
for (std::size_t column = 0; column < presetValues.size(); ++column) {
if (column > 0) {
ImGui::SameLine();
}
const bool selected = matches(0, presetValues[column]) && matches(1, presetValues[row]);
const char label[] = {rowLetters[row], columnLetters[column], '\0'};
ImGui::PushID(static_cast<int>(row * presetValues.size() + column));
if (selected) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
}
if (ImGui::Button(label, ImVec2{28.0f, 0.0f})) {
ApplyAnchorPreset({presetValues[column], presetValues[row]});
}
if (selected) {
ImGui::PopStyleColor();
}
ImGui::SetItemTooltip("%s %s", rowNames[row], columnNames[column]);
ImGui::PopID();
}
}
ImGui::DragFloat2("Anchor Min", &m_AnchorMin.x, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat2("Anchor Max", &m_AnchorMax.x, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat2("Pivot", &m_Pivot.x, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat2("Position", &m_Position.x, 1.0f);
ImGui::DragFloat2("Size", &m_Size.x, 1.0f);
ImGui::TextDisabled("Screen: %.0f, %.0f %.0f x %.0f",
m_ScreenRect.min.x, m_ScreenRect.min.y, m_ScreenRect.size.x, m_ScreenRect.size.y);
}
+108
View File
@@ -0,0 +1,108 @@
#include <destrum/Components/UI/UIImage.h>
#include <array>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Renderer.h>
#include <destrum/Serialization/GlmJson.h>
#include <destrum/Util/GameState.h>
#include <destrum/Util/ImGuiUtils.h>
#include <imgui.h>
#include <spdlog/spdlog.h>
UIImage::UIImage(GameObject& parent, const glm::vec4& color)
: UIGraphic(parent, "UIImage")
{
m_Color = color;
}
void UIImage::DrawUI(const UIRect& rect, float scale)
{
if (rect.size.x <= 0.0f || rect.size.y <= 0.0f || m_Color.w <= 0.0f) {
return;
}
const ShapeStyle style{
.color = m_Color,
.outline = m_Outline * scale,
.cornerRadius = m_CornerRadius * scale,
.texture = {.id = ResolveTexture(), .filter = m_Filter},
};
auto& renderer2D = Renderer2D::GetInstance();
if (m_Shape == UIImageShape::Ellipse) {
renderer2D.DrawEllipse(rect.Center(), rect.size * 0.5f, style);
} else {
renderer2D.DrawRect(rect.min, rect.size, style);
}
}
ImageID UIImage::ResolveTexture()
{
if (m_TexturePath == m_LoadedTexturePath) {
return m_Texture;
}
auto& gameState = GameState::GetInstance();
if (m_TexturePath.empty()) {
m_Texture = NULL_IMAGE_ID;
} else if (gameState.HasGfxDevice() && gameState.HasRenderer() && gameState.Renderer().getResources() != nullptr) {
try {
m_Texture = gameState.Renderer().getResources()->loadImageFromFile(
gameState.Gfx(), AssetFS::GetInstance().GetFullPath(m_TexturePath));
} catch (const std::exception& exception) {
spdlog::warn("UIImage: failed to load texture '{}': {}", m_TexturePath, exception.what());
m_Texture = NULL_IMAGE_ID;
}
} else {
return NULL_IMAGE_ID;
}
m_LoadedTexturePath = m_TexturePath;
return m_Texture;
}
nlohmann::json UIImage::Serialize() const
{
return {
{"color", m_Color},
{"shape", m_Shape == UIImageShape::Ellipse ? "ellipse" : "rect"},
{"outline", m_Outline},
{"cornerRadius", m_CornerRadius},
{"texture", m_TexturePath},
{"filter", m_Filter == TextureFilter::Nearest ? "nearest" : "linear"},
};
}
void UIImage::Deserialize(const nlohmann::json& data)
{
m_Color = data.value("color", m_Color);
m_Shape = data.value("shape", std::string{"rect"}) == "ellipse" ? UIImageShape::Ellipse : UIImageShape::Rect;
m_Outline = data.value("outline", m_Outline);
m_CornerRadius = data.value("cornerRadius", m_CornerRadius);
m_TexturePath = data.value("texture", m_TexturePath);
m_Filter = data.value("filter", std::string{"linear"}) == "nearest" ? TextureFilter::Nearest : TextureFilter::Linear;
}
void UIImage::ImGuiInspector()
{
constexpr std::array shapeNames{"Rect", "Ellipse"};
int shape = static_cast<int>(m_Shape);
if (ImGui::Combo("Shape", &shape, shapeNames.data(), static_cast<int>(shapeNames.size()))) {
m_Shape = static_cast<UIImageShape>(shape);
}
ImGui::ColorEdit4("Color", &m_Color.x);
ImGui::DragFloat("Outline", &m_Outline, 0.1f, 0.0f, 100.0f);
if (m_Shape == UIImageShape::Rect) {
ImGui::DragFloat("Corner Radius", &m_CornerRadius, 0.1f, 0.0f, 500.0f);
}
ImGuiUtils::InputString("Texture", m_TexturePath);
bool nearest = m_Filter == TextureFilter::Nearest;
if (ImGui::Checkbox("Nearest Filter", &nearest)) {
m_Filter = nearest ? TextureFilter::Nearest : TextureFilter::Linear;
}
}
+133
View File
@@ -0,0 +1,133 @@
#include <destrum/Components/UI/UIText.h>
#include <array>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/Serialization/GlmJson.h>
#include <destrum/Util/ImGuiUtils.h>
#include <imgui.h>
namespace {
[[nodiscard]] float AlignFactor(UITextAlign align)
{
switch (align) {
case UITextAlign::Start: return 0.0f;
case UITextAlign::Center: return 0.5f;
case UITextAlign::End: return 1.0f;
}
return 0.0f;
}
[[nodiscard]] const char* AlignName(UITextAlign align)
{
switch (align) {
case UITextAlign::Start: return "start";
case UITextAlign::Center: return "center";
case UITextAlign::End: return "end";
}
return "start";
}
[[nodiscard]] UITextAlign ParseAlign(const std::string& name, UITextAlign fallback)
{
if (name == "start") return UITextAlign::Start;
if (name == "center") return UITextAlign::Center;
if (name == "end") return UITextAlign::End;
return fallback;
}
}
UIText::UIText(GameObject& parent, std::string text, float fontSize)
: UIGraphic(parent, "UIText")
, m_Text(std::move(text))
, m_FontPath(FontCache::DefaultFont)
, m_FontSize(fontSize)
{
}
void UIText::SetOutline(float thickness, const glm::vec4& color)
{
m_Outline = thickness;
m_OutlineColor = color;
}
void UIText::SetAlignment(UITextAlign horizontal, UITextAlign vertical)
{
m_HorizontalAlign = horizontal;
m_VerticalAlign = vertical;
}
glm::vec2 UIText::PlaceText(const UIRect& rect, const glm::vec2& textSize) const
{
return rect.min + (rect.size - textSize) *
glm::vec2{AlignFactor(m_HorizontalAlign), AlignFactor(m_VerticalAlign)};
}
void UIText::DrawUI(const UIRect& rect, float scale)
{
if (m_Text.empty() || m_FontSize <= 0.0f || m_Color.w <= 0.0f) {
return;
}
const Font* font = FontCache::GetInstance().Get(m_FontPath);
if (font == nullptr) {
return;
}
const float size = m_FontSize * scale;
const glm::vec2 topLeft = PlaceText(rect, font->MeasureText(m_Text, size));
Renderer2D::GetInstance().DrawText(
*font,
m_Text,
topLeft,
size,
TextStyle{.color = m_Color, .outline = m_Outline * scale, .outlineColor = m_OutlineColor});
}
nlohmann::json UIText::Serialize() const
{
return {
{"text", m_Text},
{"font", m_FontPath},
{"fontSize", m_FontSize},
{"color", m_Color},
{"outline", m_Outline},
{"outlineColor", m_OutlineColor},
{"horizontalAlign", AlignName(m_HorizontalAlign)},
{"verticalAlign", AlignName(m_VerticalAlign)},
};
}
void UIText::Deserialize(const nlohmann::json& data)
{
m_Text = data.value("text", m_Text);
m_FontPath = data.value("font", m_FontPath);
m_FontSize = data.value("fontSize", m_FontSize);
m_Color = data.value("color", m_Color);
m_Outline = data.value("outline", m_Outline);
m_OutlineColor = data.value("outlineColor", m_OutlineColor);
m_HorizontalAlign = ParseAlign(data.value("horizontalAlign", std::string{}), m_HorizontalAlign);
m_VerticalAlign = ParseAlign(data.value("verticalAlign", std::string{}), m_VerticalAlign);
}
void UIText::ImGuiInspector()
{
ImGuiUtils::InputStringMultiline("Text", m_Text);
ImGuiUtils::InputString("Font", m_FontPath);
ImGui::DragFloat("Font Size", &m_FontSize, 0.5f, 1.0f, 1000.0f);
ImGui::ColorEdit4("Color", &m_Color.x);
ImGui::DragFloat("Outline", &m_Outline, 0.1f, 0.0f, 16.0f);
ImGui::ColorEdit4("Outline Color", &m_OutlineColor.x);
constexpr std::array alignNames{"Start", "Center", "End"};
int horizontal = static_cast<int>(m_HorizontalAlign);
if (ImGui::Combo("Horizontal", &horizontal, alignNames.data(), static_cast<int>(alignNames.size()))) {
m_HorizontalAlign = static_cast<UITextAlign>(horizontal);
}
int vertical = static_cast<int>(m_VerticalAlign);
if (ImGui::Combo("Vertical", &vertical, alignNames.data(), static_cast<int>(alignNames.size()))) {
m_VerticalAlign = static_cast<UITextAlign>(vertical);
}
}
@@ -0,0 +1,34 @@
#include <destrum/Graphics/Resources/FontCache.h>
#include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Renderer.h>
#include <destrum/Util/GameState.h>
#include <spdlog/spdlog.h>
const Font* FontCache::Get(std::string_view assetPath)
{
auto it = fonts.find(std::string{assetPath});
if (it == fonts.end()) {
std::unique_ptr<Font> font;
try {
font = std::make_unique<Font>(Font::LoadFromFile(AssetFS::GetInstance().GetFullPath(assetPath)));
} catch (const std::exception& exception) {
spdlog::warn("FontCache: failed to load '{}': {}", assetPath, exception.what());
}
// Failures are cached too so a bad path doesn't hit the disk every frame
it = fonts.emplace(std::string{assetPath}, std::move(font)).first;
}
Font* font = it->second.get();
if (font == nullptr) {
return nullptr;
}
auto& gameState = GameState::GetInstance();
if (font->GetAtlas() == NULL_IMAGE_ID && gameState.HasGfxDevice() && gameState.HasRenderer() &&
gameState.Renderer().getResources() != nullptr) {
font->Upload(gameState.Gfx(), *gameState.Renderer().getResources());
}
return font;
}
+22
View File
@@ -195,6 +195,28 @@ void Transform::RemoveChild(Transform* transform) {
} }
} }
int Transform::GetSiblingIndex() const {
if (m_Parent == nullptr) {
return 0;
}
const auto& siblings = m_Parent->m_Children;
return static_cast<int>(std::find(siblings.begin(), siblings.end(), this) - siblings.begin());
}
void Transform::SetSiblingIndex(int index) {
if (m_Parent == nullptr) {
return;
}
auto& siblings = m_Parent->m_Children;
const auto it = std::find(siblings.begin(), siblings.end(), this);
if (it == siblings.end()) {
return;
}
siblings.erase(it);
const int clamped = std::clamp(index, 0, static_cast<int>(siblings.size()));
siblings.insert(siblings.begin() + clamped, this);
}
void Transform::AddChild(Transform* transform) { void Transform::AddChild(Transform* transform) {
if (transform == nullptr || transform == this) { if (transform == nullptr || transform == this) {
return; return;
+62 -9
View File
@@ -1,7 +1,6 @@
#include <destrum/Scene/Scene.h> #include <destrum/Scene/Scene.h>
#include <destrum/Components/CameraComponent.h> #include <destrum/Components/CameraComponent.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/Util/ImGuiUtils.h>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
@@ -238,14 +237,68 @@ void Scene::SetCurrentCamera(const CameraComponent* camera) {
} }
void Scene::RenderImgui(bool* open) { void Scene::RenderImgui(bool* open) {
ImGuiUtils::RenderSceneInspector( m_Inspector.Draw(*this, m_SelectedObjectId, [this] { DrawSettingsImGui(); }, open);
m_name, }
m_objects,
m_pendingAdditions, bool Scene::MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore) {
m_SelectedObjectId, if (IsIterating() || object.IsBeingDestroyed() || object.GetScene() != this || parent == &object) {
[this] { return CreateGameObject("GameObject"); }, return false;
[this] { DrawSettingsImGui(); }, }
open);
Transform& transform = object.GetTransform();
Transform* parentTransform = parent != nullptr ? &parent->GetTransform() : nullptr;
if (parentTransform != nullptr && transform.IsChild(parentTransform)) {
return false;
}
transform.SetParent(parentTransform, true);
if (transform.GetParent() != parentTransform) {
return false;
}
const auto findIn = [](auto& objects, const GameObject* target) {
return std::find_if(objects.begin(), objects.end(), [target](const auto& candidate) {
return candidate.get() == target;
});
};
if (parentTransform != nullptr) {
int index = parentTransform->GetChildCount();
if (insertBefore != nullptr && insertBefore->GetTransform().GetParent() == parentTransform) {
index = insertBefore->GetTransform().GetSiblingIndex();
if (transform.GetSiblingIndex() < index) {
--index;
}
}
transform.SetSiblingIndex(index);
}
// Loading rebuilds sibling order from the object list, so keep the list in the same order
const auto it = findIn(m_objects, &object);
if (it == m_objects.end()) {
return true;
}
std::shared_ptr<GameObject> moved = *it;
m_objects.erase(it);
auto position = m_objects.end();
if (insertBefore != nullptr) {
position = findIn(m_objects, insertBefore);
} else if (parentTransform != nullptr) {
// After the last sibling that is already in the list, or right after the parent
const GameObject* anchor = parent;
for (const Transform* sibling : parentTransform->GetChildren()) {
if (sibling != &transform && findIn(m_objects, sibling->GetOwner()) != m_objects.end()) {
anchor = sibling->GetOwner();
}
}
position = findIn(m_objects, anchor);
if (position != m_objects.end()) {
++position;
}
}
m_objects.insert(position, std::move(moved));
return true;
} }
void Scene::CleanupDestroyedGameObjects() { void Scene::CleanupDestroyedGameObjects() {
+60 -343
View File
@@ -4,7 +4,6 @@
#include <cfloat> #include <cfloat>
#include <exception> #include <exception>
#include <string> #include <string>
#include <unordered_set>
#include <vector> #include <vector>
#include <imgui.h> #include <imgui.h>
@@ -15,90 +14,21 @@
#include <destrum/ObjectModel/Component.h> #include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h> #include <destrum/ObjectModel/Transform.h>
#include <destrum/Serialization/ComponentFactory.h> #include <destrum/Scene/Scene.h>
#include <destrum/Serialization/ComponentRegistry.h>
#include <destrum/Serialization/PrefabSerializer.h>
namespace { namespace {
int ResizeStringCallback(ImGuiInputTextCallbackData* data) {
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
auto* string = static_cast<std::string*>(data->UserData);
string->resize(static_cast<std::size_t>(data->BufTextLen));
data->Buf = string->data();
}
return 0;
}
[[nodiscard]] bool IsInspectableObject(const GameObject* object) { [[nodiscard]] bool IsInspectableObject(const GameObject* object) {
return object != nullptr && !object->IsBeingDestroyed(); return object != nullptr && !object->IsBeingDestroyed();
} }
[[nodiscard]] bool HasInspectableChildren(const GameObject& object) {
return std::ranges::any_of(object.GetTransform().GetChildren(),
[](const Transform* child) {
return child != nullptr && IsInspectableObject(child->GetOwner());
});
}
void DrawGameObjectNode(
GameObject& object,
ObjectId& selectedObjectId,
std::unordered_set<const GameObject*>& visited,
bool defaultOpen = false) {
if (!IsInspectableObject(&object) || !visited.insert(&object).second) {
return;
}
const bool hasChildren = HasInspectableChildren(object);
ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_SpanAvailWidth;
if (object.GetId() == selectedObjectId) {
treeFlags |= ImGuiTreeNodeFlags_Selected;
}
if (defaultOpen) {
treeFlags |= ImGuiTreeNodeFlags_DefaultOpen;
}
if (!hasChildren) {
treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
}
const std::string displayName = object.GetName().empty()
? "<unnamed>"
: object.GetName();
const bool isOpen = ImGui::TreeNodeEx(
static_cast<const void*>(&object),
treeFlags,
"%s",
displayName.c_str());
if (ImGui::IsItemClicked()) {
selectedObjectId = object.GetId();
}
if (!object.IsActive()) {
ImGui::SameLine();
ImGui::TextDisabled("[inactive]");
} else if (!object.IsActiveInHierarchy()) {
ImGui::SameLine();
ImGui::TextDisabled("[parent inactive]");
}
if (isOpen && hasChildren) {
for (Transform* childTransform : object.GetTransform().GetChildren()) {
if (childTransform == nullptr) {
continue;
}
GameObject* child = childTransform->GetOwner();
if (IsInspectableObject(child)) {
DrawGameObjectNode(*child, selectedObjectId, visited);
}
}
ImGui::TreePop();
}
}
[[nodiscard]] GameObject* FindSelectedObject(
const std::vector<GameObject*>& objects,
ObjectId selectedObjectId) {
const auto it = std::find_if(
objects.begin(),
objects.end(),
[selectedObjectId](const GameObject* object) {
return object != nullptr && object->GetId() == selectedObjectId;
});
return it == objects.end() ? nullptr : *it;
}
} }
namespace ImGuiUtils { namespace ImGuiUtils {
@@ -110,7 +40,7 @@ namespace ImGuiUtils {
ImGui::PushID(label); ImGui::PushID(label);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(label); ImGui::TextUnformatted(label);
ImGui::SameLine(); ImGui::SameLine(ImGui::GetFontSize() * 5.0f);
ImGui::SetNextItemWidth(-FLT_MIN); ImGui::SetNextItemWidth(-FLT_MIN);
const bool changed = ImGui::DragFloat3( const bool changed = ImGui::DragFloat3(
"##value", "##value",
@@ -120,272 +50,59 @@ namespace ImGuiUtils {
return changed; return changed;
} }
void RenderSceneInspector( bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags, const char* hint)
std::string_view sceneName, {
const std::vector<std::shared_ptr<GameObject>>& objectsSource, flags |= ImGuiInputTextFlags_CallbackResize;
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions, if (hint != nullptr) {
ObjectId& selectedObjectId, return ImGui::InputTextWithHint(label, hint, value.data(), value.capacity() + 1, flags, ResizeStringCallback, &value);
const std::function<GameObject*()>& createGameObject, }
const std::function<void()>& drawSceneSettings, return ImGui::InputText(label, value.data(), value.capacity() + 1, flags, ResizeStringCallback, &value);
bool* open) { }
RegisterEngineComponents();
std::vector<GameObject*> objects; bool InputStringMultiline(const char* label, std::string& value)
objects.reserve(objectsSource.size() + pendingAdditions.size()); {
return ImGui::InputTextMultiline(label, value.data(), value.capacity() + 1, ImVec2{0.0f, ImGui::GetTextLineHeight() * 4.0f},
ImGuiInputTextFlags_CallbackResize, ResizeStringCallback, &value);
}
const auto appendInspectableObjects = [&objects](const auto& source) { bool DrawObjectRefPicker(const char* label, ObjectRef& ref, const Scene& scene,
for (const auto& object : source) { const std::function<bool(const GameObject&)>& filter)
if (IsInspectableObject(object.get())) { {
objects.push_back(object.get()); const GameObject* current = ref.Get(scene);
const std::string preview = current != nullptr ? current->GetName()
: ref.IsSet() ? "Missing object " + std::to_string(ref.GetId())
: "None";
bool changed = false;
if (ImGui::BeginCombo(label, preview.c_str())) {
if (ImGui::Selectable("None", !ref.IsSet())) {
ref = ObjectRef{};
changed = true;
}
for (const auto& object : scene.GetObjects()) {
if (!IsInspectableObject(object.get()) || (filter && !filter(*object))) {
continue;
}
ImGui::PushID(static_cast<int>(object->GetId()));
if (ImGui::Selectable(object->GetName().c_str(), object.get() == current)) {
ref = ObjectRef(*object);
changed = true;
}
ImGui::PopID();
}
ImGui::EndCombo();
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(GameObjectPayload)) {
GameObject* dropped = scene.FindGameObject(*static_cast<const ObjectId*>(payload->Data));
if (dropped != nullptr && (!filter || filter(*dropped))) {
ref = ObjectRef(*dropped);
changed = true;
} }
} }
}; ImGui::EndDragDropTarget();
appendInspectableObjects(objectsSource);
appendInspectableObjects(pendingAdditions);
std::vector<GameObject*> roots;
roots.reserve(objects.size());
for (GameObject* object : objects) {
Transform* parent = object->GetTransform().GetParent();
if (parent == nullptr || !IsInspectableObject(parent->GetOwner())) {
roots.push_back(object);
}
} }
return changed;
if (FindSelectedObject(objects, selectedObjectId) == nullptr) {
selectedObjectId = InvalidObjectId;
}
ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Inspector", open)) {
ImGui::End();
return;
}
const std::string displaySceneName = sceneName.empty()
? "<unnamed>"
: std::string{sceneName};
ImGui::Text("Scene: %s", displaySceneName.c_str());
ImGui::SameLine();
ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size());
if (createGameObject) {
ImGui::SameLine();
if (ImGui::Button("+ Add GameObject")) {
if (GameObject* created = createGameObject()) {
objects.push_back(created);
selectedObjectId = created->GetId();
}
}
}
if (drawSceneSettings && ImGui::CollapsingHeader("Scene Settings")) {
drawSceneSettings();
}
ImGui::Separator();
const float availableWidth = ImGui::GetContentRegionAvail().x;
const float hierarchyWidth = std::max(180.0f, availableWidth * 0.45f);
if (ImGui::BeginChild(
"SceneHierarchy",
ImVec2(hierarchyWidth, 0.0f),
ImGuiChildFlags_Borders)) {
std::unordered_set<const GameObject*> visited;
visited.reserve(objects.size());
for (GameObject* root : roots) {
DrawGameObjectNode(*root, selectedObjectId, visited, true);
}
// Keep malformed or partially loaded hierarchies visible instead of
// silently dropping objects whose parent is not in this list.
bool hasUnvisitedObjects = false;
for (GameObject* object : objects) {
if (!visited.contains(object)) {
if (!hasUnvisitedObjects) {
ImGui::Separator();
ImGui::TextDisabled("OTHER OBJECTS");
hasUnvisitedObjects = true;
}
DrawGameObjectNode(*object, selectedObjectId, visited);
}
}
if (objects.empty()) {
ImGui::TextDisabled("This scene has no GameObjects.");
}
}
ImGui::EndChild();
GameObject* selectedObject = FindSelectedObject(objects, selectedObjectId);
if (selectedObject == nullptr) {
selectedObjectId = InvalidObjectId;
}
ImGui::SameLine();
if (ImGui::BeginChild(
"ObjectInspector",
ImVec2(0.0f, 0.0f),
ImGuiChildFlags_Borders)) {
if (selectedObject == nullptr) {
ImGui::TextDisabled("Select a GameObject to inspect it.");
} else {
const std::string displayName = selectedObject->GetName().empty()
? "<unnamed>"
: selectedObject->GetName();
ImGui::TextUnformatted(displayName.c_str());
ImGui::TextDisabled(
"GameObject | ID %llu",
static_cast<unsigned long long>(selectedObject->GetId()));
ImGui::Separator();
bool active = selectedObject->IsActive();
if (ImGui::Checkbox("Active", &active)) {
selectedObject->SetActive(active);
}
ImGui::SameLine();
const bool duplicateShortcut = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_D);
if (ImGui::Button("Duplicate") || duplicateShortcut) {
if (GameObject* duplicate = PrefabSerializer::Duplicate(*selectedObject)) {
selectedObjectId = duplicate->GetId();
}
}
const Transform* parent = selectedObject->GetTransform().GetParent();
if (parent != nullptr && parent->GetOwner() != nullptr) {
ImGui::Text("Parent: %s", parent->GetOwner()->GetName().c_str());
} else {
ImGui::Text("Parent: <scene root>");
}
ImGui::Text("Children: %d", selectedObject->GetTransform().GetChildCount());
if (ImGui::CollapsingHeader(
"Transform",
ImGuiTreeNodeFlags_DefaultOpen)) {
Transform& transform = selectedObject->GetTransform();
bool transformChanged = false;
glm::vec3 localPosition = transform.GetLocalPosition();
if (DrawVec3Control("Position", localPosition, 0.05f)) {
transform.SetLocalPosition(localPosition);
transformChanged = true;
}
glm::vec3 localRotation = glm::degrees(
glm::eulerAngles(transform.GetLocalRotation()));
if (DrawVec3Control("Rotation", localRotation, 0.5f)) {
transform.SetLocalRotation(localRotation);
transformChanged = true;
}
glm::vec3 localScale = transform.GetLocalScale();
if (DrawVec3Control("Scale", localScale, 0.05f)) {
transform.SetLocalScale(localScale);
transformChanged = true;
}
if (transformChanged) {
selectedObject->RefreshPhysicsInHierarchy();
}
const glm::vec3& worldPosition = transform.GetWorldPosition();
ImGui::Text(
"World position: %.2f, %.2f, %.2f",
worldPosition.x,
worldPosition.y,
worldPosition.z);
}
const bool componentsOpen = ImGui::CollapsingHeader(
"Components",
ImGuiTreeNodeFlags_DefaultOpen);
if (componentsOpen) {
if (ImGui::Button("+ Add Component")) {
ImGui::OpenPopup("AddComponentPopup");
}
std::string addComponentError;
if (ImGui::BeginPopup("AddComponentPopup")) {
std::vector<std::string> componentTypes =
ComponentFactory::GetRegisteredTypeNames();
std::sort(componentTypes.begin(), componentTypes.end());
bool hasAvailableComponent = false;
for (const std::string& componentType : componentTypes) {
const bool alreadyAdded = std::any_of(
selectedObject->GetComponents().begin(),
selectedObject->GetComponents().end(),
[&componentType](const auto& component) {
return component != nullptr &&
!component->IsBeingDestroyed() &&
component->GetTypeName() == componentType;
});
if (alreadyAdded) {
const std::string label =
componentType + " (already added)";
ImGui::BeginDisabled();
ImGui::MenuItem(label.c_str());
ImGui::EndDisabled();
continue;
}
hasAvailableComponent = true;
if (ImGui::MenuItem(componentType.c_str())) {
try {
if (ComponentFactory::Create(
componentType,
*selectedObject) == nullptr) {
addComponentError =
"Component type is not registered: " +
componentType;
} else {
ImGui::CloseCurrentPopup();
}
} catch (const std::exception& exception) {
addComponentError = exception.what();
}
}
}
if (!hasAvailableComponent) {
ImGui::TextDisabled("All components are already added.");
}
if (!addComponentError.empty()) {
ImGui::TextWrapped("Failed to add component: %s",
addComponentError.c_str());
}
ImGui::EndPopup();
}
bool hasComponents = false;
for (const auto& component : selectedObject->GetComponents()) {
if (component == nullptr || component->IsBeingDestroyed()) {
continue;
}
hasComponents = true;
ImGui::PushID(component.get());
const std::string typeName = component->GetTypeName();
if (ImGui::CollapsingHeader(typeName.c_str())) {
bool enabled = component->isEnabled();
if (ImGui::Checkbox("Enabled", &enabled)) {
component->SetEnabled(enabled);
}
component->ImGuiInspector();
}
ImGui::PopID();
}
if (!hasComponents) {
ImGui::TextDisabled("No components.");
}
}
}
}
ImGui::EndChild();
ImGui::End();
} }
} }
+167
View File
@@ -0,0 +1,167 @@
#include <destrum/Util/RectGizmo.h>
#include <array>
#include <cmath>
#include <destrum/Components/UI/Canvas.h>
#include <destrum/Components/UI/RectTransform.h>
#include <destrum/ObjectModel/GameObject.h>
#include <imgui.h>
namespace {
constexpr int MoveHandle = 0;
constexpr float HandleRadius = 5.0f;
constexpr std::array<glm::vec2, 8> HandleDirections{{
{-1.0f, -1.0f}, {0.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 0.0f},
{1.0f, 1.0f}, {0.0f, 1.0f}, {-1.0f, 1.0f}, {-1.0f, 0.0f},
}};
constexpr ImU32 OutlineColor = IM_COL32(60, 200, 255, 255);
constexpr ImU32 ActiveColor = IM_COL32(255, 200, 60, 255);
constexpr ImU32 HiddenColor = IM_COL32(150, 150, 150, 200);
constexpr ImU32 AnchorColor = IM_COL32(255, 255, 255, 200);
[[nodiscard]] ImVec2 ToImVec(const glm::vec2& value) { return ImVec2{value.x, value.y}; }
[[nodiscard]] glm::vec2 ToVec(const ImVec2& value) { return glm::vec2{value.x, value.y}; }
[[nodiscard]] ImGuiMouseCursor CursorFor(int handle)
{
if (handle == MoveHandle) {
return ImGuiMouseCursor_ResizeAll;
}
const glm::vec2 direction = HandleDirections[static_cast<std::size_t>(handle - 1)];
if (direction.x == 0.0f) {
return ImGuiMouseCursor_ResizeNS;
}
if (direction.y == 0.0f) {
return ImGuiMouseCursor_ResizeEW;
}
return direction.x == direction.y ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_ResizeNESW;
}
void DrawAnchor(ImDrawList& drawList, const glm::vec2& point, const glm::vec2& outwards)
{
constexpr float size = 8.0f;
const glm::vec2 tip = point;
const glm::vec2 a = point + glm::vec2{outwards.x * size, outwards.y * size * 0.4f};
const glm::vec2 b = point + glm::vec2{outwards.x * size * 0.4f, outwards.y * size};
drawList.AddTriangleFilled(ToImVec(tip), ToImVec(a), ToImVec(b), AnchorColor);
}
}
bool RectGizmo::CanManipulate(GameObject& object)
{
return object.GetComponent<RectTransform>() != nullptr && object.GetComponent<Canvas>() == nullptr &&
Canvas::FindFor(object) != nullptr;
}
bool RectGizmo::Manipulate(GameObject& object, const glm::vec2& drawImageSize)
{
auto* rectTransform = object.GetComponent<RectTransform>();
const Canvas* canvas = Canvas::FindFor(object);
if (!CanManipulate(object) || drawImageSize.x <= 0.0f || drawImageSize.y <= 0.0f) {
m_ActiveHandle = -1;
return false;
}
const ImGuiViewport* viewport = ImGui::GetMainViewport();
const glm::vec2 viewOrigin = ToVec(viewport->Pos);
const glm::vec2 viewScale = ToVec(viewport->Size) / drawImageSize;
const auto toView = [&](const glm::vec2& point) { return viewOrigin + point * viewScale; };
const UIRect& rect = rectTransform->GetScreenRect();
const glm::vec2 viewMin = toView(rect.min);
const glm::vec2 viewMax = toView(rect.Max());
const glm::vec2 viewCenter = (viewMin + viewMax) * 0.5f;
const glm::vec2 viewHalfSize = (viewMax - viewMin) * 0.5f;
const glm::vec2 mouse = ToVec(ImGui::GetIO().MousePos);
int hovered = -1;
const bool overImGui = ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) || ImGui::IsAnyItemActive();
if (m_ActiveHandle < 0 && !overImGui) {
for (std::size_t i = 0; i < HandleDirections.size(); ++i) {
const glm::vec2 handle = viewCenter + HandleDirections[i] * viewHalfSize;
if (glm::length(mouse - handle) <= HandleRadius + 2.0f) {
hovered = static_cast<int>(i) + 1;
break;
}
}
if (hovered < 0 && mouse.x >= viewMin.x && mouse.y >= viewMin.y && mouse.x <= viewMax.x && mouse.y <= viewMax.y) {
hovered = MoveHandle;
}
if (hovered >= 0 && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
m_ActiveHandle = hovered;
m_DragStartMouse = mouse;
m_DragStartPosition = rectTransform->GetPosition();
m_DragStartSize = rectTransform->GetSize();
}
}
bool changed = false;
if (m_ActiveHandle >= 0) {
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
m_ActiveHandle = -1;
} else {
const float canvasScale = canvas->GetScale(drawImageSize);
glm::vec2 delta = (mouse - m_DragStartMouse) / (viewScale * canvasScale);
if (snap && snapStep > 0.0f) {
delta = glm::round(delta / snapStep) * snapStep;
}
glm::vec2 position = m_DragStartPosition;
glm::vec2 size = m_DragStartSize;
if (m_ActiveHandle == MoveHandle) {
position += delta;
} else {
// The edge opposite the handle stays put, so the pivot moves by the pivot's share of the resize
const glm::vec2 direction = HandleDirections[static_cast<std::size_t>(m_ActiveHandle - 1)];
const glm::vec2& pivot = rectTransform->GetPivot();
for (int axis = 0; axis < 2; ++axis) {
if (direction[axis] == 0.0f) {
continue;
}
size[axis] += direction[axis] * delta[axis];
position[axis] += delta[axis] * (direction[axis] > 0.0f ? pivot[axis] : 1.0f - pivot[axis]);
}
}
if (position != rectTransform->GetPosition() || size != rectTransform->GetSize()) {
rectTransform->SetPosition(position);
rectTransform->SetSize(size);
changed = true;
}
}
}
const int highlighted = m_ActiveHandle >= 0 ? m_ActiveHandle : hovered;
if (highlighted >= 0) {
ImGui::SetMouseCursor(CursorFor(highlighted));
// Keeps the click from reaching the game, same as the 3D gizmo
ImGui::SetNextFrameWantCaptureMouse(true);
}
ImDrawList& drawList = *ImGui::GetBackgroundDrawList();
const ImU32 outlineColor = object.IsActiveInHierarchy() ? OutlineColor : HiddenColor;
const UIRect& parentRect = rectTransform->GetParentRect();
const glm::vec2 anchorMin = toView(parentRect.min + parentRect.size * rectTransform->GetAnchorMin());
const glm::vec2 anchorMax = toView(parentRect.min + parentRect.size * rectTransform->GetAnchorMax());
DrawAnchor(drawList, {anchorMin.x, anchorMin.y}, {-1.0f, -1.0f});
DrawAnchor(drawList, {anchorMax.x, anchorMin.y}, {1.0f, -1.0f});
DrawAnchor(drawList, {anchorMax.x, anchorMax.y}, {1.0f, 1.0f});
DrawAnchor(drawList, {anchorMin.x, anchorMax.y}, {-1.0f, 1.0f});
drawList.AddRect(ToImVec(viewMin), ToImVec(viewMax), highlighted == MoveHandle ? ActiveColor : outlineColor, 0.0f, 0, 1.5f);
const glm::vec2 pivot = viewMin + (viewMax - viewMin) * rectTransform->GetPivot();
drawList.AddCircle(ToImVec(pivot), 4.0f, outlineColor, 12, 1.5f);
for (std::size_t i = 0; i < HandleDirections.size(); ++i) {
const glm::vec2 handle = viewCenter + HandleDirections[i] * viewHalfSize;
const ImU32 color = highlighted == static_cast<int>(i) + 1 ? ActiveColor : outlineColor;
drawList.AddRectFilled(ToImVec(handle - glm::vec2{HandleRadius}), ToImVec(handle + glm::vec2{HandleRadius}), color);
}
return changed;
}
+745
View File
@@ -0,0 +1,745 @@
#include <destrum/Util/SceneInspector.h>
#include <algorithm>
#include <cctype>
#include <cfloat>
#include <utility>
#include <vector>
#include <imgui.h>
#include <spdlog/spdlog.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/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
#include <destrum/Scene/Scene.h>
#include <destrum/Serialization/ComponentFactory.h>
#include <destrum/Serialization/ComponentRegistry.h>
#include <destrum/Serialization/PrefabSerializer.h>
#include <destrum/Util/ImGuiUtils.h>
namespace {
[[nodiscard]] bool IsInspectable(const GameObject* object)
{
return object != nullptr && !object->IsBeingDestroyed();
}
[[nodiscard]] GameObject* ParentOf(const GameObject& object)
{
const Transform* parent = object.GetTransform().GetParent();
return parent != nullptr ? parent->GetOwner() : nullptr;
}
[[nodiscard]] bool HasInspectableChildren(const GameObject& object)
{
return std::ranges::any_of(object.GetTransform().GetChildren(), [](const Transform* child) {
return child != nullptr && IsInspectable(child->GetOwner());
});
}
[[nodiscard]] std::vector<GameObject*> CollectObjects(const Scene& scene)
{
std::vector<GameObject*> objects;
objects.reserve(scene.GetObjects().size() + scene.GetPendingAdditions().size());
for (const auto* source : {&scene.GetObjects(), &scene.GetPendingAdditions()}) {
for (const auto& object : *source) {
if (IsInspectable(object.get())) {
objects.push_back(object.get());
}
}
}
return objects;
}
[[nodiscard]] bool IsRoot(const GameObject& object)
{
return !IsInspectable(ParentOf(object));
}
[[nodiscard]] GameObject* NextSibling(const Scene& scene, const GameObject& object)
{
if (const Transform* parent = object.GetTransform().GetParent()) {
const auto& siblings = parent->GetChildren();
const auto it = std::ranges::find(siblings, &object.GetTransform());
for (auto next = it == siblings.end() ? it : it + 1; next != siblings.end(); ++next) {
if (IsInspectable((*next)->GetOwner())) {
return (*next)->GetOwner();
}
}
return nullptr;
}
const auto objects = CollectObjects(scene);
const auto it = std::ranges::find(objects, &object);
for (auto next = it == objects.end() ? it : it + 1; next != objects.end(); ++next) {
if (IsRoot(**next)) {
return *next;
}
}
return nullptr;
}
[[nodiscard]] bool ContainsIgnoreCase(std::string_view text, std::string_view query)
{
const auto it = std::search(text.begin(), text.end(), query.begin(), query.end(), [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
});
return it != text.end();
}
[[nodiscard]] const char* DisplayName(const GameObject& object)
{
return object.GetName().empty() ? "<unnamed>" : object.GetName().c_str();
}
GameObject* CreateChild(Scene& scene, GameObject* parent, const std::string& name)
{
GameObject* object = scene.CreateGameObject(name);
if (parent != nullptr) {
object->GetTransform().SetParent(&parent->GetTransform(), false);
}
return object;
}
GameObject* CreateUIChild(Scene& scene, GameObject* parent, const std::string& name)
{
if (parent == nullptr || Canvas::FindFor(*parent) == nullptr) {
parent = CreateChild(scene, parent, "Canvas");
parent->AddComponent<Canvas>();
}
GameObject* object = CreateChild(scene, parent, name);
object->AddComponent<RectTransform>();
return object;
}
// Returns the created object so the caller can select and rename it
GameObject* DrawCreateMenu(Scene& scene, GameObject* parent)
{
GameObject* created = nullptr;
if (ImGui::MenuItem("Empty")) {
created = CreateChild(scene, parent, "GameObject");
}
if (ImGui::BeginMenu("UI")) {
if (ImGui::MenuItem("Canvas")) {
created = CreateChild(scene, parent, "Canvas");
created->AddComponent<Canvas>();
}
if (ImGui::MenuItem("Rect")) {
created = CreateUIChild(scene, parent, "Rect");
}
if (ImGui::MenuItem("Image")) {
created = CreateUIChild(scene, parent, "Image");
created->AddComponent<UIImage>();
}
if (ImGui::MenuItem("Text")) {
created = CreateUIChild(scene, parent, "Text");
created->GetComponent<RectTransform>()->SetSize({300.0f, 60.0f});
created->AddComponent<UIText>("Text", 32.0f);
}
ImGui::EndMenu();
}
return created;
}
// Right aligned controls drawn over a collapsing header, x is from the start of the line
[[nodiscard]] float RightAlignedX(float lineStartX, float lineWidth, float itemsWidth)
{
return lineStartX + lineWidth - itemsWidth;
}
}
void SceneInspector::Draw(Scene& scene, ObjectId& selectedObjectId, const std::function<void()>& drawSceneSettings, bool* open)
{
RegisterEngineComponents();
if (scene.FindGameObject(selectedObjectId) == nullptr) {
selectedObjectId = InvalidObjectId;
}
ImGui::SetNextWindowSize(ImVec2(720.0f, 560.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Inspector", open)) {
ImGui::End();
return;
}
HandleShortcuts(scene, selectedObjectId);
if (ImGui::BeginChild("Hierarchy", ImVec2(std::max(220.0f, ImGui::GetContentRegionAvail().x * 0.4f), 0.0f),
ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX)) {
DrawHierarchy(scene, selectedObjectId);
}
ImGui::EndChild();
ImGui::SameLine();
if (ImGui::BeginChild("Properties", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) {
if (GameObject* selected = scene.FindGameObject(selectedObjectId)) {
DrawObject(*selected);
} else if (drawSceneSettings) {
drawSceneSettings();
}
}
ImGui::EndChild();
ImGui::End();
ApplyPendingActions(scene, selectedObjectId);
}
void SceneInspector::HandleShortcuts(Scene& scene, ObjectId& selectedObjectId)
{
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) || ImGui::GetIO().WantTextInput ||
selectedObjectId == InvalidObjectId) {
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
m_PendingDelete = selectedObjectId;
}
if (ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_D)) {
m_PendingDuplicate = selectedObjectId;
}
if (ImGui::IsKeyPressed(ImGuiKey_F2, false)) {
if (const GameObject* selected = scene.FindGameObject(selectedObjectId)) {
BeginRename(*selected);
}
}
}
void SceneInspector::DrawHierarchy(Scene& scene, ObjectId& selectedObjectId)
{
if (ImGui::Button("+")) {
ImGui::OpenPopup("CreateObject");
}
if (ImGui::BeginPopup("CreateObject")) {
if (GameObject* created = DrawCreateMenu(scene, nullptr)) {
selectedObjectId = created->GetId();
BeginRename(*created);
}
ImGui::EndPopup();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
ImGuiUtils::InputString("##Search", m_Filter, 0, "Search");
if (!ImGui::BeginChild("Tree")) {
ImGui::EndChild();
return;
}
const std::vector<GameObject*> objects = CollectObjects(scene);
std::unordered_set<const GameObject*> visited;
visited.reserve(objects.size());
if (m_Filter.empty()) {
for (GameObject* object : objects) {
if (IsRoot(*object)) {
DrawNode(scene, *object, selectedObjectId, visited);
}
}
// Anything not reachable from a root is stuck in a parent cycle, still show it
std::unordered_set<const GameObject*> reachable;
std::vector<const GameObject*> pending;
for (const GameObject* object : objects) {
if (IsRoot(*object)) {
pending.push_back(object);
}
}
while (!pending.empty()) {
const GameObject* object = pending.back();
pending.pop_back();
if (!reachable.insert(object).second) {
continue;
}
for (const Transform* child : object->GetTransform().GetChildren()) {
if (child != nullptr && IsInspectable(child->GetOwner())) {
pending.push_back(child->GetOwner());
}
}
}
for (GameObject* object : objects) {
if (!reachable.contains(object)) {
DrawNode(scene, *object, selectedObjectId, visited);
}
}
} else {
for (GameObject* object : objects) {
if (ContainsIgnoreCase(object->GetName(), m_Filter)) {
DrawNode(scene, *object, selectedObjectId, visited);
}
}
}
// The empty space below the tree: click to deselect, drop to move to the root, right click to create
const ImVec2 remaining = ImGui::GetContentRegionAvail();
ImGui::InvisibleButton("##Background", ImVec2(std::max(remaining.x, 1.0f), std::max(remaining.y, ImGui::GetFrameHeight())));
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
selectedObjectId = InvalidObjectId;
}
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(ImGuiUtils::GameObjectPayload)) {
m_PendingMove = {.object = *static_cast<const ObjectId*>(payload->Data)};
}
ImGui::EndDragDropTarget();
}
if (ImGui::BeginPopupContextItem("BackgroundMenu")) {
DrawObjectMenu(scene, nullptr, selectedObjectId);
ImGui::EndPopup();
}
ImGui::EndChild();
}
void SceneInspector::DrawNode(Scene& scene, GameObject& object, ObjectId& selectedObjectId,
std::unordered_set<const GameObject*>& visited)
{
if (!IsInspectable(&object) || !visited.insert(&object).second) {
return;
}
const bool flat = !m_Filter.empty();
const bool hasChildren = !flat && HasInspectableChildren(object);
const bool renaming = m_RenamingId == object.GetId();
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth |
ImGuiTreeNodeFlags_DrawLinesToNodes;
if (object.GetId() == selectedObjectId) {
flags |= ImGuiTreeNodeFlags_Selected;
}
if (!hasChildren) {
flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
}
if (m_OpenNextFrame == object.GetId()) {
ImGui::SetNextItemOpen(true);
m_OpenNextFrame = InvalidObjectId;
}
const bool inactive = !object.IsActiveInHierarchy();
if (inactive) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
const bool open = ImGui::TreeNodeEx(reinterpret_cast<void*>(static_cast<std::uintptr_t>(object.GetId())), flags, "%s",
renaming ? "" : DisplayName(object));
if (inactive) {
ImGui::PopStyleColor();
}
if ((ImGui::IsItemClicked(ImGuiMouseButton_Left) || ImGui::IsItemClicked(ImGuiMouseButton_Right)) &&
!ImGui::IsItemToggledOpen()) {
selectedObjectId = object.GetId();
}
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && !ImGui::IsItemToggledOpen()) {
BeginRename(object);
}
DrawNodeDragDrop(object);
if (ImGui::BeginPopupContextItem("ObjectMenu")) {
DrawObjectMenu(scene, &object, selectedObjectId);
ImGui::EndPopup();
}
if (renaming) {
ImGui::SameLine();
DrawRenameField(object);
}
if (open && hasChildren) {
for (Transform* child : object.GetTransform().GetChildren()) {
if (child != nullptr && IsInspectable(child->GetOwner())) {
DrawNode(scene, *child->GetOwner(), selectedObjectId, visited);
}
}
ImGui::TreePop();
}
}
void SceneInspector::DrawNodeDragDrop(GameObject& object)
{
const ObjectId id = object.GetId();
if (ImGui::BeginDragDropSource()) {
ImGui::SetDragDropPayload(ImGuiUtils::GameObjectPayload, &id, sizeof(id));
ImGui::TextUnformatted(DisplayName(object));
ImGui::EndDragDropSource();
}
if (!ImGui::BeginDragDropTarget()) {
return;
}
const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(
ImGuiUtils::GameObjectPayload,
ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect);
if (payload != nullptr) {
const ObjectId dragged = *static_cast<const ObjectId*>(payload->Data);
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
const float zone = (max.y - min.y) * 0.25f;
const float mouseY = ImGui::GetMousePos().y;
const bool before = mouseY < min.y + zone;
const bool after = mouseY > max.y - zone;
ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImU32 color = ImGui::GetColorU32(ImGuiCol_DragDropTarget);
if (before) {
drawList->AddLine(min, ImVec2(max.x, min.y), color, 2.0f);
} else if (after) {
drawList->AddLine(ImVec2(min.x, max.y), max, color, 2.0f);
} else {
drawList->AddRect(min, max, color, 0.0f, 0, 2.0f);
}
if (payload->IsDelivery() && dragged != id) {
const GameObject* parent = ParentOf(object);
const ObjectId parentId = IsInspectable(parent) ? parent->GetId() : InvalidObjectId;
if (before || after) {
m_PendingMove = {.object = dragged, .parent = parentId, .anchor = id, .after = after};
} else {
m_PendingMove = {.object = dragged, .parent = id};
}
}
}
ImGui::EndDragDropTarget();
}
void SceneInspector::DrawRenameField(GameObject& object)
{
if (m_FocusRename) {
ImGui::SetKeyboardFocusHere();
m_FocusRename = false;
}
ImGui::SetNextItemWidth(-FLT_MIN);
const bool submitted = ImGuiUtils::InputString("##Rename", m_RenameBuffer,
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll);
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
m_RenamingId = InvalidObjectId;
return;
}
if (submitted || ImGui::IsItemDeactivated()) {
if (!m_RenameBuffer.empty()) {
object.SetName(m_RenameBuffer);
}
m_RenamingId = InvalidObjectId;
}
}
void SceneInspector::BeginRename(const GameObject& object)
{
m_RenamingId = object.GetId();
m_RenameBuffer = object.GetName();
m_FocusRename = true;
}
void SceneInspector::DrawObjectMenu(Scene& scene, GameObject* object, ObjectId& selectedObjectId)
{
if (ImGui::BeginMenu(object != nullptr ? "Create Child" : "Create")) {
if (GameObject* created = DrawCreateMenu(scene, object)) {
selectedObjectId = created->GetId();
if (object != nullptr) {
m_OpenNextFrame = object->GetId();
}
BeginRename(*created);
}
ImGui::EndMenu();
}
if (object == nullptr) {
return;
}
ImGui::Separator();
if (ImGui::MenuItem("Rename", "F2")) {
BeginRename(*object);
}
if (ImGui::MenuItem("Duplicate", "Ctrl+D")) {
m_PendingDuplicate = object->GetId();
}
if (ImGui::MenuItem("Unparent", nullptr, false, !IsRoot(*object))) {
m_PendingMove = {.object = object->GetId()};
}
bool active = object->IsActive();
if (ImGui::MenuItem("Active", nullptr, &active)) {
object->SetActive(active);
}
ImGui::Separator();
if (ImGui::MenuItem("Delete", "Del")) {
m_PendingDelete = object->GetId();
}
}
void SceneInspector::ApplyPendingActions(Scene& scene, ObjectId& selectedObjectId)
{
if (m_PendingMove.object != InvalidObjectId) {
GameObject* object = scene.FindGameObject(m_PendingMove.object);
GameObject* parent = scene.FindGameObject(m_PendingMove.parent);
GameObject* insertBefore = scene.FindGameObject(m_PendingMove.anchor);
if (insertBefore != nullptr && m_PendingMove.after) {
insertBefore = NextSibling(scene, *insertBefore);
}
if (insertBefore == object && object != nullptr) {
insertBefore = NextSibling(scene, *object);
}
if (object != nullptr && scene.MoveInHierarchy(*object, parent, insertBefore) && parent != nullptr) {
m_OpenNextFrame = parent->GetId();
}
m_PendingMove = {};
}
if (m_PendingDuplicate != InvalidObjectId) {
if (GameObject* object = scene.FindGameObject(m_PendingDuplicate)) {
if (const GameObject* duplicate = PrefabSerializer::Duplicate(*object)) {
selectedObjectId = duplicate->GetId();
}
}
m_PendingDuplicate = InvalidObjectId;
}
if (m_PendingDelete != InvalidObjectId) {
if (GameObject* object = scene.FindGameObject(m_PendingDelete)) {
object->Destroy();
}
if (selectedObjectId == m_PendingDelete) {
selectedObjectId = InvalidObjectId;
}
m_PendingDelete = InvalidObjectId;
}
}
void SceneInspector::DrawObject(GameObject& object)
{
bool active = object.IsActive();
if (ImGui::Checkbox("##Active", &active)) {
object.SetActive(active);
}
ImGui::SetItemTooltip("Active");
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
std::string name = object.GetName();
if (ImGuiUtils::InputString("##Name", name)) {
object.SetName(name);
}
ImGui::Spacing();
DrawTransform(object);
DrawComponents(object);
DrawAddComponent(object);
}
void SceneInspector::DrawTransform(GameObject& object)
{
auto* rectTransform = object.GetComponent<RectTransform>();
// Everything under a canvas has a RectTransform, a stretched one keeps the old layout
if (rectTransform == nullptr && object.GetComponent<Canvas>() == nullptr && Canvas::FindFor(object) != nullptr) {
rectTransform = object.AddComponent<RectTransform>();
rectTransform->Stretch();
}
if (rectTransform != nullptr) {
const bool open = ImGui::CollapsingHeader("Rect Transform", ImGuiTreeNodeFlags_DefaultOpen);
if (ImGui::BeginPopupContextItem("RectTransformMenu")) {
if (ImGui::MenuItem("Stretch")) {
rectTransform->Stretch();
}
ImGui::EndPopup();
}
if (open) {
ImGui::PushID(rectTransform);
rectTransform->ImGuiInspector();
ImGui::PopID();
}
return;
}
Transform& transform = object.GetTransform();
const bool open = ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen);
bool changed = false;
if (ImGui::BeginPopupContextItem("TransformMenu")) {
if (ImGui::MenuItem("Reset Position")) {
transform.SetLocalPosition(glm::vec3{0.0f});
changed = true;
}
if (ImGui::MenuItem("Reset Rotation")) {
transform.SetLocalRotation(glm::quat{1.0f, 0.0f, 0.0f, 0.0f});
changed = true;
}
if (ImGui::MenuItem("Reset Scale")) {
transform.SetLocalScale(glm::vec3{1.0f});
changed = true;
}
ImGui::EndPopup();
}
if (open) {
glm::vec3 position = transform.GetLocalPosition();
if (ImGuiUtils::DrawVec3Control("Position", position, 0.05f)) {
transform.SetLocalPosition(position);
changed = true;
}
glm::vec3 rotation = glm::degrees(glm::eulerAngles(transform.GetLocalRotation()));
if (ImGuiUtils::DrawVec3Control("Rotation", rotation, 0.5f)) {
transform.SetLocalRotation(rotation);
changed = true;
}
glm::vec3 scale = transform.GetLocalScale();
if (ImGuiUtils::DrawVec3Control("Scale", scale, 0.05f)) {
transform.SetLocalScale(scale);
changed = true;
}
}
if (changed) {
object.RefreshPhysicsInHierarchy();
}
}
void SceneInspector::DrawComponents(GameObject& object)
{
auto& components = object.GetComponents();
const auto* rectTransform = object.GetComponent<RectTransform>();
const bool underCanvas = Canvas::FindFor(object) != nullptr;
for (std::size_t i = 0; i < components.size(); ++i) {
Component* component = components[i].get();
if (component == nullptr || component->IsBeingDestroyed() || component == rectTransform) {
continue;
}
ImGui::PushID(component);
const float lineStart = ImGui::GetCursorPosX();
const float lineWidth = ImGui::GetContentRegionAvail().x;
const bool enabled = component->isEnabled();
if (!enabled) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
const std::string typeName = component->GetTypeName();
const bool open = ImGui::CollapsingHeader(typeName.c_str(),
ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_AllowOverlap);
if (!enabled) {
ImGui::PopStyleColor();
}
const bool removable = !(underCanvas && dynamic_cast<RectTransform*>(component) != nullptr);
if (ImGui::BeginPopupContextItem("ComponentMenu")) {
DrawComponentMenu(object, *component, i, removable);
ImGui::EndPopup();
}
const ImGuiStyle& style = ImGui::GetStyle();
const float buttonWidth = ImGui::CalcTextSize("...").x + style.FramePadding.x * 2.0f;
const float controlsWidth = ImGui::GetFrameHeight() + style.ItemSpacing.x + buttonWidth + style.FramePadding.x;
ImGui::SameLine(RightAlignedX(lineStart, lineWidth, controlsWidth));
bool enabledToggle = enabled;
if (ImGui::Checkbox("##Enabled", &enabledToggle)) {
component->SetEnabled(enabledToggle);
}
ImGui::SetItemTooltip("Enabled");
ImGui::SameLine();
if (ImGui::Button("...")) {
ImGui::OpenPopup("ComponentMenu");
}
if (open) {
component->ImGuiInspector();
ImGui::Spacing();
}
ImGui::PopID();
// A move or remove from the menu changed the list, the rest is drawn next frame
if (components.size() <= i || components[i].get() != component) {
break;
}
}
}
void SceneInspector::DrawComponentMenu(GameObject& object, Component& component, std::size_t index, bool removable)
{
auto& components = object.GetComponents();
if (ImGui::MenuItem("Move Up", nullptr, false, index > 0)) {
std::swap(components[index], components[index - 1]);
}
if (ImGui::MenuItem("Move Down", nullptr, false, index + 1 < components.size())) {
std::swap(components[index], components[index + 1]);
}
ImGui::Separator();
const std::string typeName = component.GetTypeName();
if (ImGui::MenuItem("Copy Values")) {
m_ClipboardType = typeName;
m_Clipboard = component.Serialize();
}
if (ImGui::MenuItem("Paste Values", nullptr, false, m_ClipboardType == typeName)) {
try {
component.Deserialize(m_Clipboard);
if (component.AffectsPhysics()) {
object.RefreshPhysics();
}
} catch (const std::exception& exception) {
spdlog::warn("Pasting {} values failed: {}", typeName, exception.what());
}
}
ImGui::Separator();
if (ImGui::MenuItem("Remove", nullptr, false, removable)) {
const bool affectsPhysics = component.AffectsPhysics();
component.Destroy();
if (affectsPhysics) {
object.RefreshPhysics();
}
}
}
void SceneInspector::DrawAddComponent(GameObject& object)
{
ImGui::Spacing();
if (ImGui::Button("Add Component", ImVec2(-FLT_MIN, 0.0f))) {
m_ComponentFilter.clear();
ImGui::OpenPopup("AddComponent");
}
ImGui::SetNextWindowSizeConstraints(ImVec2(ImGui::GetItemRectSize().x, 0.0f), ImVec2(FLT_MAX, 320.0f));
if (!ImGui::BeginPopup("AddComponent")) {
return;
}
if (ImGui::IsWindowAppearing()) {
ImGui::SetKeyboardFocusHere();
}
ImGui::SetNextItemWidth(-FLT_MIN);
const bool submitted = ImGuiUtils::InputString("##ComponentSearch", m_ComponentFilter,
ImGuiInputTextFlags_EnterReturnsTrue, "Search");
std::vector<std::string> types = ComponentFactory::GetRegisteredTypeNames();
std::ranges::sort(types);
std::erase_if(types, [&](const std::string& type) {
const bool added = std::ranges::any_of(object.GetComponents(), [&](const auto& component) {
return component != nullptr && !component->IsBeingDestroyed() && component->GetTypeName() == type;
});
return added || !ContainsIgnoreCase(type, m_ComponentFilter);
});
std::string chosen = submitted && !types.empty() ? types.front() : std::string{};
for (const std::string& type : types) {
if (ImGui::Selectable(type.c_str())) {
chosen = type;
}
}
if (!chosen.empty()) {
try {
if (ComponentFactory::Create(chosen, object) == nullptr) {
spdlog::warn("Component type is not registered: {}", chosen);
}
} catch (const std::exception& exception) {
spdlog::warn("Adding {} failed: {}", chosen, exception.what());
}
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
+5
View File
@@ -13,14 +13,19 @@ set(GAME_SRC
src/components/GameComponentRegistry.cpp src/components/GameComponentRegistry.cpp
src/components/Activatable.cpp src/components/Activatable.cpp
src/components/Checkpoint.cpp
src/components/CrosshairUI.cpp
src/components/Door.cpp src/components/Door.cpp
src/components/FirstPersonController.cpp src/components/FirstPersonController.cpp
src/components/Interactable.cpp src/components/Interactable.cpp
src/components/InteractableButton.cpp src/components/InteractableButton.cpp
src/components/LightToggle.cpp src/components/LightToggle.cpp
src/components/MovingPlatform.cpp src/components/MovingPlatform.cpp
src/components/PlayerRespawn.cpp
src/components/PrintComponent.cpp src/components/PrintComponent.cpp
src/components/TriggerLoggerComponent.cpp src/components/TriggerLoggerComponent.cpp
src/components/WinGoal.cpp
src/components/WinScreenUI.cpp
) )
add_executable(lightkeeper ${GAME_SRC}) add_executable(lightkeeper ${GAME_SRC})
+2 -3
View File
@@ -6,10 +6,10 @@
#include <destrum/App.h> #include <destrum/App.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
#include <destrum/Graphics/RenderResources.h> #include <destrum/Graphics/RenderResources.h>
#include <destrum/Util/RectGizmo.h>
#include <destrum/Util/TransformGizmo.h> #include <destrum/Util/TransformGizmo.h>
#include "destrum/Graphics/Resources/Cubemap.h" #include "destrum/Graphics/Resources/Cubemap.h"
#include "destrum/Graphics/Resources/Font.h"
#include "destrum/ObjectModel/GameObject.h" #include "destrum/ObjectModel/GameObject.h"
#include <scenes/MainScene.h> #include <scenes/MainScene.h>
@@ -31,7 +31,6 @@ public:
private: private:
void setupSkybox(); void setupSkybox();
void drawCrosshair();
void updateViewMode(); void updateViewMode();
[[nodiscard]] CameraComponent* getSceneCamera() const; [[nodiscard]] CameraComponent* getSceneCamera() const;
[[nodiscard]] Camera& getViewCamera(); [[nodiscard]] Camera& getViewCamera();
@@ -59,8 +58,8 @@ private:
MainSceneObjects mainScene{}; MainSceneObjects mainScene{};
ImageID debug2DTexture{NULL_IMAGE_ID}; ImageID debug2DTexture{NULL_IMAGE_ID};
Font hudFont;
TransformGizmo transformGizmo; TransformGizmo transformGizmo;
RectGizmo rectGizmo;
char scenePath[512]{"scenes/debug_scene.json"}; char scenePath[512]{"scenes/debug_scene.json"};
char newSceneName[128]{"EmptyScene"}; 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]] float GetMouseSensitivity() const { return m_MouseSensitivity; }
[[nodiscard]] bool IsGrounded() const { return m_Grounded; } [[nodiscard]] bool IsGrounded() const { return m_Grounded; }
void Teleport(const glm::vec3& worldPosition);
[[nodiscard]] Interactable* GetFocusedInteractable() const { return m_FocusedInteractable; } [[nodiscard]] Interactable* GetFocusedInteractable() const { return m_FocusedInteractable; }
[[nodiscard]] glm::vec3 GetLookDirection() const; [[nodiscard]] glm::vec3 GetLookDirection() const;
@@ -1,21 +1,31 @@
#ifndef LIGHTKEEPER_GAMECOMPONENTLIST_H #ifndef LIGHTKEEPER_GAMECOMPONENTLIST_H
#define LIGHTKEEPER_GAMECOMPONENTLIST_H #define LIGHTKEEPER_GAMECOMPONENTLIST_H
#include <components/Checkpoint.h>
#include <components/CrosshairUI.h>
#include <components/Door.h> #include <components/Door.h>
#include <components/FirstPersonController.h> #include <components/FirstPersonController.h>
#include <components/InteractableButton.h> #include <components/InteractableButton.h>
#include <components/LightToggle.h> #include <components/LightToggle.h>
#include <components/MovingPlatform.h> #include <components/MovingPlatform.h>
#include <components/PlayerRespawn.h>
#include <components/PrintComponent.h> #include <components/PrintComponent.h>
#include <components/TriggerLoggerComponent.h> #include <components/TriggerLoggerComponent.h>
#include <components/WinGoal.h>
#include <components/WinScreenUI.h>
#define LIGHTKEEPER_GAME_COMPONENTS(X) \ #define LIGHTKEEPER_GAME_COMPONENTS(X) \
X(Checkpoint) \
X(CrosshairUI) \
X(Door) \ X(Door) \
X(FirstPersonController) \ X(FirstPersonController) \
X(InteractableButton) \ X(InteractableButton) \
X(LightToggle) \ X(LightToggle) \
X(MovingPlatform) \ X(MovingPlatform) \
X(PlayerRespawn) \
X(PrintComponent) \ X(PrintComponent) \
X(TriggerLoggerComponent) X(TriggerLoggerComponent) \
X(WinGoal) \
X(WinScreenUI)
#endif // LIGHTKEEPER_GAMECOMPONENTLIST_H #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{}; 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); MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources);
#endif // LIGHTKEEPER_MAINSCENE_H #endif // LIGHTKEEPER_MAINSCENE_H
-39
View File
@@ -2,11 +2,9 @@
#include <destrum/Components/CameraComponent.h> #include <destrum/Components/CameraComponent.h>
#include <destrum/FS/AssetFS.h> #include <destrum/FS/AssetFS.h>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Scene/Scene.h> #include <destrum/Scene/Scene.h>
#include <components/FirstPersonController.h> #include <components/FirstPersonController.h>
#include <components/Interactable.h>
#include <components/GameComponentRegistry.h> #include <components/GameComponentRegistry.h>
LightKeeper::LightKeeper() : App() LightKeeper::LightKeeper() : App()
@@ -43,9 +41,6 @@ void LightKeeper::customInit()
debug2DTexture = resources.loadImageFromFile( debug2DTexture = resources.loadImageFromFile(
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png")); 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() 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 bool LightKeeper::isPlayerViewActive() const
{ {
return mainScene.playerController != nullptr && mainScene.playerController->IsPossessed(); return mainScene.playerController != nullptr && mainScene.playerController->IsPossessed();
@@ -165,7 +127,6 @@ void LightKeeper::customDraw()
renderer.beginDrawing(gfxDevice); renderer.beginDrawing(gfxDevice);
submitDebugDraws(); submitDebugDraws();
drawCrosshair();
if (isPlayerViewActive()) if (isPlayerViewActive())
{ {
+6
View File
@@ -190,6 +190,12 @@ void LightKeeper::drawSelectionGizmo()
return; 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.HandleShortcuts();
transformGizmo.DrawToolbar(); transformGizmo.DrawToolbar();
transformGizmo.Manipulate(*selected, getViewCamera()); 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); 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) void FirstPersonController::FixedUpdate(float fixedDt)
{ {
if (m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody()) 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/BoxCollider.h>
#include <destrum/Components/Physics/CapsuleCollider.h> #include <destrum/Components/Physics/CapsuleCollider.h>
#include <destrum/Components/Physics/Rigidbody.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/FS/AssetFS.h>
#include <destrum/Graphics/RenderResources.h> #include <destrum/Graphics/RenderResources.h>
#include <destrum/Graphics/Resources/PrimitiveMeshes.h> #include <destrum/Graphics/Resources/PrimitiveMeshes.h>
@@ -16,11 +20,16 @@
#include <destrum/Util/ModelDocUtils.h> #include <destrum/Util/ModelDocUtils.h>
#include <components/FirstPersonController.h> #include <components/FirstPersonController.h>
#include <components/Checkpoint.h>
#include <components/CrosshairUI.h>
#include <components/Door.h> #include <components/Door.h>
#include <components/InteractableButton.h> #include <components/InteractableButton.h>
#include <components/LightToggle.h> #include <components/LightToggle.h>
#include <components/MovingPlatform.h> #include <components/MovingPlatform.h>
#include <components/PlayerRespawn.h>
#include <components/TriggerLoggerComponent.h> #include <components/TriggerLoggerComponent.h>
#include <components/WinGoal.h>
#include <components/WinScreenUI.h>
#include <glm/gtc/constants.hpp> #include <glm/gtc/constants.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
@@ -262,6 +271,167 @@ namespace {
{2.0f, platformHeight, platformSize.z}, rampMaterial, RigidbodyType::Static); {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 { struct WallOpening {
float offset; float offset;
float width; float width;
@@ -493,12 +663,97 @@ namespace {
// Facing the parkour course // Facing the parkour course
auto* controller = playerObj->AddComponent<FirstPersonController>(); auto* controller = playerObj->AddComponent<FirstPersonController>();
controller->SetYaw(glm::half_pi<float>()); controller->SetYaw(glm::half_pi<float>());
playerObj->AddComponent<PlayerRespawn>();
scene.GetPhysics().RegisterGameObject(*playerObj); scene.GetPhysics().RegisterGameObject(*playerObj);
objects.player = playerObj; objects.player = playerObj;
objects.playerController = controller; 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) void LoadSpawnBallResources(GfxDevice& gfxDevice, RenderResources& resources, MainSceneObjects& objects)
{ {
const auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", StaticModelOptions()); 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); CreateButton(scene, gfxDevice, resources, *door);
CreateHouse(scene, gfxDevice, resources); CreateHouse(scene, gfxDevice, resources);
CreateMovingPlatform(scene, gfxDevice, resources); CreateMovingPlatform(scene, gfxDevice, resources);
const WinGoal* winGoal = CreateWinCourse(scene, gfxDevice, resources);
CreatePlayer(scene, objects); CreatePlayer(scene, objects);
CreateHud(scene, *objects.player, *winGoal->GetGameObject());
LoadSpawnBallResources(gfxDevice, resources, objects); LoadSpawnBallResources(gfxDevice, resources, objects);
return objects; return objects;
+6
View File
@@ -3,6 +3,12 @@ add_executable(destrum_tests
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Activatable.cpp ${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Activatable.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Interactable.cpp ${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Interactable.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/InteractableButton.cpp ${CMAKE_SOURCE_DIR}/lightkeeper/src/components/InteractableButton.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/WinGoal.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Checkpoint.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/PlayerRespawn.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/FirstPersonController.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/CrosshairUI.cpp
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/WinScreenUI.cpp
) )
set_target_properties(destrum_tests PROPERTIES set_target_properties(destrum_tests PROPERTIES
+324 -1
View File
@@ -36,6 +36,10 @@
#include <destrum/Components/Physics/Collider.h> #include <destrum/Components/Physics/Collider.h>
#include <destrum/Components/Animator.h> #include <destrum/Components/Animator.h>
#include <destrum/Components/CameraComponent.h> #include <destrum/Components/CameraComponent.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/Graphics/Resources/PrimitiveMeshes.h> #include <destrum/Graphics/Resources/PrimitiveMeshes.h>
#include <destrum/Graphics/Managers/Renderer2D.h> #include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Graphics/Resources/Font.h> #include <destrum/Graphics/Resources/Font.h>
@@ -49,6 +53,12 @@
#include <components/Activatable.h> #include <components/Activatable.h>
#include <components/InteractableButton.h> #include <components/InteractableButton.h>
#include <components/Checkpoint.h>
#include <components/CrosshairUI.h>
#include <components/FirstPersonController.h>
#include <components/PlayerRespawn.h>
#include <components/WinGoal.h>
#include <components/WinScreenUI.h>
namespace { namespace {
class TestComponent final : public Component { class TestComponent final : public Component {
@@ -252,7 +262,11 @@ namespace {
"Rigidbody", "Rigidbody",
"BoxCollider", "BoxCollider",
"SphereCollider", "SphereCollider",
"CapsuleCollider" "CapsuleCollider",
"Canvas",
"RectTransform",
"UIImage",
"UIText"
}; };
for (const char* name : canonicalNames) { for (const char* name : canonicalNames) {
check(ComponentFactory::IsRegistered(name), check(ComponentFactory::IsRegistered(name),
@@ -1745,6 +1759,309 @@ namespace {
const json future = json::parse(R"({"version":99,"objects":[{"id":1}]})"); const json future = json::parse(R"({"version":99,"objects":[{"id":1}]})");
check(PrefabSerializer::Validate(future).has_value(), "an unknown prefab version must be rejected"); check(PrefabSerializer::Validate(future).has_value(), "an unknown prefab version must be rejected");
} }
void testWinGoalButton()
{
ComponentFactory::Register<InteractableButton>("InteractableButton");
ComponentFactory::Register<WinGoal>("WinGoal");
Scene& scene = SceneManager::GetInstance().CreateScene("win goal");
GameObject* goal = scene.CreateGameObject("goal");
auto* winGoal = goal->AddComponent<WinGoal>();
GameObject* buttonObject = scene.CreateGameObject("button");
auto* button = buttonObject->AddComponent<InteractableButton>();
button->SetCycleColors(false);
button->AddTarget(*goal);
scene.CommitPendingAdditions();
check(!winGoal->IsReached(), "the goal must start unreached");
button->Interact(*buttonObject);
check(winGoal->IsReached(), "pressing the win button must reach the goal");
const auto path = std::filesystem::temp_directory_path() / "destrum_win_goal_scene.json";
check(SceneSerializer::Save(scene, path), "the win goal scene must save");
check(SceneSerializer::Load(scene, path), "the win goal scene must load");
std::filesystem::remove(path);
scene.CommitPendingAdditions();
const GameObject* loadedGoal = findByName(scene, "goal");
check(loadedGoal != nullptr && loadedGoal->GetComponent<WinGoal>()->IsReached(),
"a reached goal must stay reached after a round-trip");
SceneManager::GetInstance().Destroy();
}
void testCheckpointRespawn()
{
Scene& scene = SceneManager::GetInstance().CreateScene("respawn");
GameObject* player = scene.CreateGameObject("player");
player->GetTransform().SetWorldPosition(glm::vec3{1.0f, 2.0f, 3.0f});
player->AddComponent<CapsuleCollider>(0.4f, 1.8f);
auto* rigidbody = player->AddComponent<Rigidbody>();
auto* respawn = player->AddComponent<PlayerRespawn>();
respawn->SetKillHeight(-10.0f);
GameObject* checkpointObject = scene.CreateGameObject("checkpoint");
checkpointObject->GetTransform().SetWorldPosition(glm::vec3{-5.0f, 4.0f, 0.0f});
auto* checkpoint = checkpointObject->AddComponent<Checkpoint>();
checkpoint->SetSpawnOffset({0.0f, 0.5f, 0.0f});
scene.CommitPendingAdditions();
check(rigidbody->HasPhysicsBody(), "the player must have a physics body");
respawn->Start();
check(respawn->GetRespawnPoint() == glm::vec3(1.0f, 2.0f, 3.0f),
"the respawn point must default to where the player starts");
checkpoint->OnTriggerEnter(checkpointObject);
check(respawn->GetRespawnPoint() == glm::vec3(1.0f, 2.0f, 3.0f),
"a checkpoint must ignore objects without PlayerRespawn");
checkpoint->OnTriggerEnter(player);
check(respawn->GetRespawnPoint() == glm::vec3(-5.0f, 4.5f, 0.0f),
"touching a checkpoint must move the respawn point");
rigidbody->Teleport(glm::vec3{0.0f, -20.0f, 0.0f});
rigidbody->SetLinearVelocity(glm::vec3{0.0f, -30.0f, 0.0f});
respawn->FixedUpdate(1.0f / 60.0f);
const glm::vec3 position = player->GetTransform().GetWorldPosition();
check(close(position.x, -5.0f) && close(position.y, 4.5f) && close(position.z, 0.0f),
"falling below the kill height must respawn at the checkpoint");
check(glm::length(rigidbody->GetLinearVelocity()) < 0.001f, "respawning must stop the fall");
scene.GetPhysics().FixedUpdate(1.0f / 60.0f);
check(player->GetTransform().GetWorldPosition().y > 4.0f,
"the physics body must have moved with the teleport");
SceneManager::GetInstance().Destroy();
}
void testCanvasLayout()
{
Scene& scene = SceneManager::GetInstance().CreateScene("canvas");
GameObject* hud = scene.CreateGameObject("hud");
auto* canvas = hud->AddComponent<Canvas>();
const auto addChild = [&](const std::string& name, GameObject& parent) {
GameObject* child = scene.CreateGameObject(name);
child->GetTransform().SetParent(&parent.GetTransform(), false);
return child;
};
GameObject* panel = addChild("panel", *hud);
auto* panelRect = panel->AddComponent<RectTransform>();
panelRect->SetSize({100.0f, 50.0f});
panelRect->SetPosition({10.0f, 0.0f});
panel->AddComponent<UIImage>(glm::vec4{1.0f, 0.0f, 0.0f, 1.0f});
GameObject* fill = addChild("fill", *panel);
fill->AddComponent<UIImage>(glm::vec4{0.0f, 1.0f, 0.0f, 1.0f})->SetShape(UIImageShape::Ellipse);
GameObject* corner = addChild("corner", *hud);
auto* cornerRect = corner->AddComponent<RectTransform>();
cornerRect->SetAnchor({1.0f, 1.0f});
cornerRect->SetPivot({1.0f, 1.0f});
cornerRect->SetPosition({-20.0f, -20.0f});
cornerRect->SetSize({40.0f, 40.0f});
corner->AddComponent<UIImage>();
GameObject* hidden = addChild("hidden", *hud);
auto* hiddenRect = hidden->AddComponent<RectTransform>();
hiddenRect->SetSize({20.0f, 20.0f});
hidden->AddComponent<UIImage>();
hidden->SetActive(false);
GameObject* nested = addChild("nested", *hud);
nested->AddComponent<Canvas>();
nested->AddComponent<UIImage>();
scene.CommitPendingAdditions();
auto& r2d = Renderer2D::GetInstance();
r2d.Clear();
canvas->Draw({1920.0f, 1080.0f});
const auto& shapes = r2d.GetShapes();
check(shapes.size() == 3, "a canvas must draw its active children and skip inactive ones and nested canvases");
check(shapes[0].type == Shape2DType::Rect && shapes[0].center == glm::vec2{970.0f, 540.0f} &&
shapes[0].halfSize == glm::vec2{50.0f, 25.0f},
"a centered rect must be placed around its anchor, offset by its position");
check(shapes[1].type == Shape2DType::Ellipse && shapes[1].center == shapes[0].center &&
shapes[1].halfSize == shapes[0].halfSize,
"an object without a RectTransform must fill its parent and draw after it");
check(shapes[2].center == glm::vec2{1880.0f, 1040.0f},
"anchors and pivot must place a rect relative to the screen corner");
check(panelRect->GetScreenRect().min == glm::vec2{920.0f, 515.0f},
"the canvas must store where it drew each rect");
check(hiddenRect->GetScreenRect().size == glm::vec2{20.0f, 20.0f},
"hidden objects must still be laid out so the editor can show them");
r2d.Clear();
canvas->Draw({3840.0f, 2160.0f});
check(r2d.GetShapes().size() == 3 && r2d.GetShapes()[0].halfSize == glm::vec2{100.0f, 50.0f} &&
r2d.GetShapes()[0].center == glm::vec2{1940.0f, 1080.0f},
"sizes and offsets must scale with the screen height");
canvas->SetScaleMode(CanvasScaleMode::ConstantPixelSize);
r2d.Clear();
canvas->Draw({3840.0f, 2160.0f});
check(r2d.GetShapes()[0].halfSize == glm::vec2{50.0f, 25.0f}, "a constant pixel size canvas must not scale");
check(Canvas::FindFor(*fill) == canvas && Canvas::FindFor(*nested) == nested->GetComponent<Canvas>(),
"FindFor must return the closest canvas on the object or its parents");
check(cornerRect->GetParentRect().size == glm::vec2{3840.0f, 2160.0f},
"the canvas must store the parent rect each rect was laid out in");
cornerRect->ApplyAnchorPreset({0.0f, -1.0f});
check(cornerRect->GetAnchorMin() == glm::vec2{0.0f, 0.0f} && cornerRect->GetAnchorMax() == glm::vec2{0.0f, 1.0f} &&
cornerRect->GetPivot() == glm::vec2{0.0f, 0.5f} && cornerRect->GetPosition() == glm::vec2{0.0f} &&
cornerRect->GetSize() == glm::vec2{40.0f, 0.0f},
"an anchor preset must pin or stretch each axis and keep the size on pinned axes");
panelRect->Stretch();
check(panelRect->ComputeRect(UIRect{.min = {10.0f, 20.0f}, .size = {30.0f, 40.0f}}, 2.0f).size == glm::vec2{30.0f, 40.0f},
"a stretched rect must fill its parent");
r2d.Clear();
SceneManager::GetInstance().Destroy();
}
void testUITextPlacement()
{
Scene& scene = SceneManager::GetInstance().CreateScene("ui text");
auto* text = scene.CreateGameObject("text")->AddComponent<UIText>("hi", 20.0f);
const UIRect rect{.min = {100.0f, 100.0f}, .size = {200.0f, 100.0f}};
check(text->PlaceText(rect, {50.0f, 20.0f}) == glm::vec2{175.0f, 140.0f}, "text must be centered by default");
text->SetAlignment(UITextAlign::End, UITextAlign::Start);
check(text->PlaceText(rect, {50.0f, 20.0f}) == glm::vec2{250.0f, 100.0f}, "text must follow its alignment");
SceneManager::GetInstance().Destroy();
}
void testHudRoundTrip()
{
RegisterEngineComponents();
ComponentFactory::Register<CrosshairUI>("CrosshairUI");
ComponentFactory::Register<WinScreenUI>("WinScreenUI");
ComponentFactory::Register<WinGoal>("WinGoal");
ComponentFactory::Register<FirstPersonController>("FirstPersonController");
Scene& scene = SceneManager::GetInstance().CreateScene("hud");
GameObject* player = scene.CreateGameObject("player");
player->AddComponent<FirstPersonController>();
GameObject* goal = scene.CreateGameObject("goal");
auto* winGoal = goal->AddComponent<WinGoal>();
GameObject* hud = scene.CreateGameObject("hud");
hud->AddComponent<Canvas>()->SetReferenceHeight(720.0f);
GameObject* crosshair = scene.CreateGameObject("crosshair");
crosshair->GetTransform().SetParent(&hud->GetTransform(), false);
auto* crosshairRect = crosshair->AddComponent<RectTransform>();
crosshairRect->SetAnchors({0.25f, 0.5f}, {0.75f, 0.5f});
crosshair->AddComponent<UIImage>()->SetOutline(3.0f);
GameObject* screen = scene.CreateGameObject("screen");
screen->GetTransform().SetParent(&hud->GetTransform(), false);
auto* label = screen->AddComponent<UIText>("YOU WIN", 64.0f);
label->SetAlignment(UITextAlign::Start, UITextAlign::End);
auto* crosshairUi = hud->AddComponent<CrosshairUI>();
crosshairUi->SetPlayer(*player);
crosshairUi->SetCrosshair(*crosshair);
auto* winScreenUi = hud->AddComponent<WinScreenUI>();
winScreenUi->SetGoal(*goal);
winScreenUi->SetScreen(*screen);
scene.CommitPendingAdditions();
crosshairUi->LateUpdate(0.0f);
winScreenUi->LateUpdate(0.0f);
check(!crosshair->IsActive(), "the crosshair must hide while the player isn't possessed");
check(!screen->IsActive(), "the win screen must hide until the goal is reached");
winGoal->Activate(*player);
winScreenUi->LateUpdate(0.0f);
check(screen->IsActive(), "the win screen must show once the goal is reached");
const auto path = std::filesystem::temp_directory_path() / "destrum_hud_scene.json";
check(SceneSerializer::Save(scene, path), "the hud scene must save");
Scene& copy = SceneManager::GetInstance().CreateScene("hud copy");
check(SceneSerializer::Load(copy, path), "the hud scene must load");
std::filesystem::remove(path);
copy.CommitPendingAdditions();
GameObject* loadedHud = findByName(copy, "hud");
GameObject* loadedCrosshair = findByName(copy, "crosshair");
GameObject* loadedScreen = findByName(copy, "screen");
check(loadedHud != nullptr && loadedCrosshair != nullptr && loadedScreen != nullptr,
"the hud objects must survive a round-trip");
check(loadedCrosshair->GetTransform().GetParent() == &loadedHud->GetTransform(),
"ui objects must stay parented to their canvas");
const auto* loadedCanvas = loadedHud->GetComponent<Canvas>();
check(loadedCanvas != nullptr && loadedCanvas->GetReferenceHeight() == 720.0f, "canvas settings must round-trip");
const auto* loadedRect = loadedCrosshair->GetComponent<RectTransform>();
check(loadedRect != nullptr && loadedRect->GetAnchorMin() == glm::vec2{0.25f, 0.5f} &&
loadedRect->GetAnchorMax() == glm::vec2{0.75f, 0.5f},
"rect transforms must round-trip");
const auto* loadedImage = loadedCrosshair->GetComponent<UIImage>();
check(loadedImage != nullptr && loadedImage->GetOutline() == 3.0f, "ui images must round-trip");
const auto* loadedLabel = loadedScreen->GetComponent<UIText>();
check(loadedLabel != nullptr && loadedLabel->GetText() == "YOU WIN" && loadedLabel->GetFontSize() == 64.0f &&
loadedLabel->GetHorizontalAlign() == UITextAlign::Start &&
loadedLabel->GetVerticalAlign() == UITextAlign::End,
"ui text must round-trip");
const auto* loadedCrosshairUi = loadedHud->GetComponent<CrosshairUI>();
const auto* loadedWinScreenUi = loadedHud->GetComponent<WinScreenUI>();
check(loadedCrosshairUi != nullptr && loadedCrosshairUi->GetPlayer().Get(copy) == findByName(copy, "player") &&
loadedCrosshairUi->GetCrosshair().Get(copy) == loadedCrosshair,
"crosshair references must point at the loaded objects");
check(loadedWinScreenUi != nullptr && loadedWinScreenUi->GetGoal().Get(copy) == findByName(copy, "goal") &&
loadedWinScreenUi->GetScreen().Get(copy) == loadedScreen,
"win screen references must point at the loaded objects");
SceneManager::GetInstance().Destroy();
}
void testHierarchyMoves()
{
Scene& scene = SceneManager::GetInstance().CreateScene("hierarchy");
GameObject* parent = scene.CreateGameObject("parent");
GameObject* a = scene.CreateGameObject("a");
GameObject* b = scene.CreateGameObject("b");
GameObject* c = scene.CreateGameObject("c");
GameObject* other = scene.CreateGameObject("other");
for (GameObject* child : {a, b, c}) {
child->GetTransform().SetParent(&parent->GetTransform(), false);
}
scene.CommitPendingAdditions();
const auto childNames = [](const GameObject& object) {
std::string names;
for (const Transform* child : object.GetTransform().GetChildren()) {
names += child->GetOwner()->GetName();
}
return names;
};
check(scene.MoveInHierarchy(*c, parent, a) && childNames(*parent) == "cab",
"moving before a sibling must reorder the children");
check(scene.MoveInHierarchy(*c, parent) && childNames(*parent) == "abc",
"moving without a sibling must put the object last");
check(scene.MoveInHierarchy(*a, other) && childNames(*parent) == "bc" && childNames(*other) == "a",
"moving onto another object must reparent it");
check(!scene.MoveInHierarchy(*other, a) && a->GetTransform().GetParent() == &other->GetTransform(),
"an object must not be moved under its own child");
check(scene.MoveInHierarchy(*a, nullptr, parent) && a->GetTransform().GetParent() == nullptr &&
scene.GetObjects().front().get() == a,
"moving to the root must unparent and order the roots");
check(scene.MoveInHierarchy(*c, parent, b) && childNames(*parent) == "cb", "reordering must still work");
const auto path = std::filesystem::temp_directory_path() / "destrum_hierarchy_scene.json";
check(SceneSerializer::Save(scene, path), "the hierarchy scene must save");
Scene& copy = SceneManager::GetInstance().CreateScene("hierarchy copy");
check(SceneSerializer::Load(copy, path), "the hierarchy scene must load");
std::filesystem::remove(path);
copy.CommitPendingAdditions();
const GameObject* loadedParent = findByName(copy, "parent");
check(loadedParent != nullptr && childNames(*loadedParent) == "cb", "sibling order must survive a save");
check(copy.GetObjects().front()->GetName() == "a", "root order must survive a save");
SceneManager::GetInstance().Destroy();
}
} }
int main() int main()
@@ -1796,6 +2113,12 @@ int main()
testPrefabRoundTrip(); testPrefabRoundTrip();
testPrefabDuplicate(); testPrefabDuplicate();
testPrefabValidation(); testPrefabValidation();
testWinGoalButton();
testCheckpointRespawn();
testCanvasLayout();
testUITextPlacement();
testHudRoundTrip();
testHierarchyMoves();
std::cout << "destrum tests passed\n"; std::cout << "destrum tests passed\n";
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }