feat: add basic canvas / improve inspector
This commit is contained in:
@@ -50,6 +50,11 @@ set(SRC_FILES
|
||||
"src/Components/Physics/Collider.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/Camera.cpp"
|
||||
"src/Graphics/DebugDraw.cpp"
|
||||
@@ -80,6 +85,7 @@ set(SRC_FILES
|
||||
"src/Graphics/Resources/NBuffer.cpp"
|
||||
"src/Graphics/Resources/Cubemap.cpp"
|
||||
"src/Graphics/Resources/Font.cpp"
|
||||
"src/Graphics/Resources/FontCache.cpp"
|
||||
"src/Graphics/Resources/PrimitiveMeshes.cpp"
|
||||
|
||||
"src/Graphics/Pipelines/MeshPipeline.cpp"
|
||||
@@ -117,6 +123,8 @@ set(SRC_FILES
|
||||
"src/Util/DeltaTime.cpp"
|
||||
"src/Util/ImGuiUtils.cpp"
|
||||
"src/Util/TransformGizmo.cpp"
|
||||
"src/Util/RectGizmo.cpp"
|
||||
"src/Util/SceneInspector.cpp"
|
||||
|
||||
"src/Physics/PhysicsWorld.cpp"
|
||||
"src/Physics/SimplePhysicsWorld.cpp"
|
||||
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
void AddImpulse(const glm::vec3& impulse);
|
||||
|
||||
void SetLinearVelocity(const glm::vec3& velocity);
|
||||
void Teleport(const glm::vec3& worldPosition);
|
||||
[[nodiscard]] glm::vec3 GetLinearVelocity() const;
|
||||
|
||||
[[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]] GameObject* GetOwner() const;
|
||||
|
||||
[[nodiscard]] int GetSiblingIndex() const;
|
||||
void SetSiblingIndex(int index);
|
||||
|
||||
private:
|
||||
void AddChild(Transform* transform);
|
||||
void RemoveChild(Transform* transform);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <destrum/Event.h>
|
||||
#include <destrum/ObjectModel/ObjectId.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Util/SceneInspector.h>
|
||||
|
||||
#include "destrum/Physics/JoltPhysicsWorld.h"
|
||||
#include "destrum/Physics/PhysicsSceneBridge.h"
|
||||
@@ -66,7 +67,15 @@ public:
|
||||
[[nodiscard]] const std::vector<std::shared_ptr<GameObject>>& GetObjects() const {
|
||||
return m_objects;
|
||||
}
|
||||
[[nodiscard]] const std::vector<std::shared_ptr<GameObject>>& GetPendingAdditions() const {
|
||||
return m_pendingAdditions;
|
||||
}
|
||||
[[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); }
|
||||
|
||||
void SetRegisterBindings(std::function<void()> registerBindings) {
|
||||
@@ -150,6 +159,7 @@ private:
|
||||
//Imgui vars
|
||||
bool m_ShowDemoWindow{false};
|
||||
ObjectId m_SelectedObjectId{InvalidObjectId};
|
||||
SceneInspector m_Inspector;
|
||||
|
||||
ObjectId m_CurrentCameraId{InvalidObjectId};
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.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) \
|
||||
X(MeshRendererComponent) \
|
||||
@@ -25,5 +29,9 @@
|
||||
X(BoxCollider) \
|
||||
X(SphereCollider) \
|
||||
X(CapsuleCollider) \
|
||||
X(Canvas) \
|
||||
X(RectTransform) \
|
||||
X(UIImage) \
|
||||
X(UIText) \
|
||||
|
||||
#endif // DESTRUM_ENGINECOMPONENTLIST_H
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <destrum/ObjectModel/ObjectId.h>
|
||||
#include <destrum/ObjectModel/ObjectRef.h>
|
||||
|
||||
class GameObject;
|
||||
class Scene;
|
||||
|
||||
using ImGuiInputTextFlags = int;
|
||||
|
||||
namespace ImGuiUtils {
|
||||
[[nodiscard]] bool DrawVec3Control(
|
||||
@@ -18,14 +23,17 @@ namespace ImGuiUtils {
|
||||
glm::vec3& value,
|
||||
float speed = 0.1f);
|
||||
|
||||
void RenderSceneInspector(
|
||||
std::string_view sceneName,
|
||||
const std::vector<std::shared_ptr<GameObject>>& objects,
|
||||
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
||||
ObjectId& selectedObjectId,
|
||||
const std::function<GameObject*()>& createGameObject,
|
||||
const std::function<void()>& drawSceneSettings,
|
||||
bool* open = nullptr);
|
||||
inline constexpr const char* GameObjectPayload = "DESTRUM_GAMEOBJECT";
|
||||
|
||||
bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags = 0, const char* hint = nullptr);
|
||||
bool InputStringMultiline(const char* label, std::string& value);
|
||||
|
||||
// Combo over the scene's objects, filter can hide objects that can't be picked
|
||||
bool DrawObjectRefPicker(
|
||||
const char* label,
|
||||
ObjectRef& ref,
|
||||
const Scene& scene,
|
||||
const std::function<bool(const GameObject&)>& filter = {});
|
||||
}
|
||||
|
||||
#endif // DESTRUM_IMGUIUTILS_H
|
||||
|
||||
@@ -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
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <destrum/App.h>
|
||||
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Resources/FontCache.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Util/DeltaTime.h>
|
||||
|
||||
@@ -358,6 +359,7 @@ void App::cleanup()
|
||||
attempt([this] { customCleanup(); });
|
||||
}
|
||||
attempt([] { SceneManager::GetInstance().Destroy(); });
|
||||
attempt([] { FontCache::GetInstance().Clear(); });
|
||||
attempt([this] { renderer.cleanup(gfxDevice); });
|
||||
attempt([this] { imguiPass.cleanup(); });
|
||||
attempt([this] { resources.cleanup(gfxDevice); });
|
||||
|
||||
@@ -91,6 +91,17 @@ void Rigidbody::SetLinearVelocity(const glm::vec3& 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 {
|
||||
if (!HasPhysicsBody()) {
|
||||
return glm::vec3{0.0f};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
if (transform == nullptr || transform == this) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -238,14 +237,68 @@ void Scene::SetCurrentCamera(const CameraComponent* camera) {
|
||||
}
|
||||
|
||||
void Scene::RenderImgui(bool* open) {
|
||||
ImGuiUtils::RenderSceneInspector(
|
||||
m_name,
|
||||
m_objects,
|
||||
m_pendingAdditions,
|
||||
m_SelectedObjectId,
|
||||
[this] { return CreateGameObject("GameObject"); },
|
||||
[this] { DrawSettingsImGui(); },
|
||||
open);
|
||||
m_Inspector.Draw(*this, m_SelectedObjectId, [this] { DrawSettingsImGui(); }, open);
|
||||
}
|
||||
|
||||
bool Scene::MoveInHierarchy(GameObject& object, GameObject* parent, const GameObject* insertBefore) {
|
||||
if (IsIterating() || object.IsBeingDestroyed() || object.GetScene() != this || parent == &object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
+51
-334
@@ -4,7 +4,6 @@
|
||||
#include <cfloat>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -15,90 +14,21 @@
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/ObjectModel/Transform.h>
|
||||
#include <destrum/Serialization/ComponentFactory.h>
|
||||
#include <destrum/Serialization/ComponentRegistry.h>
|
||||
#include <destrum/Serialization/PrefabSerializer.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
|
||||
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) {
|
||||
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 {
|
||||
@@ -110,7 +40,7 @@ namespace ImGuiUtils {
|
||||
ImGui::PushID(label);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine();
|
||||
ImGui::SameLine(ImGui::GetFontSize() * 5.0f);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
const bool changed = ImGui::DragFloat3(
|
||||
"##value",
|
||||
@@ -120,272 +50,59 @@ namespace ImGuiUtils {
|
||||
return changed;
|
||||
}
|
||||
|
||||
void RenderSceneInspector(
|
||||
std::string_view sceneName,
|
||||
const std::vector<std::shared_ptr<GameObject>>& objectsSource,
|
||||
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
|
||||
ObjectId& selectedObjectId,
|
||||
const std::function<GameObject*()>& createGameObject,
|
||||
const std::function<void()>& drawSceneSettings,
|
||||
bool* open) {
|
||||
RegisterEngineComponents();
|
||||
|
||||
std::vector<GameObject*> objects;
|
||||
objects.reserve(objectsSource.size() + pendingAdditions.size());
|
||||
|
||||
const auto appendInspectableObjects = [&objects](const auto& source) {
|
||||
for (const auto& object : source) {
|
||||
if (IsInspectableObject(object.get())) {
|
||||
objects.push_back(object.get());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
bool InputString(const char* label, std::string& value, ImGuiInputTextFlags flags, const char* hint)
|
||||
{
|
||||
flags |= ImGuiInputTextFlags_CallbackResize;
|
||||
if (hint != nullptr) {
|
||||
return ImGui::InputTextWithHint(label, hint, value.data(), value.capacity() + 1, flags, ResizeStringCallback, &value);
|
||||
}
|
||||
return ImGui::InputText(label, value.data(), value.capacity() + 1, flags, ResizeStringCallback, &value);
|
||||
}
|
||||
|
||||
if (FindSelectedObject(objects, selectedObjectId) == nullptr) {
|
||||
selectedObjectId = InvalidObjectId;
|
||||
bool InputStringMultiline(const char* label, std::string& value)
|
||||
{
|
||||
return ImGui::InputTextMultiline(label, value.data(), value.capacity() + 1, ImVec2{0.0f, ImGui::GetTextLineHeight() * 4.0f},
|
||||
ImGuiInputTextFlags_CallbackResize, ResizeStringCallback, &value);
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Inspector", open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
bool DrawObjectRefPicker(const char* label, ObjectRef& ref, const Scene& scene,
|
||||
const std::function<bool(const GameObject&)>& filter)
|
||||
{
|
||||
const GameObject* current = ref.Get(scene);
|
||||
const std::string preview = current != nullptr ? current->GetName()
|
||||
: ref.IsSet() ? "Missing object " + std::to_string(ref.GetId())
|
||||
: "None";
|
||||
|
||||
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();
|
||||
bool changed = false;
|
||||
if (ImGui::BeginCombo(label, preview.c_str())) {
|
||||
if (ImGui::Selectable("None", !ref.IsSet())) {
|
||||
ref = ObjectRef{};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
for (const auto& object : scene.GetObjects()) {
|
||||
if (!IsInspectableObject(object.get()) || (filter && !filter(*object))) {
|
||||
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::PushID(static_cast<int>(object->GetId()));
|
||||
if (ImGui::Selectable(object->GetName().c_str(), object.get() == current)) {
|
||||
ref = ObjectRef(*object);
|
||||
changed = true;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!hasComponents) {
|
||||
ImGui::TextDisabled("No components.");
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
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();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -13,14 +13,19 @@ set(GAME_SRC
|
||||
|
||||
src/components/GameComponentRegistry.cpp
|
||||
src/components/Activatable.cpp
|
||||
src/components/Checkpoint.cpp
|
||||
src/components/CrosshairUI.cpp
|
||||
src/components/Door.cpp
|
||||
src/components/FirstPersonController.cpp
|
||||
src/components/Interactable.cpp
|
||||
src/components/InteractableButton.cpp
|
||||
src/components/LightToggle.cpp
|
||||
src/components/MovingPlatform.cpp
|
||||
src/components/PlayerRespawn.cpp
|
||||
src/components/PrintComponent.cpp
|
||||
src/components/TriggerLoggerComponent.cpp
|
||||
src/components/WinGoal.cpp
|
||||
src/components/WinScreenUI.cpp
|
||||
)
|
||||
|
||||
add_executable(lightkeeper ${GAME_SRC})
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
#include <destrum/App.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Graphics/RenderResources.h>
|
||||
#include <destrum/Util/RectGizmo.h>
|
||||
#include <destrum/Util/TransformGizmo.h>
|
||||
|
||||
#include "destrum/Graphics/Resources/Cubemap.h"
|
||||
#include "destrum/Graphics/Resources/Font.h"
|
||||
#include "destrum/ObjectModel/GameObject.h"
|
||||
|
||||
#include <scenes/MainScene.h>
|
||||
@@ -31,7 +31,6 @@ public:
|
||||
|
||||
private:
|
||||
void setupSkybox();
|
||||
void drawCrosshair();
|
||||
void updateViewMode();
|
||||
[[nodiscard]] CameraComponent* getSceneCamera() const;
|
||||
[[nodiscard]] Camera& getViewCamera();
|
||||
@@ -59,8 +58,8 @@ private:
|
||||
MainSceneObjects mainScene{};
|
||||
|
||||
ImageID debug2DTexture{NULL_IMAGE_ID};
|
||||
Font hudFont;
|
||||
TransformGizmo transformGizmo;
|
||||
RectGizmo rectGizmo;
|
||||
|
||||
char scenePath[512]{"scenes/debug_scene.json"};
|
||||
char newSceneName[128]{"EmptyScene"};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef LIGHTKEEPER_CHECKPOINT_H
|
||||
#define LIGHTKEEPER_CHECKPOINT_H
|
||||
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
|
||||
class Checkpoint final : public Component {
|
||||
public:
|
||||
explicit Checkpoint(GameObject& parent);
|
||||
|
||||
std::string GetTypeName() const override { return "Checkpoint"; }
|
||||
|
||||
void OnTriggerEnter(GameObject* other) override;
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
|
||||
void SetSpawnOffset(const glm::vec3& offset) { m_SpawnOffset = offset; }
|
||||
[[nodiscard]] glm::vec3 GetSpawnPoint() const;
|
||||
|
||||
private:
|
||||
glm::vec3 m_SpawnOffset{0.0f};
|
||||
};
|
||||
|
||||
#endif // LIGHTKEEPER_CHECKPOINT_H
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef LIGHTKEEPER_CROSSHAIRUI_H
|
||||
#define LIGHTKEEPER_CROSSHAIRUI_H
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
#include <destrum/ObjectModel/ObjectRef.h>
|
||||
|
||||
// Shows the crosshair while the player is possessed, tints and grows it and shows the focus
|
||||
// indicator while the player looks at an Interactable
|
||||
class CrosshairUI final : public Component {
|
||||
public:
|
||||
explicit CrosshairUI(GameObject& parent);
|
||||
|
||||
std::string GetTypeName() const override { return "CrosshairUI"; }
|
||||
|
||||
void LateUpdate(float dt) override;
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ResolveReferences(const ObjectMap& objects) override;
|
||||
void ImGuiInspector() override;
|
||||
|
||||
void SetPlayer(const GameObject& player) { m_Player = ObjectRef(player); }
|
||||
void SetCrosshair(const GameObject& crosshair) { m_Crosshair = ObjectRef(crosshair); }
|
||||
void SetFocusIndicator(const GameObject& indicator) { m_FocusIndicator = ObjectRef(indicator); }
|
||||
|
||||
[[nodiscard]] const ObjectRef& GetPlayer() const { return m_Player; }
|
||||
[[nodiscard]] const ObjectRef& GetCrosshair() const { return m_Crosshair; }
|
||||
[[nodiscard]] const ObjectRef& GetFocusIndicator() const { return m_FocusIndicator; }
|
||||
|
||||
private:
|
||||
ObjectRef m_Player;
|
||||
ObjectRef m_Crosshair;
|
||||
ObjectRef m_FocusIndicator;
|
||||
|
||||
glm::vec4 m_Color{1.0f, 1.0f, 1.0f, 0.8f};
|
||||
glm::vec4 m_FocusedColor{1.0f, 0.8f, 0.25f, 1.0f};
|
||||
float m_Size{10.0f};
|
||||
float m_FocusedSize{16.0f};
|
||||
};
|
||||
|
||||
#endif // LIGHTKEEPER_CROSSHAIRUI_H
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
[[nodiscard]] float GetMouseSensitivity() const { return m_MouseSensitivity; }
|
||||
|
||||
[[nodiscard]] bool IsGrounded() const { return m_Grounded; }
|
||||
void Teleport(const glm::vec3& worldPosition);
|
||||
|
||||
[[nodiscard]] Interactable* GetFocusedInteractable() const { return m_FocusedInteractable; }
|
||||
[[nodiscard]] glm::vec3 GetLookDirection() const;
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
#ifndef LIGHTKEEPER_GAMECOMPONENTLIST_H
|
||||
#define LIGHTKEEPER_GAMECOMPONENTLIST_H
|
||||
|
||||
#include <components/Checkpoint.h>
|
||||
#include <components/CrosshairUI.h>
|
||||
#include <components/Door.h>
|
||||
#include <components/FirstPersonController.h>
|
||||
#include <components/InteractableButton.h>
|
||||
#include <components/LightToggle.h>
|
||||
#include <components/MovingPlatform.h>
|
||||
#include <components/PlayerRespawn.h>
|
||||
#include <components/PrintComponent.h>
|
||||
#include <components/TriggerLoggerComponent.h>
|
||||
#include <components/WinGoal.h>
|
||||
#include <components/WinScreenUI.h>
|
||||
|
||||
#define LIGHTKEEPER_GAME_COMPONENTS(X) \
|
||||
X(Checkpoint) \
|
||||
X(CrosshairUI) \
|
||||
X(Door) \
|
||||
X(FirstPersonController) \
|
||||
X(InteractableButton) \
|
||||
X(LightToggle) \
|
||||
X(MovingPlatform) \
|
||||
X(PlayerRespawn) \
|
||||
X(PrintComponent) \
|
||||
X(TriggerLoggerComponent)
|
||||
X(TriggerLoggerComponent) \
|
||||
X(WinGoal) \
|
||||
X(WinScreenUI)
|
||||
|
||||
#endif // LIGHTKEEPER_GAMECOMPONENTLIST_H
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef LIGHTKEEPER_PLAYERRESPAWN_H
|
||||
#define LIGHTKEEPER_PLAYERRESPAWN_H
|
||||
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
#include <destrum/ObjectModel/Component.h>
|
||||
|
||||
class PlayerRespawn final : public Component {
|
||||
public:
|
||||
explicit PlayerRespawn(GameObject& parent);
|
||||
|
||||
std::string GetTypeName() const override { return "PlayerRespawn"; }
|
||||
|
||||
void Start() override;
|
||||
void FixedUpdate(float fixedDt) override;
|
||||
|
||||
nlohmann::json Serialize() const override;
|
||||
void Deserialize(const nlohmann::json& data) override;
|
||||
void ImGuiInspector() override;
|
||||
|
||||
void SetRespawnPoint(const glm::vec3& worldPosition);
|
||||
[[nodiscard]] const glm::vec3& GetRespawnPoint() const { return m_RespawnPoint; }
|
||||
|
||||
void SetKillHeight(float height) { m_KillHeight = height; }
|
||||
[[nodiscard]] float GetKillHeight() const { return m_KillHeight; }
|
||||
|
||||
void Respawn();
|
||||
|
||||
private:
|
||||
glm::vec3 m_RespawnPoint{0.0f};
|
||||
bool m_HasRespawnPoint{false};
|
||||
float m_KillHeight{-15.0f};
|
||||
};
|
||||
|
||||
#endif // LIGHTKEEPER_PLAYERRESPAWN_H
|
||||
@@ -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
|
||||
@@ -19,7 +19,7 @@ struct MainSceneObjects {
|
||||
MaterialID sphereMaterial{};
|
||||
};
|
||||
|
||||
// Sun, ground, parkour course, models, trigger zone and the player
|
||||
// Sun, ground, parkour course, models, trigger zone, the player and the HUD
|
||||
MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources);
|
||||
|
||||
#endif // LIGHTKEEPER_MAINSCENE_H
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
#include <destrum/Components/CameraComponent.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Managers/Renderer2D.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
|
||||
#include <components/FirstPersonController.h>
|
||||
#include <components/Interactable.h>
|
||||
#include <components/GameComponentRegistry.h>
|
||||
|
||||
LightKeeper::LightKeeper() : App()
|
||||
@@ -43,9 +41,6 @@ void LightKeeper::customInit()
|
||||
debug2DTexture = resources.loadImageFromFile(
|
||||
gfxDevice, AssetFS::GetInstance().GetFullPath("engine://textures/kobe.png"));
|
||||
|
||||
hudFont = Font::LoadFromFile(AssetFS::GetInstance().GetFullPath("engine://fonts/Roboto-Medium.ttf"));
|
||||
hudFont.Upload(gfxDevice, resources);
|
||||
|
||||
}
|
||||
|
||||
void LightKeeper::setupSkybox()
|
||||
@@ -103,39 +98,6 @@ void LightKeeper::updateViewMode()
|
||||
}
|
||||
}
|
||||
|
||||
void LightKeeper::drawCrosshair()
|
||||
{
|
||||
if (!isPlayerViewActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool focused = mainScene.playerController->GetFocusedInteractable() != nullptr;
|
||||
const glm::vec4 color = focused ? glm::vec4{1.0f, 0.8f, 0.25f, 1.0f} : glm::vec4{1.0f, 1.0f, 1.0f, 0.8f};
|
||||
const float armLength = focused ? 16.0f : 10.0f;
|
||||
constexpr float thickness = 2.0f;
|
||||
|
||||
const VkExtent3D extent = renderer.getDrawImage().extent;
|
||||
const glm::vec2 center{static_cast<float>(extent.width) * 0.5f, static_cast<float>(extent.height) * 0.5f};
|
||||
|
||||
auto& renderer2D = Renderer2D::GetInstance();
|
||||
const float halfArm = armLength * 0.5f;
|
||||
renderer2D.DrawLine(center - glm::vec2{halfArm, 0.0f}, center + glm::vec2{halfArm, 0.0f}, thickness, color);
|
||||
renderer2D.DrawLine(center - glm::vec2{0.0f, halfArm}, center + glm::vec2{0.0f, halfArm}, thickness, color);
|
||||
if (focused) {
|
||||
renderer2D.DrawEllipseOutline(center, glm::vec2{14.0f}, thickness, color);
|
||||
|
||||
constexpr std::string_view prompt = "[E] Interact";
|
||||
constexpr float promptSize = 22.0f;
|
||||
const glm::vec2 promptExtent = hudFont.MeasureText(prompt, promptSize);
|
||||
renderer2D.DrawText(
|
||||
hudFont,
|
||||
prompt,
|
||||
center + glm::vec2{-promptExtent.x * 0.5f, 28.0f},
|
||||
promptSize,
|
||||
TextStyle{.color = color, .outline = 2.0f, .outlineColor = {0.0f, 0.0f, 0.0f, 0.75f}});
|
||||
}
|
||||
}
|
||||
|
||||
bool LightKeeper::isPlayerViewActive() const
|
||||
{
|
||||
return mainScene.playerController != nullptr && mainScene.playerController->IsPossessed();
|
||||
@@ -165,7 +127,6 @@ void LightKeeper::customDraw()
|
||||
|
||||
renderer.beginDrawing(gfxDevice);
|
||||
submitDebugDraws();
|
||||
drawCrosshair();
|
||||
|
||||
if (isPlayerViewActive())
|
||||
{
|
||||
|
||||
@@ -190,6 +190,12 @@ void LightKeeper::drawSelectionGizmo()
|
||||
return;
|
||||
}
|
||||
|
||||
if (RectGizmo::CanManipulate(*selected)) {
|
||||
const VkExtent3D extent = renderer.getDrawImage().extent;
|
||||
rectGizmo.Manipulate(*selected, glm::vec2{static_cast<float>(extent.width), static_cast<float>(extent.height)});
|
||||
return;
|
||||
}
|
||||
|
||||
transformGizmo.HandleShortcuts();
|
||||
transformGizmo.DrawToolbar();
|
||||
transformGizmo.Manipulate(*selected, getViewCamera());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#include <components/CrosshairUI.h>
|
||||
|
||||
#include <components/FirstPersonController.h>
|
||||
|
||||
#include <destrum/Components/UI/RectTransform.h>
|
||||
#include <destrum/Components/UI/UIGraphic.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
#include <destrum/Util/ImGuiUtils.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace {
|
||||
void TintHierarchy(GameObject& object, const glm::vec4& color)
|
||||
{
|
||||
for (const auto& component : object.GetComponents()) {
|
||||
if (auto* graphic = dynamic_cast<UIGraphic*>(component.get())) {
|
||||
graphic->SetColor(color);
|
||||
}
|
||||
}
|
||||
for (Transform* child : object.GetTransform().GetChildren()) {
|
||||
if (child != nullptr && child->GetOwner() != nullptr) {
|
||||
TintHierarchy(*child->GetOwner(), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CrosshairUI::CrosshairUI(GameObject& parent)
|
||||
: Component(parent, "CrosshairUI")
|
||||
{
|
||||
}
|
||||
|
||||
void CrosshairUI::LateUpdate(float)
|
||||
{
|
||||
const Scene* scene = GetGameObject()->GetScene();
|
||||
if (scene == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject* player = m_Player.Get(*scene);
|
||||
const auto* controller = player != nullptr ? player->GetComponent<FirstPersonController>() : nullptr;
|
||||
const bool possessed = controller != nullptr && controller->IsPossessed();
|
||||
const bool focused = possessed && controller->GetFocusedInteractable() != nullptr;
|
||||
|
||||
if (GameObject* indicator = m_FocusIndicator.Get(*scene)) {
|
||||
indicator->SetActive(focused);
|
||||
}
|
||||
|
||||
GameObject* crosshair = m_Crosshair.Get(*scene);
|
||||
if (crosshair == nullptr) {
|
||||
return;
|
||||
}
|
||||
crosshair->SetActive(possessed);
|
||||
if (auto* rectTransform = crosshair->GetComponent<RectTransform>()) {
|
||||
rectTransform->SetSize(glm::vec2{focused ? m_FocusedSize : m_Size});
|
||||
}
|
||||
TintHierarchy(*crosshair, focused ? m_FocusedColor : m_Color);
|
||||
}
|
||||
|
||||
nlohmann::json CrosshairUI::Serialize() const
|
||||
{
|
||||
return {
|
||||
{"player", m_Player},
|
||||
{"crosshair", m_Crosshair},
|
||||
{"focusIndicator", m_FocusIndicator},
|
||||
{"color", m_Color},
|
||||
{"focusedColor", m_FocusedColor},
|
||||
{"size", m_Size},
|
||||
{"focusedSize", m_FocusedSize},
|
||||
};
|
||||
}
|
||||
|
||||
void CrosshairUI::Deserialize(const nlohmann::json& data)
|
||||
{
|
||||
m_Player = data.value("player", ObjectRef{});
|
||||
m_Crosshair = data.value("crosshair", ObjectRef{});
|
||||
m_FocusIndicator = data.value("focusIndicator", ObjectRef{});
|
||||
m_Color = data.value("color", m_Color);
|
||||
m_FocusedColor = data.value("focusedColor", m_FocusedColor);
|
||||
m_Size = data.value("size", m_Size);
|
||||
m_FocusedSize = data.value("focusedSize", m_FocusedSize);
|
||||
}
|
||||
|
||||
void CrosshairUI::ResolveReferences(const ObjectMap& objects)
|
||||
{
|
||||
m_Player.Resolve(objects);
|
||||
m_Crosshair.Resolve(objects);
|
||||
m_FocusIndicator.Resolve(objects);
|
||||
}
|
||||
|
||||
void CrosshairUI::ImGuiInspector()
|
||||
{
|
||||
if (const Scene* scene = GetGameObject()->GetScene()) {
|
||||
ImGuiUtils::DrawObjectRefPicker("Player", m_Player, *scene, [](const GameObject& object) {
|
||||
return object.GetComponent<FirstPersonController>() != nullptr;
|
||||
});
|
||||
ImGuiUtils::DrawObjectRefPicker("Crosshair", m_Crosshair, *scene);
|
||||
ImGuiUtils::DrawObjectRefPicker("Focus Indicator", m_FocusIndicator, *scene);
|
||||
}
|
||||
|
||||
ImGui::ColorEdit4("Color", &m_Color.x);
|
||||
ImGui::ColorEdit4("Focused Color", &m_FocusedColor.x);
|
||||
ImGui::DragFloat("Size", &m_Size, 0.5f, 0.0f, 500.0f);
|
||||
ImGui::DragFloat("Focused Size", &m_FocusedSize, 0.5f, 0.0f, 500.0f);
|
||||
}
|
||||
@@ -132,6 +132,17 @@ Interactable* FirstPersonController::FindFocusedInteractable() const
|
||||
return Interactable::FindOnObjectOrParents(hit.object);
|
||||
}
|
||||
|
||||
void FirstPersonController::Teleport(const glm::vec3& worldPosition)
|
||||
{
|
||||
if (m_Rigidbody != nullptr) {
|
||||
m_Rigidbody->Teleport(worldPosition);
|
||||
} else {
|
||||
GetTransform().SetWorldPosition(worldPosition);
|
||||
}
|
||||
m_PreviousPosition = worldPosition;
|
||||
m_HasPreviousPosition = true;
|
||||
}
|
||||
|
||||
void FirstPersonController::FixedUpdate(float fixedDt)
|
||||
{
|
||||
if (m_Rigidbody == nullptr || !m_Rigidbody->HasPhysicsBody())
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <components/PlayerRespawn.h>
|
||||
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Serialization/GlmJson.h>
|
||||
|
||||
#include <components/FirstPersonController.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
PlayerRespawn::PlayerRespawn(GameObject& parent)
|
||||
: Component(parent, "PlayerRespawn")
|
||||
{
|
||||
}
|
||||
|
||||
void PlayerRespawn::Start()
|
||||
{
|
||||
if (!m_HasRespawnPoint) {
|
||||
SetRespawnPoint(GetTransform().GetWorldPosition());
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRespawn::FixedUpdate(float)
|
||||
{
|
||||
if (GetTransform().GetWorldPosition().y < m_KillHeight) {
|
||||
Respawn();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRespawn::SetRespawnPoint(const glm::vec3& worldPosition)
|
||||
{
|
||||
m_RespawnPoint = worldPosition;
|
||||
m_HasRespawnPoint = true;
|
||||
}
|
||||
|
||||
void PlayerRespawn::Respawn()
|
||||
{
|
||||
GameObject* object = GetGameObject();
|
||||
if (auto* controller = object->GetComponent<FirstPersonController>()) {
|
||||
controller->Teleport(m_RespawnPoint);
|
||||
} else if (auto* rigidbody = object->GetComponent<Rigidbody>()) {
|
||||
rigidbody->Teleport(m_RespawnPoint);
|
||||
} else {
|
||||
GetTransform().SetWorldPosition(m_RespawnPoint);
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json PlayerRespawn::Serialize() const
|
||||
{
|
||||
nlohmann::json data{{"killHeight", m_KillHeight}};
|
||||
if (m_HasRespawnPoint) {
|
||||
data["respawnPoint"] = m_RespawnPoint;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void PlayerRespawn::Deserialize(const nlohmann::json& data)
|
||||
{
|
||||
m_KillHeight = data.value("killHeight", m_KillHeight);
|
||||
if (data.contains("respawnPoint")) {
|
||||
SetRespawnPoint(data.at("respawnPoint").get<glm::vec3>());
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRespawn::ImGuiInspector()
|
||||
{
|
||||
ImGui::DragFloat("Kill height", &m_KillHeight, 0.1f);
|
||||
ImGui::DragFloat3("Respawn point", glm::value_ptr(m_RespawnPoint), 0.1f);
|
||||
if (ImGui::Button("Respawn now")) {
|
||||
Respawn();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -7,6 +7,10 @@
|
||||
#include <destrum/Components/Physics/BoxCollider.h>
|
||||
#include <destrum/Components/Physics/CapsuleCollider.h>
|
||||
#include <destrum/Components/Physics/Rigidbody.h>
|
||||
#include <destrum/Components/UI/Canvas.h>
|
||||
#include <destrum/Components/UI/RectTransform.h>
|
||||
#include <destrum/Components/UI/UIImage.h>
|
||||
#include <destrum/Components/UI/UIText.h>
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/RenderResources.h>
|
||||
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
|
||||
@@ -16,11 +20,16 @@
|
||||
#include <destrum/Util/ModelDocUtils.h>
|
||||
|
||||
#include <components/FirstPersonController.h>
|
||||
#include <components/Checkpoint.h>
|
||||
#include <components/CrosshairUI.h>
|
||||
#include <components/Door.h>
|
||||
#include <components/InteractableButton.h>
|
||||
#include <components/LightToggle.h>
|
||||
#include <components/MovingPlatform.h>
|
||||
#include <components/PlayerRespawn.h>
|
||||
#include <components/TriggerLoggerComponent.h>
|
||||
#include <components/WinGoal.h>
|
||||
#include <components/WinScreenUI.h>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
@@ -262,6 +271,167 @@ namespace {
|
||||
{2.0f, platformHeight, platformSize.z}, rampMaterial, RigidbodyType::Static);
|
||||
}
|
||||
|
||||
// Floats over the void past the west edge of the ground. The controller jumps 1.3 m and sprints 8.5 m/s,
|
||||
// which carries about 5 m on a level jump, the hardest gaps stay a little under that
|
||||
WinGoal* CreateWinCourse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources)
|
||||
{
|
||||
const MaterialID checkpointMaterial = AddPrototypeMaterial(gfxDevice, resources, "Green", 0.6f);
|
||||
const MaterialID pillarMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.6f);
|
||||
const MaterialID stoneMaterial = AddPrototypeMaterial(gfxDevice, resources, "Purple", 0.5f);
|
||||
const MaterialID beamMaterial = AddPrototypeMaterial(gfxDevice, resources, "Dark", 0.9f);
|
||||
const MaterialID movingMaterial = AddPrototypeMaterial(gfxDevice, resources, "Light", 0.5f);
|
||||
const MaterialID goalMaterial = AddPrototypeMaterial(gfxDevice, resources, "Red", 0.6f);
|
||||
|
||||
constexpr float slabThickness = 0.5f;
|
||||
|
||||
GameObject* course = scene.CreateGameObject("WinCourse");
|
||||
course->GetTransform().SetWorldPosition({-GroundHalfExtent, GroundHeight, 0.0f});
|
||||
const auto addToCourse = [&](GameObject* object)
|
||||
{
|
||||
object->GetTransform().SetParent(&course->GetTransform(), true);
|
||||
return object;
|
||||
};
|
||||
|
||||
// Top center and half size of the last placed platform, the course runs along -X
|
||||
glm::vec3 top{-GroundHalfExtent - 2.5f, GroundHeight, 0.0f};
|
||||
glm::vec2 half{2.5f, 2.5f};
|
||||
int platformCount = 0;
|
||||
int checkpointCount = 0;
|
||||
|
||||
const auto slab = [&](const std::string& name, const glm::vec3& topCenter, const glm::vec2& size,
|
||||
MaterialID materialID, RigidbodyType type)
|
||||
{
|
||||
return addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_" + name,
|
||||
topCenter - glm::vec3{0.0f, slabThickness * 0.5f, 0.0f},
|
||||
{size.x, slabThickness, size.y}, materialID, type));
|
||||
};
|
||||
|
||||
// gap is the edge to edge distance along X, rise and dz move the top center
|
||||
const auto next = [&](float gap, float rise, float dz, const glm::vec2& size, MaterialID materialID,
|
||||
RigidbodyType type = RigidbodyType::Static)
|
||||
{
|
||||
top = glm::vec3{top.x - half.x - gap - size.x * 0.5f, top.y + rise, top.z + dz};
|
||||
half = size * 0.5f;
|
||||
return slab("Platform" + std::to_string(++platformCount), top, size, materialID, type);
|
||||
};
|
||||
|
||||
const auto mover = [&](float gap, float rise, float dz, const glm::vec2& size, const glm::vec3& travel,
|
||||
float travelDuration, float pauseDuration)
|
||||
{
|
||||
auto* platform = next(gap, rise, dz, size, movingMaterial, RigidbodyType::Kinematic)
|
||||
->AddComponent<MovingPlatform>();
|
||||
platform->SetTravelOffset(travel);
|
||||
platform->SetTravelDuration(travelDuration);
|
||||
platform->SetPauseDuration(pauseDuration);
|
||||
};
|
||||
|
||||
const auto addCheckpoint = [&]()
|
||||
{
|
||||
GameObject* trigger = addToCourse(
|
||||
scene.CreateGameObject("WinCourse_Checkpoint" + std::to_string(++checkpointCount)));
|
||||
trigger->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 1.0f, 0.0f});
|
||||
trigger->AddComponent<BoxCollider>(glm::vec3{half.x, 1.0f, half.y})->SetTrigger(true);
|
||||
trigger->AddComponent<Rigidbody>()->SetType(RigidbodyType::Static);
|
||||
trigger->AddComponent<Checkpoint>()->SetSpawnOffset({0.0f, 0.2f, 0.0f});
|
||||
scene.GetPhysics().RegisterGameObject(*trigger);
|
||||
};
|
||||
|
||||
// The start pad touches the ground edge so it can be walked onto
|
||||
slab("Start", top, {5.0f, 5.0f}, checkpointMaterial, RigidbodyType::Static);
|
||||
addCheckpoint();
|
||||
|
||||
// Warm-up hops
|
||||
next(2.0f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
|
||||
next(2.4f, 0.5f, 1.5f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(2.6f, 0.5f, -3.0f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(2.8f, 0.0f, 1.5f, {1.2f, 1.2f}, pillarMaterial);
|
||||
next(2.2f, -0.5f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// Zig-zag over shrinking stones
|
||||
next(2.6f, 0.0f, 1.0f, {1.0f, 1.0f}, stoneMaterial);
|
||||
next(2.8f, 0.4f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
|
||||
next(2.8f, 0.4f, 2.0f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(3.0f, 0.4f, -2.0f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(3.2f, -0.8f, 1.0f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(2.5f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// Offset balance beams, then long sprint jumps
|
||||
next(1.2f, 0.0f, 0.0f, {6.0f, 0.4f}, beamMaterial);
|
||||
next(1.4f, 0.3f, 0.9f, {5.0f, 0.35f}, beamMaterial);
|
||||
next(1.4f, 0.3f, -1.8f, {4.0f, 0.3f}, beamMaterial);
|
||||
next(3.6f, 0.0f, 0.9f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(4.0f, -0.6f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
|
||||
next(3.8f, 0.0f, 0.0f, {1.2f, 1.2f}, pillarMaterial);
|
||||
next(3.0f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// A ferry over a 9 m hole, then sideways movers that drift out of sync
|
||||
constexpr float ferryTravel = 9.0f;
|
||||
mover(0.6f, 0.0f, 0.0f, {2.5f, 2.5f}, {-ferryTravel, 0.0f, 0.0f}, 3.5f, 1.0f);
|
||||
top.x -= ferryTravel;
|
||||
next(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, pillarMaterial);
|
||||
|
||||
constexpr float sway = 3.0f;
|
||||
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 2.0f, 0.3f);
|
||||
top.z += sway;
|
||||
mover(2.2f, 0.0f, sway, {1.5f, 1.5f}, {0.0f, 0.0f, -sway * 2.0f}, 2.6f, 0.3f);
|
||||
top.z -= sway;
|
||||
mover(2.2f, 0.0f, -sway, {1.5f, 1.5f}, {0.0f, 0.0f, sway * 2.0f}, 1.7f, 0.3f);
|
||||
top.z += sway;
|
||||
next(2.2f, 0.0f, 0.0f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// An elevator up to the last stretch
|
||||
constexpr float liftHeight = 5.0f;
|
||||
mover(0.6f, 0.0f, 0.0f, {2.0f, 2.0f}, {0.0f, liftHeight, 0.0f}, 3.0f, 1.5f);
|
||||
top.y += liftHeight;
|
||||
next(1.2f, 0.0f, 0.0f, {1.5f, 1.5f}, pillarMaterial);
|
||||
|
||||
next(2.8f, 0.5f, 1.2f, {0.8f, 0.8f}, stoneMaterial);
|
||||
next(3.0f, 0.5f, -2.4f, {0.7f, 0.7f}, stoneMaterial);
|
||||
next(3.2f, 0.0f, 2.4f, {0.6f, 0.6f}, stoneMaterial);
|
||||
next(3.4f, -0.4f, -2.4f, {0.6f, 0.6f}, stoneMaterial);
|
||||
next(2.5f, 0.0f, 1.2f, {3.0f, 3.0f}, checkpointMaterial);
|
||||
addCheckpoint();
|
||||
|
||||
// Final gauntlet: a fast sideways mover and three tiny stones
|
||||
constexpr float fastSway = 2.5f;
|
||||
mover(2.4f, 0.0f, -fastSway, {1.2f, 1.2f}, {0.0f, 0.0f, fastSway * 2.0f}, 1.6f, 0.2f);
|
||||
top.z += fastSway;
|
||||
next(2.6f, 0.8f, 0.0f, {1.0f, 1.0f}, stoneMaterial);
|
||||
next(3.4f, 0.0f, 1.0f, {0.8f, 0.8f}, stoneMaterial);
|
||||
next(3.6f, -0.3f, -2.0f, {0.8f, 0.8f}, stoneMaterial);
|
||||
|
||||
GameObject* goal = next(2.5f, 0.5f, 1.0f, {4.0f, 4.0f}, goalMaterial);
|
||||
goal->SetName("WinCourse_Goal");
|
||||
auto* winGoal = goal->AddComponent<WinGoal>();
|
||||
|
||||
const glm::vec3 buttonBase = top + glm::vec3{-1.0f, 0.0f, 0.0f};
|
||||
addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_ButtonPedestal",
|
||||
buttonBase + glm::vec3{0.0f, 0.55f, 0.0f}, {0.4f, 1.1f, 0.4f},
|
||||
beamMaterial, RigidbodyType::Static));
|
||||
|
||||
// Every button needs its own material, pressing it recolors the material
|
||||
Material buttonMaterial = Material::SimpleColor(glm::vec3{1.0f, 0.85f, 0.2f}, "WinButtonMaterial");
|
||||
buttonMaterial.roughnessFactor = 0.4f;
|
||||
GameObject* button = addToCourse(CreateBox(scene, gfxDevice, resources, "WinCourse_Button",
|
||||
buttonBase + glm::vec3{0.0f, 1.15f, 0.0f}, {0.25f, 0.1f, 0.25f},
|
||||
resources.materials().addMaterial(std::move(buttonMaterial)),
|
||||
RigidbodyType::Static));
|
||||
button->AddComponent<InteractableButton>()->AddTarget(*goal);
|
||||
|
||||
GameObject* goalLight = addToCourse(scene.CreateGameObject("WinCourse_GoalLight"));
|
||||
goalLight->GetTransform().SetWorldPosition(top + glm::vec3{0.0f, 3.0f, 0.0f});
|
||||
auto* light = goalLight->AddComponent<LightComponent>(LightType::Point);
|
||||
light->SetColor(glm::vec3{1.0f, 0.8f, 0.3f});
|
||||
light->SetIntensity(6.0f);
|
||||
light->SetRange(8.0f);
|
||||
|
||||
return winGoal;
|
||||
}
|
||||
|
||||
struct WallOpening {
|
||||
float offset;
|
||||
float width;
|
||||
@@ -493,12 +663,97 @@ namespace {
|
||||
// Facing the parkour course
|
||||
auto* controller = playerObj->AddComponent<FirstPersonController>();
|
||||
controller->SetYaw(glm::half_pi<float>());
|
||||
playerObj->AddComponent<PlayerRespawn>();
|
||||
scene.GetPhysics().RegisterGameObject(*playerObj);
|
||||
|
||||
objects.player = playerObj;
|
||||
objects.playerController = controller;
|
||||
}
|
||||
|
||||
// Fills its parent until it is given anchors or a size
|
||||
RectTransform* CreateUIObject(Scene& scene, const std::string& name, GameObject& parent)
|
||||
{
|
||||
GameObject* object = scene.CreateGameObject(name);
|
||||
object->GetTransform().SetParent(&parent.GetTransform(), false);
|
||||
auto* rect = object->AddComponent<RectTransform>();
|
||||
rect->Stretch();
|
||||
return rect;
|
||||
}
|
||||
|
||||
GameObject* CreateCrosshair(Scene& scene, GameObject& hud)
|
||||
{
|
||||
auto* crosshair = CreateUIObject(scene, "Crosshair", hud);
|
||||
crosshair->SetAnchor({0.5f, 0.5f});
|
||||
crosshair->SetSize({10.0f, 10.0f});
|
||||
|
||||
constexpr float thickness = 2.0f;
|
||||
auto* horizontal = CreateUIObject(scene, "Crosshair_Horizontal", *crosshair->GetGameObject());
|
||||
horizontal->SetAnchors({0.0f, 0.5f}, {1.0f, 0.5f});
|
||||
horizontal->SetSize({0.0f, thickness});
|
||||
horizontal->GetGameObject()->AddComponent<UIImage>();
|
||||
|
||||
auto* vertical = CreateUIObject(scene, "Crosshair_Vertical", *crosshair->GetGameObject());
|
||||
vertical->SetAnchors({0.5f, 0.0f}, {0.5f, 1.0f});
|
||||
vertical->SetSize({thickness, 0.0f});
|
||||
vertical->GetGameObject()->AddComponent<UIImage>();
|
||||
|
||||
return crosshair->GetGameObject();
|
||||
}
|
||||
|
||||
GameObject* CreateFocusIndicator(Scene& scene, GameObject& crosshair)
|
||||
{
|
||||
GameObject* indicator = CreateUIObject(scene, "FocusIndicator", crosshair)->GetGameObject();
|
||||
|
||||
auto* ring = CreateUIObject(scene, "FocusRing", *indicator);
|
||||
ring->SetAnchor({0.5f, 0.5f});
|
||||
ring->SetSize({28.0f, 28.0f});
|
||||
auto* ringImage = ring->GetGameObject()->AddComponent<UIImage>();
|
||||
ringImage->SetShape(UIImageShape::Ellipse);
|
||||
ringImage->SetOutline(2.0f);
|
||||
|
||||
auto* prompt = CreateUIObject(scene, "InteractPrompt", *indicator);
|
||||
prompt->SetAnchor({0.5f, 0.5f});
|
||||
prompt->SetPivot({0.5f, 0.0f});
|
||||
prompt->SetPosition({0.0f, 28.0f});
|
||||
prompt->SetSize({400.0f, 30.0f});
|
||||
auto* promptText = prompt->GetGameObject()->AddComponent<UIText>("[E] Interact", 22.0f);
|
||||
promptText->SetOutline(2.0f, {0.0f, 0.0f, 0.0f, 0.75f});
|
||||
promptText->SetAlignment(UITextAlign::Center, UITextAlign::Start);
|
||||
|
||||
return indicator;
|
||||
}
|
||||
|
||||
GameObject* CreateWinScreen(Scene& scene, GameObject& hud)
|
||||
{
|
||||
GameObject* screen = CreateUIObject(scene, "WinScreen", hud)->GetGameObject();
|
||||
screen->AddComponent<UIImage>(glm::vec4{0.0f, 0.0f, 0.0f, 0.35f});
|
||||
|
||||
auto* titleText = CreateUIObject(scene, "WinScreen_Title", *screen)->GetGameObject()->AddComponent<UIText>("YOU WIN", 195.0f);
|
||||
titleText->SetColor({1.0f, 0.85f, 0.2f, 1.0f});
|
||||
titleText->SetOutline(4.0f, {0.0f, 0.0f, 0.0f, 0.9f});
|
||||
|
||||
screen->SetActive(false);
|
||||
return screen;
|
||||
}
|
||||
|
||||
// Sizes are in 1080p pixels, the canvas scales them with the screen height
|
||||
void CreateHud(Scene& scene, const GameObject& player, const GameObject& winGoal)
|
||||
{
|
||||
GameObject* hud = scene.CreateGameObject("HUD");
|
||||
hud->AddComponent<Canvas>();
|
||||
|
||||
GameObject* crosshair = CreateCrosshair(scene, *hud);
|
||||
const GameObject* focusIndicator = CreateFocusIndicator(scene, *crosshair);
|
||||
auto* crosshairUi = hud->AddComponent<CrosshairUI>();
|
||||
crosshairUi->SetPlayer(player);
|
||||
crosshairUi->SetCrosshair(*crosshair);
|
||||
crosshairUi->SetFocusIndicator(*focusIndicator);
|
||||
|
||||
auto* winScreenUi = hud->AddComponent<WinScreenUI>();
|
||||
winScreenUi->SetGoal(winGoal);
|
||||
winScreenUi->SetScreen(*CreateWinScreen(scene, *hud));
|
||||
}
|
||||
|
||||
void LoadSpawnBallResources(GfxDevice& gfxDevice, RenderResources& resources, MainSceneObjects& objects)
|
||||
{
|
||||
const auto sphereModel = AssetManager::GetInstance().LoadModel("engine://sphere.fbx", StaticModelOptions());
|
||||
@@ -528,7 +783,9 @@ MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResour
|
||||
CreateButton(scene, gfxDevice, resources, *door);
|
||||
CreateHouse(scene, gfxDevice, resources);
|
||||
CreateMovingPlatform(scene, gfxDevice, resources);
|
||||
const WinGoal* winGoal = CreateWinCourse(scene, gfxDevice, resources);
|
||||
CreatePlayer(scene, objects);
|
||||
CreateHud(scene, *objects.player, *winGoal->GetGameObject());
|
||||
LoadSpawnBallResources(gfxDevice, resources, objects);
|
||||
|
||||
return objects;
|
||||
|
||||
@@ -3,6 +3,12 @@ add_executable(destrum_tests
|
||||
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Activatable.cpp
|
||||
${CMAKE_SOURCE_DIR}/lightkeeper/src/components/Interactable.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
|
||||
|
||||
+324
-1
@@ -36,6 +36,10 @@
|
||||
#include <destrum/Components/Physics/Collider.h>
|
||||
#include <destrum/Components/Animator.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/Managers/Renderer2D.h>
|
||||
#include <destrum/Graphics/Resources/Font.h>
|
||||
@@ -49,6 +53,12 @@
|
||||
|
||||
#include <components/Activatable.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 {
|
||||
class TestComponent final : public Component {
|
||||
@@ -252,7 +262,11 @@ namespace {
|
||||
"Rigidbody",
|
||||
"BoxCollider",
|
||||
"SphereCollider",
|
||||
"CapsuleCollider"
|
||||
"CapsuleCollider",
|
||||
"Canvas",
|
||||
"RectTransform",
|
||||
"UIImage",
|
||||
"UIText"
|
||||
};
|
||||
for (const char* name : canonicalNames) {
|
||||
check(ComponentFactory::IsRegistered(name),
|
||||
@@ -1745,6 +1759,309 @@ namespace {
|
||||
const json future = json::parse(R"({"version":99,"objects":[{"id":1}]})");
|
||||
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()
|
||||
@@ -1796,6 +2113,12 @@ int main()
|
||||
testPrefabRoundTrip();
|
||||
testPrefabDuplicate();
|
||||
testPrefabValidation();
|
||||
testWinGoalButton();
|
||||
testCheckpointRespawn();
|
||||
testCanvasLayout();
|
||||
testUITextPlacement();
|
||||
testHudRoundTrip();
|
||||
testHierarchyMoves();
|
||||
std::cout << "destrum tests passed\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user