diff --git a/.gitmodules b/.gitmodules index 8ad20a2..0a160ce 100644 --- a/.gitmodules +++ b/.gitmodules @@ -51,3 +51,6 @@ [submodule "destrum/third_party/tracy"] path = destrum/third_party/tracy url = https://github.com/wolfpld/tracy.git +[submodule "destrum/third_party/imguizmo"] + path = destrum/third_party/imguizmo + url = https://github.com/CedricGuillemet/ImGuizmo.git diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 7b3b534..06e8d66 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -112,6 +112,7 @@ set(SRC_FILES "src/Util/DeltaTime.cpp" "src/Util/ImGuiUtils.cpp" + "src/Util/TransformGizmo.cpp" "src/Physics/PhysicsWorld.cpp" "src/Physics/SimplePhysicsWorld.cpp" @@ -150,6 +151,7 @@ target_link_libraries(destrum tinygltf assimp imgui + imguizmo Jolt Tracy::TracyClient diff --git a/destrum/include/destrum/Components/Physics/Collider.h b/destrum/include/destrum/Components/Physics/Collider.h index a4280ce..b6b4302 100644 --- a/destrum/include/destrum/Components/Physics/Collider.h +++ b/destrum/include/destrum/Components/Physics/Collider.h @@ -13,6 +13,8 @@ public: ~Collider() override = default; + [[nodiscard]] bool AffectsPhysics() const override { return true; } + void ImGuiInspector() override; [[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0; diff --git a/destrum/include/destrum/Components/Physics/Rigidbody.h b/destrum/include/destrum/Components/Physics/Rigidbody.h index d927ee4..d004ce0 100644 --- a/destrum/include/destrum/Components/Physics/Rigidbody.h +++ b/destrum/include/destrum/Components/Physics/Rigidbody.h @@ -15,6 +15,8 @@ public: ~Rigidbody() override; + [[nodiscard]] bool AffectsPhysics() const override { return true; } + void Update(float) override {} void Start() override; void Destroy() override; diff --git a/destrum/include/destrum/Graphics/Camera.h b/destrum/include/destrum/Graphics/Camera.h index ae88b3d..c835524 100644 --- a/destrum/include/destrum/Graphics/Camera.h +++ b/destrum/include/destrum/Graphics/Camera.h @@ -126,6 +126,9 @@ public: void SetMovementSpeed(float speed) { m_movementSpeed = speed; } [[nodiscard]] float GetMovementSpeed() const { return m_movementSpeed; } + void SetMouseSensitivity(float sensitivity) { m_mouseSensitivity = sensitivity; } + [[nodiscard]] float GetMouseSensitivity() const { return m_mouseSensitivity; } + void SetRotation(float yawRadians, float pitchRadians); void SetRotation(const glm::vec2& yawPitchRadians); void SetOrientation(const glm::vec3& forward, const glm::vec3& up); @@ -168,6 +171,7 @@ private: Frustum m_frustum{}; float m_movementSpeed{2.0f}; + float m_mouseSensitivity{0.003f}; float m_yaw = 0.0f; // radians float m_pitch = 0.0f; // radians diff --git a/destrum/include/destrum/Graphics/GfxDevice.h b/destrum/include/destrum/Graphics/GfxDevice.h index 273476a..0ed3b04 100644 --- a/destrum/include/destrum/Graphics/GfxDevice.h +++ b/destrum/include/destrum/Graphics/GfxDevice.h @@ -63,7 +63,7 @@ public: void init(SDL_Window* window, const std::string& appName, bool vSync); void cleanup(); - void recreateSwapchain(int width, int height); + [[nodiscard]] bool recreateSwapchain(int width, int height); VkCommandBuffer beginFrame(); diff --git a/destrum/include/destrum/Graphics/Swapchain.h b/destrum/include/destrum/Graphics/Swapchain.h index 2700d88..d373a34 100644 --- a/destrum/include/destrum/Graphics/Swapchain.h +++ b/destrum/include/destrum/Graphics/Swapchain.h @@ -34,7 +34,7 @@ public: std::uint32_t height, bool vSync); - void recreateSwapchain( + [[nodiscard]] bool recreateSwapchain( VkDevice device, vkb::Device vkbDevice, VkSurfaceKHR surface, diff --git a/destrum/include/destrum/ObjectModel/Component.h b/destrum/include/destrum/ObjectModel/Component.h index 75f4266..43bd829 100644 --- a/destrum/include/destrum/ObjectModel/Component.h +++ b/destrum/include/destrum/ObjectModel/Component.h @@ -33,6 +33,8 @@ public: virtual void Start(); virtual void SetEnabled(bool enabled); + [[nodiscard]] virtual bool AffectsPhysics() const { return false; } + // Reason for keeping No DT function -> i am tired and lazy // If im feeling frisky im gonna claim backwards compatibility virtual void Update(float dt); diff --git a/destrum/include/destrum/ObjectModel/GameObject.h b/destrum/include/destrum/ObjectModel/GameObject.h index 75868eb..ef8cd79 100644 --- a/destrum/include/destrum/ObjectModel/GameObject.h +++ b/destrum/include/destrum/ObjectModel/GameObject.h @@ -28,6 +28,7 @@ public: void CleanupComponents(); void RefreshPhysics(); + void RefreshPhysicsInHierarchy(); void SetActive(bool active) { if (active == m_Active) { @@ -36,7 +37,7 @@ public: m_Active = active; SetActiveDirty(); - RefreshPhysics(); + RefreshPhysicsInHierarchy(); } [[nodiscard]] bool IsActive() const { return m_Active; } @@ -76,7 +77,9 @@ public: auto& addedComponent = m_Components.emplace_back( std::make_unique(*this, std::forward(args)...)); - RefreshPhysics(); + if (addedComponent->AffectsPhysics()) { + RefreshPhysics(); + } return static_cast(addedComponent.get()); } diff --git a/destrum/include/destrum/Scene/Scene.h b/destrum/include/destrum/Scene/Scene.h index 0b32f2b..1d23a2e 100644 --- a/destrum/include/destrum/Scene/Scene.h +++ b/destrum/include/destrum/Scene/Scene.h @@ -65,6 +65,7 @@ public: return m_objects; } [[nodiscard]] GameObject* FindGameObject(ObjectId id) const; + [[nodiscard]] GameObject* GetSelectedObject() const { return FindGameObject(m_SelectedObjectId); } void SetRegisterBindings(std::function registerBindings) { m_registerBindings = std::move(registerBindings); diff --git a/destrum/include/destrum/Util/TransformGizmo.h b/destrum/include/destrum/Util/TransformGizmo.h new file mode 100644 index 0000000..44a26b6 --- /dev/null +++ b/destrum/include/destrum/Util/TransformGizmo.h @@ -0,0 +1,35 @@ +#ifndef DESTRUM_TRANSFORMGIZMO_H +#define DESTRUM_TRANSFORMGIZMO_H + +class Camera; +class GameObject; + +enum class GizmoOperation { + Translate, + Rotate, + Scale, +}; + +enum class GizmoSpace { + World, + Local, +}; + +class TransformGizmo final { +public: + void HandleShortcuts(); + void DrawToolbar(); + bool Manipulate(GameObject& object, const Camera& camera); + + [[nodiscard]] bool IsUsing() const; + [[nodiscard]] bool IsOver() const; + + GizmoOperation operation{GizmoOperation::Translate}; + GizmoSpace space{GizmoSpace::World}; + bool snap{false}; + float translationSnap{0.5f}; + float rotationSnapDegrees{15.0f}; + float scaleSnap{0.1f}; +}; + +#endif // DESTRUM_TRANSFORMGIZMO_H diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index c3a9dce..41dd4a0 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -170,6 +170,14 @@ void App::run() if (!isRunning) break; + if (window.IsMinimized()) + { + // The surface is 0x0 while minimized, there is nothing to render or present to + accumulator = 0.0f; + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + continue; + } + // Consume SDL events before updating the base camera so held and // newly pressed inputs are applied in the same frame. camera.Update(dt); @@ -290,11 +298,13 @@ if (!isRunning) break; continue; } - spdlog::info("Recreating swapchain to size: {}x{}", w, h); - { ZoneScopedN("Recreate Swapchain"); - gfxDevice.recreateSwapchain(w, h); + if (!gfxDevice.recreateSwapchain(w, h)) + { + continue; + } + spdlog::info("Recreated swapchain at {}x{}", w, h); imguiPass.onSwapchainRecreated(); onWindowResize(w, h); } diff --git a/destrum/src/Graphics/Camera.cpp b/destrum/src/Graphics/Camera.cpp index 77015e1..75df591 100644 --- a/destrum/src/Graphics/Camera.cpp +++ b/destrum/src/Graphics/Camera.cpp @@ -33,6 +33,11 @@ void Camera::Update(float deltaTime) { if (input.IsKeyDown(SDL_SCANCODE_LEFT)) m_yaw -= keyLookSpeed * deltaTime; if (input.IsKeyDown(SDL_SCANCODE_RIGHT)) m_yaw += keyLookSpeed * deltaTime; + if (input.IsMouseDown(SDL_BUTTON_RIGHT)) { + m_yaw += static_cast(input.MouseDeltaX()) * m_mouseSensitivity; + m_pitch -= static_cast(input.MouseDeltaY()) * m_mouseSensitivity; + } + if (pad0 >= 0) { const float rx = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_RIGHTX); const float ry = input.GetPadAxis(pad0, SDL_CONTROLLER_AXIS_RIGHTY); diff --git a/destrum/src/Graphics/GfxDevice.cpp b/destrum/src/Graphics/GfxDevice.cpp index dfee15a..a51f0c9 100644 --- a/destrum/src/Graphics/GfxDevice.cpp +++ b/destrum/src/Graphics/GfxDevice.cpp @@ -96,7 +96,7 @@ void GfxDevice::cleanup() { initialized = false; } -void GfxDevice::recreateSwapchain(int width, int height) { +bool GfxDevice::recreateSwapchain(int width, int height) { if (!initialized) { throw std::logic_error("GfxDevice::recreateSwapchain called before init"); } @@ -105,7 +105,7 @@ void GfxDevice::recreateSwapchain(int width, int height) { } waitIdle(); - swapchain.recreateSwapchain( + const bool recreated = swapchain.recreateSwapchain( instanceManager.getDevice(), instanceManager.getVkbDevice(), instanceManager.getSurface(), @@ -115,6 +115,7 @@ void GfxDevice::recreateSwapchain(int width, int height) { m_vSync); swapchainFormat = swapchain.getFormat(); + return recreated; } VkCommandBuffer GfxDevice::beginFrame() diff --git a/destrum/src/Graphics/Swapchain.cpp b/destrum/src/Graphics/Swapchain.cpp index 66a1d57..863aa17 100644 --- a/destrum/src/Graphics/Swapchain.cpp +++ b/destrum/src/Graphics/Swapchain.cpp @@ -107,7 +107,7 @@ void Swapchain::createSwapchain( dirty = false; } -void Swapchain::recreateSwapchain( +bool Swapchain::recreateSwapchain( VkDevice, vkb::Device vkbDevice, VkSurfaceKHR surf, @@ -120,7 +120,15 @@ void Swapchain::recreateSwapchain( if (width == 0 || height == 0) { dirty = true; - return; + return false; + } + + // A minimized window can still report its old size while the surface is 0x0 + VkSurfaceCapabilitiesKHR capabilities{}; + if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(vkbDevice.physical_device, surf, &capabilities) == VK_SUCCESS && + (capabilities.currentExtent.width == 0 || capabilities.currentExtent.height == 0)) { + dirty = true; + return false; } surface = surf; @@ -209,6 +217,8 @@ void Swapchain::recreateSwapchain( extent = m_swapchain.extent; dirty = false; + + return true; } void Swapchain::cleanup(VkDevice device) { diff --git a/destrum/src/ObjectModel/Component.cpp b/destrum/src/ObjectModel/Component.cpp index 34b1611..202fea5 100644 --- a/destrum/src/ObjectModel/Component.cpp +++ b/destrum/src/ObjectModel/Component.cpp @@ -16,7 +16,9 @@ Transform& Component::GetTransform() const { void Component::Destroy() { Object::Destroy(); - NotifyPhysicsChanged(); + if (AffectsPhysics()) { + NotifyPhysicsChanged(); + } } void Component::Start() { @@ -24,17 +26,14 @@ void Component::Start() { void Component::SetEnabled(bool enabled) { m_IsEnabled = enabled; - NotifyPhysicsChanged(); + if (AffectsPhysics()) { + NotifyPhysicsChanged(); + } } void Component::NotifyPhysicsChanged() { if (m_ParentGameObjectPtr != nullptr) { - if (auto* rigidbody = m_ParentGameObjectPtr->GetComponent(); - rigidbody != nullptr && rigidbody->GetPhysicsWorld() != nullptr) { - rigidbody->GetPhysicsWorld()->RefreshRigidbody(*rigidbody); - } else { - m_ParentGameObjectPtr->RefreshPhysics(); - } + m_ParentGameObjectPtr->RefreshPhysics(); } } diff --git a/destrum/src/ObjectModel/GameObject.cpp b/destrum/src/ObjectModel/GameObject.cpp index 184a910..fa1144f 100644 --- a/destrum/src/ObjectModel/GameObject.cpp +++ b/destrum/src/ObjectModel/GameObject.cpp @@ -196,8 +196,41 @@ void GameObject::CleanupComponents() { }); } -void GameObject::RefreshPhysics() { - if (m_Scene != nullptr) { - m_Scene->RefreshPhysics(); +namespace { + void RefreshBody(GameObject& object, Scene* scene) { + auto* rigidbody = object.GetComponent(); + if (rigidbody == nullptr || rigidbody->IsBeingDestroyed()) { + return; + } + + if (PhysicsWorld* world = rigidbody->GetPhysicsWorld()) { + world->RefreshRigidbody(*rigidbody); + } else if (scene != nullptr) { + scene->GetPhysics().RefreshGameObject(object); + } + } + + void RefreshChildBodies(const Transform& transform, Scene* scene) { + for (Transform* child : transform.GetChildren()) { + if (GameObject* owner = child->GetOwner()) { + RefreshBody(*owner, scene); + } + RefreshChildBodies(*child, scene); + } } } + +void GameObject::RefreshPhysics() { + for (Transform* transform = &m_TransformPtr; transform != nullptr; transform = transform->GetParent()) { + GameObject* owner = transform->GetOwner(); + if (owner != nullptr && owner->GetComponent() != nullptr) { + RefreshBody(*owner, m_Scene); + return; + } + } +} + +void GameObject::RefreshPhysicsInHierarchy() { + RefreshPhysics(); + RefreshChildBodies(m_TransformPtr, m_Scene); +} diff --git a/destrum/src/ObjectModel/Transform.cpp b/destrum/src/ObjectModel/Transform.cpp index e14aae6..566d844 100644 --- a/destrum/src/ObjectModel/Transform.cpp +++ b/destrum/src/ObjectModel/Transform.cpp @@ -242,7 +242,7 @@ void Transform::SetParent(Transform* parent, bool useWorldPosition) { if (m_Owner != nullptr) { m_Owner->SetActiveDirty(); - m_Owner->RefreshPhysics(); + m_Owner->RefreshPhysicsInHierarchy(); } try { diff --git a/destrum/src/Physics/JoltPhysicsWorld.cpp b/destrum/src/Physics/JoltPhysicsWorld.cpp index 8c930b1..09d247c 100644 --- a/destrum/src/Physics/JoltPhysicsWorld.cpp +++ b/destrum/src/Physics/JoltPhysicsWorld.cpp @@ -362,10 +362,11 @@ namespace m_Overlaps.clear(); } - std::unordered_map SwapOverlaps() + // Persisted contacts are not reported again, so the set has to outlive each step + std::unordered_map SnapshotOverlaps() { std::lock_guard lock(m_Mutex); - return std::move(m_Overlaps); + return m_Overlaps; } private: @@ -752,7 +753,7 @@ private: void ProcessSensorOverlaps() { - const auto currentOverlaps = m_SensorListener.SwapOverlaps(); + auto currentOverlaps = m_SensorListener.SnapshotOverlaps(); for (const auto& [pair, data] : currentOverlaps) { diff --git a/destrum/src/Util/ImGuiUtils.cpp b/destrum/src/Util/ImGuiUtils.cpp index 4f49dbc..8eec824 100644 --- a/destrum/src/Util/ImGuiUtils.cpp +++ b/destrum/src/Util/ImGuiUtils.cpp @@ -277,7 +277,7 @@ namespace ImGuiUtils { } if (transformChanged) { - selectedObject->RefreshPhysics(); + selectedObject->RefreshPhysicsInHierarchy(); } const glm::vec3& worldPosition = transform.GetWorldPosition(); diff --git a/destrum/src/Util/TransformGizmo.cpp b/destrum/src/Util/TransformGizmo.cpp new file mode 100644 index 0000000..c356e8e --- /dev/null +++ b/destrum/src/Util/TransformGizmo.cpp @@ -0,0 +1,166 @@ +#include + +#include +#include + +#include +#include + +#include +#include + +namespace { + ImGuizmo::OPERATION toImGuizmo(GizmoOperation operation) + { + switch (operation) { + case GizmoOperation::Rotate: + return ImGuizmo::ROTATE; + case GizmoOperation::Scale: + return ImGuizmo::SCALE; + case GizmoOperation::Translate: + break; + } + return ImGuizmo::TRANSLATE; + } +} + +void TransformGizmo::HandleShortcuts() +{ + if (ImGui::GetIO().WantTextInput || ImGuizmo::IsUsing()) { + return; + } + + if (ImGui::IsKeyPressed(ImGuiKey_T, false)) { + operation = GizmoOperation::Translate; + } + if (ImGui::IsKeyPressed(ImGuiKey_R, false)) { + operation = GizmoOperation::Rotate; + } + if (ImGui::IsKeyPressed(ImGuiKey_Y, false)) { + operation = GizmoOperation::Scale; + } + if (ImGui::IsKeyPressed(ImGuiKey_L, false)) { + space = space == GizmoSpace::World ? GizmoSpace::Local : GizmoSpace::World; + } +} + +void TransformGizmo::DrawToolbar() +{ + if (!ImGui::Begin("Gizmo", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::End(); + return; + } + + const auto operationButton = [this](const char* label, GizmoOperation buttonOperation) { + if (ImGui::RadioButton(label, operation == buttonOperation)) { + operation = buttonOperation; + } + }; + operationButton("Move (T)", GizmoOperation::Translate); + ImGui::SameLine(); + operationButton("Rotate (R)", GizmoOperation::Rotate); + ImGui::SameLine(); + operationButton("Scale (Y)", GizmoOperation::Scale); + + if (operation != GizmoOperation::Scale) { + bool local = space == GizmoSpace::Local; + if (ImGui::Checkbox("Local (L)", &local)) { + space = local ? GizmoSpace::Local : GizmoSpace::World; + } + ImGui::SameLine(); + } + + ImGui::Checkbox("Snap", &snap); + if (snap) { + ImGui::SetNextItemWidth(80.0f); + switch (operation) { + case GizmoOperation::Translate: + ImGui::DragFloat("Step", &translationSnap, 0.05f, 0.01f, 10.0f, "%.2f m"); + break; + case GizmoOperation::Rotate: + ImGui::DragFloat("Step", &rotationSnapDegrees, 1.0f, 1.0f, 90.0f, "%.0f deg"); + break; + case GizmoOperation::Scale: + ImGui::DragFloat("Step", &scaleSnap, 0.01f, 0.01f, 1.0f, "%.2f"); + break; + } + } + + ImGui::End(); +} + +bool TransformGizmo::Manipulate(GameObject& object, const Camera& camera) +{ + ImGuizmo::BeginFrame(); + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGuizmo::SetOrthographic(false); + ImGuizmo::SetRect(viewport->Pos.x, viewport->Pos.y, viewport->Size.x, viewport->Size.y); + + const glm::mat4 view = camera.GetViewMatrix(); + glm::mat4 projection = camera.GetProjectionMatrix(); + // ImGuizmo expects OpenGL's y up clip space, the engine flips y for Vulkan + projection[1][1] *= -1.0f; + + Transform& transform = object.GetTransform(); + glm::mat4 worldMatrix = transform.GetWorldMatrix(); + + const float snapStep = operation == GizmoOperation::Translate ? translationSnap + : operation == GizmoOperation::Rotate ? rotationSnapDegrees + : scaleSnap; + const float snapValues[3] = {snapStep, snapStep, snapStep}; + const ImGuizmo::MODE mode = + operation == GizmoOperation::Scale || space == GizmoSpace::Local ? ImGuizmo::LOCAL : ImGuizmo::WORLD; + + const bool changed = ImGuizmo::Manipulate( + glm::value_ptr(view), + glm::value_ptr(projection), + toImGuizmo(operation), + mode, + glm::value_ptr(worldMatrix), + nullptr, + snap ? snapValues : nullptr); + + // Stops the click on a handle from reaching the game, e.g. grabbing the mouse for first person + if (ImGuizmo::IsOver() || ImGuizmo::IsUsing()) { + ImGui::SetNextFrameWantCaptureMouse(true); + } + + if (!changed) { + return false; + } + + glm::vec3 position; + glm::quat rotation; + glm::vec3 scale; + glm::vec3 skew; + glm::vec4 perspective; + if (!glm::decompose(worldMatrix, scale, rotation, position, skew, perspective)) { + return false; + } + + switch (operation) { + case GizmoOperation::Translate: + transform.SetWorldPosition(position); + break; + case GizmoOperation::Rotate: + transform.SetWorldRotation(rotation); + break; + case GizmoOperation::Scale: + transform.SetWorldScale(scale); + break; + } + + object.RefreshPhysicsInHierarchy(); + return true; +} + +bool TransformGizmo::IsUsing() const +{ + return ImGuizmo::IsUsing(); +} + +bool TransformGizmo::IsOver() const +{ + return ImGuizmo::IsOver(); +} diff --git a/destrum/third_party/CMakeLists.txt b/destrum/third_party/CMakeLists.txt index e72f2e0..aaba533 100644 --- a/destrum/third_party/CMakeLists.txt +++ b/destrum/third_party/CMakeLists.txt @@ -119,6 +119,10 @@ target_link_libraries(imgui PUBLIC volk ) +# ImGuizmo +add_subdirectory(imguizmo) +target_link_libraries(imguizmo PUBLIC imgui) + # Jolt Physics set(DOUBLE_PRECISION OFF CACHE BOOL "" FORCE) set(GENERATE_DEBUG_SYMBOLS ON CACHE BOOL "" FORCE) diff --git a/destrum/third_party/imguizmo b/destrum/third_party/imguizmo new file mode 160000 index 0000000..105e4fd --- /dev/null +++ b/destrum/third_party/imguizmo @@ -0,0 +1 @@ +Subproject commit 105e4fde45e4e74aae8973d98750820ef7aca7e2 diff --git a/lightkeeper/CMakeLists.txt b/lightkeeper/CMakeLists.txt index ec291bb..5e123a6 100644 --- a/lightkeeper/CMakeLists.txt +++ b/lightkeeper/CMakeLists.txt @@ -17,6 +17,7 @@ set(GAME_SRC src/components/FirstPersonController.cpp src/components/Interactable.cpp src/components/InteractableButton.cpp + src/components/LightToggle.cpp src/components/PrintComponent.cpp src/components/TriggerLoggerComponent.cpp ) diff --git a/lightkeeper/assets_src/sofa.glb b/lightkeeper/assets_src/sofa.glb new file mode 100644 index 0000000..9f7225a Binary files /dev/null and b/lightkeeper/assets_src/sofa.glb differ diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index 2677b8d..3e188eb 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "destrum/Graphics/Resources/Cubemap.h" #include "destrum/Graphics/Resources/Font.h" @@ -31,13 +32,17 @@ public: private: void setupSkybox(); void drawCrosshair(); + void drawViewModeHint(); + void updateViewMode(); [[nodiscard]] CameraComponent* getSceneCamera() const; + [[nodiscard]] Camera& getViewCamera(); [[nodiscard]] bool isPlayerViewActive() const; // LightkeeperDebugUi.cpp void updateDebugUi(); void drawDebugMenuBar(); void drawWindowMenu(); + void drawSelectionGizmo(); void drawTestWindow(); void spawnBall(); void saveCurrentScene(); @@ -54,6 +59,7 @@ private: ImageID debug2DTexture{NULL_IMAGE_ID}; Font hudFont; + TransformGizmo transformGizmo; char scenePath[512]{"scenes/debug_scene.json"}; char newSceneName[128]{"EmptyScene"}; @@ -61,6 +67,7 @@ private: bool renderColliders{false}; bool render2DShapes{true}; bool renderDebugLines{true}; + bool showTransformGizmo{true}; bool showTestWindow{true}; bool useFreeCamera{false}; }; diff --git a/lightkeeper/include/components/FirstPersonController.h b/lightkeeper/include/components/FirstPersonController.h index 72464f5..5a5fa09 100644 --- a/lightkeeper/include/components/FirstPersonController.h +++ b/lightkeeper/include/components/FirstPersonController.h @@ -41,8 +41,8 @@ public: [[nodiscard]] Interactable* GetFocusedInteractable() const { return m_FocusedInteractable; } [[nodiscard]] glm::vec3 GetLookDirection() const; - [[nodiscard]] bool IsMouseCaptured() const { return InputManager::GetInstance().IsMouseCaptured(); } - void SetMouseCaptured(bool captured); + [[nodiscard]] bool IsPossessed() const { return m_Possessed; } + void SetPossessed(bool possessed); private: [[nodiscard]] bool CheckGrounded() const; @@ -70,6 +70,7 @@ private: Interactable* m_FocusedInteractable{nullptr}; + bool m_Possessed{false}; bool m_Grounded{false}; bool m_JumpHeld{false}; float m_CoyoteTimer{0.0f}; diff --git a/lightkeeper/include/components/GameComponentList.h b/lightkeeper/include/components/GameComponentList.h index 8193f43..75a44bb 100644 --- a/lightkeeper/include/components/GameComponentList.h +++ b/lightkeeper/include/components/GameComponentList.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ X(Door) \ X(FirstPersonController) \ X(InteractableButton) \ + X(LightToggle) \ X(PrintComponent) \ X(TriggerLoggerComponent) diff --git a/lightkeeper/include/components/InteractableButton.h b/lightkeeper/include/components/InteractableButton.h index 5178223..4a19d9f 100644 --- a/lightkeeper/include/components/InteractableButton.h +++ b/lightkeeper/include/components/InteractableButton.h @@ -24,6 +24,8 @@ public: [[nodiscard]] std::size_t GetColorIndex() const { return m_ColorIndex; } [[nodiscard]] int GetPressCount() const { return m_PressCount; } + void SetCycleColors(bool cycleColors) { m_CycleColors = cycleColors; } + // Every Activatable on a target object is activated when the button is pressed void AddTarget(const GameObject& target); void RemoveTarget(ObjectId target); @@ -36,6 +38,7 @@ private: std::size_t m_ColorIndex{0}; int m_PressCount{0}; + bool m_CycleColors{true}; std::vector m_Targets; }; diff --git a/lightkeeper/include/components/LightToggle.h b/lightkeeper/include/components/LightToggle.h new file mode 100644 index 0000000..0638cd8 --- /dev/null +++ b/lightkeeper/include/components/LightToggle.h @@ -0,0 +1,30 @@ +#ifndef LIGHTKEEPER_LIGHTTOGGLE_H +#define LIGHTKEEPER_LIGHTTOGGLE_H + +#include + +class LightToggle final : public Activatable { +public: + explicit LightToggle(GameObject& parent); + + std::string GetTypeName() const override { return "LightToggle"; } + + void Start() override; + + nlohmann::json Serialize() const override; + void Deserialize(const nlohmann::json& data) override; + void ImGuiInspector() override; + + void SetOn(bool on); + [[nodiscard]] bool IsOn() const { return m_On; } + +protected: + void OnActivate(GameObject& instigator) override; + +private: + void Apply(); + + bool m_On{true}; +}; + +#endif // LIGHTKEEPER_LIGHTTOGGLE_H diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index 671863c..c929472 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -33,6 +33,7 @@ void LightKeeper::customInit() const float aspectRatio = static_cast(m_params.renderSize.x) / static_cast(m_params.renderSize.y); camera.setAspectRatio(aspectRatio); camera.SetRotation(glm::radians(glm::vec2(90.f, 0.f))); + window.Maximize(); setupSkybox(); @@ -44,6 +45,7 @@ void LightKeeper::customInit() hudFont = Font::LoadFromFile(AssetFS::GetInstance().GetFullPath("engine://fonts/Roboto-Medium.ttf")); hudFont.Upload(gfxDevice, resources); + } void LightKeeper::setupSkybox() @@ -62,6 +64,8 @@ void LightKeeper::setupSkybox() void LightKeeper::customUpdate(float dt) { + updateViewMode(); + // LightKeeper owns a camera that hides App::camera; update this instance // after SDL events have been consumed. if (!isPlayerViewActive() && getSceneCamera() == nullptr) @@ -79,6 +83,45 @@ void LightKeeper::customUpdate(float dt) updateDebugUi(); } +void LightKeeper::updateViewMode() +{ + auto& input = InputManager::GetInstance(); + FirstPersonController* player = mainScene.playerController; + + if (player != nullptr && input.WasKeyPressed(SDL_SCANCODE_F5)) + { + player->SetPossessed(!player->IsPossessed()); + } + + if (!isPlayerViewActive()) + { + const bool looking = input.IsMouseDown(SDL_BUTTON_RIGHT); + if (looking != input.IsMouseCaptured()) + { + input.SetMouseCaptured(looking); + } + } +} + +void LightKeeper::drawViewModeHint() +{ + if (isPlayerViewActive() || mainScene.playerController == nullptr) { + return; + } + + constexpr std::string_view hint = "Free camera RMB look WASD / Q E move F5 play"; + constexpr float hintSize = 18.0f; + const VkExtent3D extent = renderer.getDrawImage().extent; + const glm::vec2 hintExtent = hudFont.MeasureText(hint, hintSize); + + Renderer2D::GetInstance().DrawText( + hudFont, + hint, + {16.0f, static_cast(extent.height) - hintExtent.y - 12.0f}, + hintSize, + TextStyle{.color = {1.0f, 1.0f, 1.0f, 0.85f}, .outline = 2.0f, .outlineColor = {0.0f, 0.0f, 0.0f, 0.6f}}); +} + void LightKeeper::drawCrosshair() { if (!isPlayerViewActive()) { @@ -114,7 +157,7 @@ void LightKeeper::drawCrosshair() bool LightKeeper::isPlayerViewActive() const { - return mainScene.playerController != nullptr && mainScene.playerController->IsMouseCaptured(); + return mainScene.playerController != nullptr && mainScene.playerController->IsPossessed(); } CameraComponent* LightKeeper::getSceneCamera() const @@ -126,6 +169,12 @@ CameraComponent* LightKeeper::getSceneCamera() const return activeScenes.front()->GetCurrentCamera(); } +Camera& LightKeeper::getViewCamera() +{ + CameraComponent* sceneCamera = isPlayerViewActive() ? nullptr : getSceneCamera(); + return sceneCamera != nullptr ? sceneCamera->GetCamera(camera.GetAspectRatio()) : camera; +} + void LightKeeper::customDraw() { const auto cmd = gfxDevice.beginFrame(); @@ -136,6 +185,7 @@ void LightKeeper::customDraw() renderer.beginDrawing(gfxDevice); submitDebugDraws(); drawCrosshair(); + drawViewModeHint(); if (isPlayerViewActive()) { @@ -145,8 +195,7 @@ void LightKeeper::customDraw() camera.CalculateProjectionMatrix(); } - CameraComponent* sceneCamera = isPlayerViewActive() ? nullptr : getSceneCamera(); - Camera& viewCamera = sceneCamera != nullptr ? sceneCamera->GetCamera(camera.GetAspectRatio()) : camera; + Camera& viewCamera = getViewCamera(); const SceneEnvironment& environment = SceneManager::GetInstance().GetCurrentScene().GetEnvironment(); const GameRenderer::SceneData sceneData{ diff --git a/lightkeeper/src/LightkeeperDebugUi.cpp b/lightkeeper/src/LightkeeperDebugUi.cpp index 2ec32fe..dec08c6 100644 --- a/lightkeeper/src/LightkeeperDebugUi.cpp +++ b/lightkeeper/src/LightkeeperDebugUi.cpp @@ -51,6 +51,9 @@ void LightKeeper::updateDebugUi() } drawDebugMenuBar(); + if (showTransformGizmo && !isPlayerViewActive()) { + drawSelectionGizmo(); + } if (showTestWindow) { drawTestWindow(); } @@ -129,6 +132,7 @@ void LightKeeper::drawDebugMenuBar() ImGui::MenuItem("Colliders", nullptr, &renderColliders); ImGui::MenuItem("2D Shapes", nullptr, &render2DShapes); ImGui::MenuItem("Debug Lines", nullptr, &renderDebugLines); + ImGui::MenuItem("Transform Gizmo", nullptr, &showTransformGizmo); bool lightHeatmap = renderer.getRenderLightHeatmap(); if (ImGui::MenuItem("Forward+ Light Heatmap", nullptr, &lightHeatmap)) { @@ -163,6 +167,18 @@ void LightKeeper::drawDebugMenuBar() ImGui::EndMainMenuBar(); } +void LightKeeper::drawSelectionGizmo() +{ + GameObject* selected = SceneManager::GetInstance().GetCurrentScene().GetSelectedObject(); + if (selected == nullptr) { + return; + } + + transformGizmo.HandleShortcuts(); + transformGizmo.DrawToolbar(); + transformGizmo.Manipulate(*selected, getViewCamera()); +} + void LightKeeper::drawWindowMenu() { if (!ImGui::BeginMenu("Window")) { diff --git a/lightkeeper/src/components/FirstPersonController.cpp b/lightkeeper/src/components/FirstPersonController.cpp index 35172c3..c86e925 100644 --- a/lightkeeper/src/components/FirstPersonController.cpp +++ b/lightkeeper/src/components/FirstPersonController.cpp @@ -34,9 +34,12 @@ FirstPersonController::FirstPersonController(GameObject& parent) { } -void FirstPersonController::SetMouseCaptured(bool captured) +void FirstPersonController::SetPossessed(bool possessed) { - InputManager::GetInstance().SetMouseCaptured(captured); + m_Possessed = possessed; + m_JumpHeld = false; + m_JumpBufferTimer = 0.0f; + InputManager::GetInstance().SetMouseCaptured(possessed); } void FirstPersonController::Start() @@ -65,26 +68,16 @@ void FirstPersonController::Update(float dt) { const auto& input = InputManager::GetInstance(); - if (input.WasKeyPressed(SDL_SCANCODE_F5)) - { - SetMouseCaptured(!IsMouseCaptured()); - } - m_FocusedInteractable = nullptr; - if (!IsMouseCaptured()) + if (!m_Possessed) { - m_JumpHeld = false; - if (input.WasMousePressed(SDL_BUTTON_LEFT) && !ImGui::GetIO().WantCaptureMouse) - { - SetMouseCaptured(true); - } return; } if (input.WasKeyPressed(SDL_SCANCODE_ESCAPE)) { - SetMouseCaptured(false); + SetPossessed(false); return; } @@ -143,7 +136,7 @@ void FirstPersonController::FixedUpdate(float fixedDt) m_CoyoteTimer = m_Grounded ? m_CoyoteTime : std::max(m_CoyoteTimer - fixedDt, 0.0f); const auto& input = InputManager::GetInstance(); - const bool sprinting = IsMouseCaptured() && input.IsKeyDown(SDL_SCANCODE_LSHIFT); + const bool sprinting = m_Possessed && input.IsKeyDown(SDL_SCANCODE_LSHIFT); const glm::vec3 wishVelocity = GetWishDirection() * (sprinting ? m_SprintSpeed : m_WalkSpeed); const float acceleration = m_Grounded ? m_GroundAcceleration : m_AirAcceleration; @@ -211,7 +204,7 @@ bool FirstPersonController::CheckGrounded() const glm::vec3 FirstPersonController::GetWishDirection() const { - if (!IsMouseCaptured()) + if (!m_Possessed) return glm::vec3{0.0f}; const auto& input = InputManager::GetInstance(); @@ -255,10 +248,8 @@ void FirstPersonController::ImGuiInspector() ImGui::Text("Grounded: %s", m_Grounded ? "yes" : "no"); ImGui::Text("Yaw: %.1f deg", glm::degrees(m_Yaw)); ImGui::Text("Pitch: %.1f deg", glm::degrees(m_Pitch)); - bool captured = IsMouseCaptured(); - ImGui::Checkbox("Mouse Captured", &captured); - if (captured != IsMouseCaptured()) { - SetMouseCaptured(captured); + if (bool possessed = m_Possessed; ImGui::Checkbox("Possessed", &possessed)) { + SetPossessed(possessed); } } diff --git a/lightkeeper/src/components/InteractableButton.cpp b/lightkeeper/src/components/InteractableButton.cpp index 386c461..995f4da 100644 --- a/lightkeeper/src/components/InteractableButton.cpp +++ b/lightkeeper/src/components/InteractableButton.cpp @@ -33,14 +33,18 @@ InteractableButton::InteractableButton(GameObject& parent) void InteractableButton::Start() { - ApplyColor(); + if (m_CycleColors) { + ApplyColor(); + } } void InteractableButton::Interact(GameObject& interactor) { - m_ColorIndex = (m_ColorIndex + 1) % ButtonColors.size(); ++m_PressCount; - ApplyColor(); + if (m_CycleColors) { + m_ColorIndex = (m_ColorIndex + 1) % ButtonColors.size(); + ApplyColor(); + } spdlog::info("{} pressed {} by {}", GetGameObject()->GetName(), m_PressCount, interactor.GetName()); ActivateTargets(interactor); @@ -107,6 +111,7 @@ nlohmann::json InteractableButton::Serialize() const { return { {"colorIndex", m_ColorIndex}, + {"cycleColors", m_CycleColors}, {"pressCount", m_PressCount}, {"targets", m_Targets}, }; @@ -115,6 +120,7 @@ nlohmann::json InteractableButton::Serialize() const void InteractableButton::Deserialize(const nlohmann::json& data) { m_ColorIndex = data.value("colorIndex", m_ColorIndex) % ButtonColors.size(); + m_CycleColors = data.value("cycleColors", m_CycleColors); m_PressCount = data.value("pressCount", m_PressCount); m_Targets = data.value("targets", std::vector{}); } @@ -125,6 +131,9 @@ void InteractableButton::ImGuiInspector() if (ImGui::Button("Press")) { Interact(*GetGameObject()); } + if (ImGui::Checkbox("Cycle Colors", &m_CycleColors) && m_CycleColors) { + ApplyColor(); + } DrawTargetsInspector(); } diff --git a/lightkeeper/src/components/LightToggle.cpp b/lightkeeper/src/components/LightToggle.cpp new file mode 100644 index 0000000..1620655 --- /dev/null +++ b/lightkeeper/src/components/LightToggle.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include +#include +#include +#include + +#include + +LightToggle::LightToggle(GameObject& parent) + : Activatable(parent, "LightToggle") +{ +} + +void LightToggle::Start() +{ + Apply(); +} + +void LightToggle::SetOn(bool on) +{ + m_On = on; + Apply(); +} + +void LightToggle::OnActivate(GameObject&) +{ + SetOn(!m_On); +} + +void LightToggle::Apply() +{ + GameObject* object = GetGameObject(); + auto* light = object->GetComponent(); + if (light != nullptr) { + light->SetEnabled(m_On); + } + + // The bulb glows in the light's colour while it is on + const auto* meshRenderer = object->GetComponent(); + if (meshRenderer == nullptr || meshRenderer->GetMaterialID() == NULL_MATERIAL_ID || + !GameState::GetInstance().HasRenderer()) { + return; + } + + GameRenderer& renderer = GameState::GetInstance().Renderer(); + Material& material = renderer.getMaterialMutable(meshRenderer->GetMaterialID()); + material.emissiveColor = m_On && light != nullptr ? light->GetColor() : glm::vec3{0.0f}; + renderer.updateMaterialGPU(meshRenderer->GetMaterialID()); +} + +nlohmann::json LightToggle::Serialize() const +{ + return {{"on", m_On}}; +} + +void LightToggle::Deserialize(const nlohmann::json& data) +{ + m_On = data.value("on", m_On); +} + +void LightToggle::ImGuiInspector() +{ + if (bool on = m_On; ImGui::Checkbox("On", &on)) { + SetOn(on); + } +} diff --git a/lightkeeper/src/scenes/MainScene.cpp b/lightkeeper/src/scenes/MainScene.cpp index f6d44fd..5cb117e 100644 --- a/lightkeeper/src/scenes/MainScene.cpp +++ b/lightkeeper/src/scenes/MainScene.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -26,11 +27,13 @@ #include #include +#include #include namespace { // The visible ground plane is at y = -1 constexpr float GroundHeight = -1.0f; + constexpr float GroundHalfExtent = 25.0f; ModelDoc::LoadOptions StaticModelOptions() { @@ -80,12 +83,12 @@ namespace { planeMeshComp->SetMeshID(resources.meshes().addMesh(gfxDevice, planePrimitive.mesh)); planeMeshComp->SetMaterialID(planeMaterialID); planeObj->GetTransform().SetWorldPosition(glm::vec3(0.f, GroundHeight, 0.f)); - planeObj->GetTransform().SetWorldScale(glm::vec3(10.f, 1.f, 10.f)); + planeObj->GetTransform().SetWorldScale(glm::vec3(GroundHalfExtent, 1.f, GroundHalfExtent)); // The plane mesh is flat, so the floor collider sits right below it GameObject* groundObj = scene.CreateGameObject("GroundCollider"); groundObj->GetTransform().SetWorldPosition(glm::vec3(0.f, GroundHeight - 0.5f, 0.f)); - groundObj->AddComponent(glm::vec3{10.0f, 0.5f, 10.0f}); + groundObj->AddComponent(glm::vec3{GroundHalfExtent, 0.5f, GroundHalfExtent}); groundObj->AddComponent()->SetType(RigidbodyType::Static); scene.GetPhysics().RegisterGameObject(*groundObj); } @@ -201,6 +204,124 @@ namespace { button->AddComponent()->AddTarget(target); } + struct WallOpening { + float offset; + float width; + float bottom; + float top; + }; + + void CreateWall(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, const std::string& name, + const glm::vec3& floorCenter, float length, float height, bool alongX, MaterialID materialID, + const std::optional& opening = std::nullopt) + { + constexpr float thickness = 0.3f; + + const auto createPiece = [&](const std::string& suffix, float from, float to, float bottom, float top) + { + if (to <= from || top <= bottom) { + return; + } + const float along = (from + to) * 0.5f; + const glm::vec3 alongOffset = alongX ? glm::vec3{along, 0.0f, 0.0f} : glm::vec3{0.0f, 0.0f, along}; + const glm::vec3 center = floorCenter + alongOffset + glm::vec3{0.0f, (bottom + top) * 0.5f, 0.0f}; + const glm::vec3 size = alongX + ? glm::vec3{to - from, top - bottom, thickness} + : glm::vec3{thickness, top - bottom, to - from}; + CreateBox(scene, gfxDevice, resources, name + suffix, center, size, materialID, RigidbodyType::Static); + }; + + const float halfLength = length * 0.5f; + if (!opening) { + createPiece("", -halfLength, halfLength, 0.0f, height); + return; + } + + const float openingStart = opening->offset - opening->width * 0.5f; + const float openingEnd = opening->offset + opening->width * 0.5f; + createPiece("_Left", -halfLength, openingStart, 0.0f, height); + createPiece("_Right", openingEnd, halfLength, 0.0f, height); + createPiece("_Below", openingStart, openingEnd, 0.0f, opening->bottom); + createPiece("_Above", openingStart, openingEnd, opening->top, height); + } + + GameObject* CreateCeilingLight(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources, + const glm::vec3& position) + { + GameObject* lightObj = scene.CreateGameObject("CeilingLight"); + lightObj->GetTransform().SetWorldPosition(position); + + // Every bulb needs its own material, the toggle changes its glow + Material bulbMaterial = Material::SimpleColor(glm::vec3{1.0f, 0.95f, 0.85f}, "BulbMaterial"); + auto* bulbMesh = lightObj->AddComponent(); + bulbMesh->SetMeshID(resources.meshes().addMesh(gfxDevice, PrimitiveMeshes::MakeBox({0.35f, 0.12f, 0.35f}))); + bulbMesh->SetMaterialID(resources.materials().addMaterial(std::move(bulbMaterial))); + + auto* light = lightObj->AddComponent(LightType::Point); + light->SetColor(glm::vec3{1.0f, 0.82f, 0.55f}); + light->SetIntensity(4.0f); + light->SetRange(7.0f); + + lightObj->AddComponent(); + return lightObj; + } + + void CreateHouse(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources) + { + const MaterialID wallMaterial = AddPrototypeMaterial(gfxDevice, resources, "Light", 0.8f); + const MaterialID floorMaterial = AddPrototypeMaterial(gfxDevice, resources, "Orange", 0.7f); + const MaterialID roofMaterial = AddPrototypeMaterial(gfxDevice, resources, "Red", 0.8f); + + const glm::vec3 center{0.0f, GroundHeight, -16.0f}; + constexpr float width = 8.0f; + constexpr float depth = 6.0f; + constexpr float wallHeight = 3.0f; + constexpr float doorOffset = -1.5f; + constexpr float doorWidth = 1.2f; + + CreateBox(scene, gfxDevice, resources, "House_Floor", center + glm::vec3{0.0f, 0.03f, 0.0f}, + {width, 0.06f, depth}, floorMaterial, RigidbodyType::Static); + + CreateWall(scene, gfxDevice, resources, "House_FrontWall", center + glm::vec3{0.0f, 0.0f, depth * 0.5f}, + width + 0.3f, wallHeight, true, wallMaterial, + WallOpening{.offset = doorOffset, .width = doorWidth, .bottom = 0.0f, .top = 2.2f}); + CreateWall(scene, gfxDevice, resources, "House_BackWall", center - glm::vec3{0.0f, 0.0f, depth * 0.5f}, + width + 0.3f, wallHeight, true, wallMaterial, + WallOpening{.offset = 1.5f, .width = 1.4f, .bottom = 1.0f, .top = 2.0f}); + CreateWall(scene, gfxDevice, resources, "House_LeftWall", center - glm::vec3{width * 0.5f, 0.0f, 0.0f}, + depth - 0.3f, wallHeight, false, wallMaterial); + CreateWall(scene, gfxDevice, resources, "House_RightWall", center + glm::vec3{width * 0.5f, 0.0f, 0.0f}, + depth - 0.3f, wallHeight, false, wallMaterial, + WallOpening{.offset = 0.0f, .width = 1.6f, .bottom = 1.0f, .top = 2.0f}); + + CreateBox(scene, gfxDevice, resources, "House_Roof", center + glm::vec3{0.0f, wallHeight + 0.15f, 0.0f}, + {width + 0.8f, 0.3f, depth + 0.8f}, roofMaterial, RigidbodyType::Static); + + const GameObject* ceilingLight = + CreateCeilingLight(scene, gfxDevice, resources, center + glm::vec3{0.0f, wallHeight - 0.06f, 0.0f}); + + // Inside, next to the door + Material switchMaterial = Material::SimpleColor(glm::vec3{0.9f}, "LightSwitchMaterial"); + switchMaterial.roughnessFactor = 0.5f; + const glm::vec3 switchPosition = + center + glm::vec3{doorOffset + doorWidth * 0.5f + 0.4f, 1.3f, depth * 0.5f - 0.15f - 0.03f}; + GameObject* lightSwitch = CreateBox(scene, gfxDevice, resources, "LightSwitch", switchPosition, + {0.12f, 0.2f, 0.06f}, + resources.materials().addMaterial(std::move(switchMaterial)), + RigidbodyType::Static); + auto* switchButton = lightSwitch->AddComponent(); + switchButton->SetCycleColors(false); + switchButton->AddTarget(*ceilingLight); + + GameObject* sofa = ModelSpawner::SpawnStaticModel( + scene, gfxDevice, resources, "game://sofa.glb", StaticModelOptions(), + "Sofa" + ); + sofa->GetTransform().SetWorldPosition({-3.7, -1, -17.5}); + sofa->GetTransform().SetWorldRotation({180, 0, 180}); + sofa->GetTransform().SetWorldScale({3, 3, 3}); + } + void CreateFlightHelmet(Scene& scene, GfxDevice& gfxDevice, RenderResources& resources) { GameObject* helmetObj = ModelSpawner::SpawnStaticModel( @@ -323,10 +444,11 @@ MainSceneObjects BuildMainScene(Scene& scene, GfxDevice& gfxDevice, RenderResour CreateParkourCourse(scene, gfxDevice, resources); CreateFlightHelmet(scene, gfxDevice, resources); CreateTree(scene, gfxDevice, resources); - CreateCapybara(scene, gfxDevice, resources); + // CreateCapybara(scene, gfxDevice, resources); CreateTriggerZone(scene); const GameObject* door = CreateDoorway(scene, gfxDevice, resources); CreateButton(scene, gfxDevice, resources, *door); + CreateHouse(scene, gfxDevice, resources); CreatePlayer(scene, objects); LoadSpawnBallResources(gfxDevice, resources, objects); diff --git a/tests/destrum_tests.cpp b/tests/destrum_tests.cpp index 344ad51..80ff290 100644 --- a/tests/destrum_tests.cpp +++ b/tests/destrum_tests.cpp @@ -453,6 +453,64 @@ namespace { SceneManager::GetInstance().Destroy(); } + class TriggerCounter final : public Component { + public: + explicit TriggerCounter(GameObject& parent) + : Component(parent, "TriggerCounter") {} + + std::string GetTypeName() const override { return "TriggerCounter"; } + void OnTriggerEnter(GameObject*) override { ++enters; } + void OnTriggerExit(GameObject*) override { ++exits; } + + int enters{0}; + int exits{0}; + }; + + void testNonPhysicsChangesKeepBodies() + { + Scene& scene = SceneManager::GetInstance().CreateScene("physics-refresh"); + + GameObject* trigger = scene.CreateGameObject("trigger"); + trigger->AddComponent(glm::vec3{3.0f})->SetTrigger(true); + trigger->AddComponent()->SetType(RigidbodyType::Static); + auto* counter = trigger->AddComponent(); + + GameObject* crate = scene.CreateGameObject("crate"); + crate->AddComponent(glm::vec3{0.5f}); + auto* crateBody = crate->AddComponent(); + crateBody->SetUseGravity(false); + crateBody->SetAllowSleep(false); + + GameObject* lamp = scene.CreateGameObject("lamp"); + auto* light = lamp->AddComponent(); + + scene.CommitPendingAdditions(); + scene.GetPhysics().RefreshGameObject(*trigger); + scene.GetPhysics().RefreshGameObject(*crate); + + const auto step = [&scene] { + for (int i = 0; i < 10; ++i) { + scene.FixedUpdate(1.0f / 60.0f); + } + }; + step(); + check(counter->enters == 1 && counter->exits == 0, "a body inside a trigger must enter it once"); + + light->SetEnabled(false); + light->SetEnabled(true); + crate->AddComponent()->SetEnabled(false); + step(); + check(counter->enters == 1 && counter->exits == 0, + "non physics components must not rebuild physics bodies"); + + crateBody->SetMass(5.0f); + step(); + check(counter->enters == 2 && counter->exits == 1, + "rebuilding a body inside a trigger must exit the old body and enter the new one"); + + SceneManager::GetInstance().Destroy(); + } + void testRenderer2DShapes() { auto& r2d = Renderer2D::GetInstance(); @@ -1230,6 +1288,7 @@ int main() testPhysicsWorldUnits(); testJoltSphereCollisions(); testSceneJoltSphereCollisions(); + testNonPhysicsChangesKeepBodies(); testAnimatorAssetReferences(); testLightComponentRoundTrip(); testModelFileRoundTrip();