diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 992f0d07..4ba9ad51 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -220,6 +220,28 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneManager.cpp + + # Audio + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Audio/AudioTypes.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Audio/AudioConfig.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Audio/IAudioBackend.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Audio/AudioSystem.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Audio/WASAPI/WASAPIBackend.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Audio/AudioSystem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Audio/WASAPI/WASAPIBackend.cpp + + # Audio Components + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioSourceComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioListenerComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioSourceComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioListenerComponent.cpp + + # Third-party (KissFFT) + ${CMAKE_SOURCE_DIR}/third_party/kissfft/kiss_fft.h + ${CMAKE_SOURCE_DIR}/third_party/kissfft/kiss_fft.c + ${CMAKE_SOURCE_DIR}/third_party/kissfft/kiss_fftr.h + ${CMAKE_SOURCE_DIR}/third_party/kissfft/kiss_fftr.c + ${CMAKE_SOURCE_DIR}/third_party/kissfft/_kiss_fft_guts.h ) target_include_directories(XCEngine PUBLIC @@ -227,6 +249,7 @@ target_include_directories(XCEngine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests/OpenGL/package/include + ${CMAKE_SOURCE_DIR}/third_party ) if(MSVC) diff --git a/engine/include/XCEngine/Components/TransformComponent.h b/engine/include/XCEngine/Components/TransformComponent.h index fb0f9b3b..7e4a2dec 100644 --- a/engine/include/XCEngine/Components/TransformComponent.h +++ b/engine/include/XCEngine/Components/TransformComponent.h @@ -60,7 +60,6 @@ public: // Hierarchy - parent TransformComponent* GetParent() const { return m_parent; } void SetParent(TransformComponent* parent, bool worldPositionStays = true); - void SetParent(TransformComponent* parent) { SetParent(parent, true); } // Hierarchy - children size_t GetChildCount() const { return m_children.size(); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 78a1cbf7..7d5c3016 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,6 +37,8 @@ add_subdirectory(containers) add_subdirectory(memory) add_subdirectory(threading) add_subdirectory(debug) +add_subdirectory(Components) +add_subdirectory(Scene) add_subdirectory(RHI) add_subdirectory(RHI/D3D12) add_subdirectory(RHI/OpenGL) diff --git a/tests/Components/CMakeLists.txt b/tests/Components/CMakeLists.txt new file mode 100644 index 00000000..cf6d1745 --- /dev/null +++ b/tests/Components/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.15) + +project(XCEngine_ComponentsTests) + +set(COMPONENTS_TEST_SOURCES + test_component.cpp + test_transform_component.cpp + test_game_object.cpp +) + +add_executable(xcengine_components_tests ${COMPONENTS_TEST_SOURCES}) + +if(MSVC) + set_target_properties(xcengine_components_tests PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + ) +endif() + +target_link_libraries(xcengine_components_tests PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main +) + +target_include_directories(xcengine_components_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include +) + +add_test(NAME ComponentTests COMMAND xcengine_components_tests) \ No newline at end of file diff --git a/tests/Components/test_component.cpp b/tests/Components/test_component.cpp new file mode 100644 index 00000000..6952ecc3 --- /dev/null +++ b/tests/Components/test_component.cpp @@ -0,0 +1,110 @@ +#include +#include +#include + +using namespace XCEngine::Components; + +namespace { + +class TestComponent : public Component { +public: + TestComponent() = default; + explicit TestComponent(const std::string& name) : m_customName(name) {} + + std::string GetName() const override { + return m_customName.empty() ? "TestComponent" : m_customName; + } + + bool m_awakeCalled = false; + bool m_startCalled = false; + bool m_updateCalled = false; + bool m_onEnableCalled = false; + bool m_onDisableCalled = false; + bool m_onDestroyCalled = false; + + void Awake() override { m_awakeCalled = true; } + void Start() override { m_startCalled = true; } + void Update(float deltaTime) override { m_updateCalled = true; } + void OnEnable() override { m_onEnableCalled = true; } + void OnDisable() override { m_onDisableCalled = true; } + void OnDestroy() override { m_onDestroyCalled = true; } + +private: + std::string m_customName; +}; + +TEST(Component_Test, DefaultConstructor) { + TestComponent comp; + EXPECT_EQ(comp.GetName(), "TestComponent"); +} + +TEST(Component_Test, GetGameObject_ReturnsNullptr_WhenNotAttached) { + TestComponent comp; + EXPECT_EQ(comp.GetGameObject(), nullptr); +} + +TEST(Component_Test, IsEnabled_DefaultTrue) { + TestComponent comp; + EXPECT_TRUE(comp.IsEnabled()); +} + +TEST(Component_Test, SetEnabled_TrueToFalse) { + TestComponent comp; + EXPECT_TRUE(comp.IsEnabled()); + comp.SetEnabled(false); + EXPECT_FALSE(comp.IsEnabled()); +} + +TEST(Component_Test, SetEnabled_FalseToTrue) { + TestComponent comp; + comp.SetEnabled(false); + EXPECT_FALSE(comp.IsEnabled()); + comp.SetEnabled(true); + EXPECT_TRUE(comp.IsEnabled()); +} + +TEST(Component_Test, Lifecycle_AwakeCalled) { + TestComponent comp; + comp.Awake(); + EXPECT_TRUE(comp.m_awakeCalled); +} + +TEST(Component_Test, Lifecycle_StartCalled) { + TestComponent comp; + comp.Start(); + EXPECT_TRUE(comp.m_startCalled); +} + +TEST(Component_Test, Lifecycle_UpdateCalled) { + TestComponent comp; + comp.Update(0.016f); + EXPECT_TRUE(comp.m_updateCalled); +} + +TEST(Component_Test, Lifecycle_OnEnableCalled_WhenEnabling) { + TestComponent comp; + comp.SetEnabled(false); + comp.SetEnabled(true); + EXPECT_TRUE(comp.m_onEnableCalled); +} + +TEST(Component_Test, Lifecycle_OnDisableCalled_WhenDisabling) { + TestComponent comp; + comp.SetEnabled(false); + EXPECT_TRUE(comp.m_onDisableCalled); +} + +TEST(Component_Test, Lifecycle_OnDestroyCalled) { + TestComponent comp; + comp.OnDestroy(); + EXPECT_TRUE(comp.m_onDestroyCalled); +} + +TEST(Component_Test, SetEnabled_NoCallback_WhenStateUnchanged) { + TestComponent comp; + comp.SetEnabled(true); + EXPECT_FALSE(comp.m_onEnableCalled); + EXPECT_FALSE(comp.m_onDisableCalled); +} + +} // namespace \ No newline at end of file diff --git a/tests/Components/test_game_object.cpp b/tests/Components/test_game_object.cpp new file mode 100644 index 00000000..98c99f2d --- /dev/null +++ b/tests/Components/test_game_object.cpp @@ -0,0 +1,277 @@ +#include +#include +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Math; + +namespace { + +class TestComponent : public Component { +public: + TestComponent() = default; + explicit TestComponent(const std::string& name) : m_customName(name) {} + + std::string GetName() const override { + return m_customName.empty() ? "TestComponent" : m_customName; + } + + bool m_awakeCalled = false; + bool m_startCalled = false; + bool m_updateCalled = false; + + void Awake() override { m_awakeCalled = true; } + void Start() override { m_startCalled = true; } + void Update(float deltaTime) override { m_updateCalled = true; } + +private: + std::string m_customName; +}; + +class GameObjectTest : public ::testing::Test { +protected: + void SetUp() override { + testScene = std::make_unique("TestScene"); + } + + std::unique_ptr testScene; +}; + +TEST(GameObject_Test, DefaultConstructor_DefaultValues) { + GameObject go; + + EXPECT_EQ(go.GetName(), "GameObject"); + EXPECT_TRUE(go.IsActive()); + EXPECT_EQ(go.GetID(), GameObject::INVALID_ID); +} + +TEST(GameObject_Test, NamedConstructor) { + GameObject go("TestObject"); + + EXPECT_EQ(go.GetName(), "TestObject"); +} + +TEST(GameObject_Test, AddComponent_Single) { + GameObject go; + + TestComponent* comp = go.AddComponent(); + + EXPECT_NE(comp, nullptr); + EXPECT_EQ(go.GetComponent(), comp); +} + +TEST(GameObject_Test, AddComponent_Multiple) { + GameObject go; + + TestComponent* comp1 = go.AddComponent(); + TestComponent* comp2 = go.AddComponent(); + + EXPECT_NE(comp1, nullptr); + EXPECT_NE(comp2, nullptr); + EXPECT_NE(comp1, comp2); +} + +TEST(GameObject_Test, AddComponent_TransformComponent) { + GameObject go; + + TransformComponent* tc = go.GetTransform(); + + EXPECT_NE(tc, nullptr); + EXPECT_EQ(tc->GetName(), "Transform"); +} + +TEST(GameObject_Test, GetComponent_Exists) { + GameObject go; + TestComponent* comp = go.AddComponent(); + + TestComponent* found = go.GetComponent(); + + EXPECT_EQ(found, comp); +} + +TEST(GameObject_Test, GetComponent_NotExists) { + GameObject go; + + TestComponent* found = go.GetComponent(); + + EXPECT_EQ(found, nullptr); +} + +TEST(GameObject_Test, GetComponents_Multiple) { + GameObject go; + go.AddComponent(); + go.AddComponent(); + go.AddComponent(); + + std::vector comps = go.GetComponents(); + + EXPECT_EQ(comps.size(), 3u); +} + +TEST(GameObject_Test, GetTransform_ReturnsTransform) { + GameObject go; + + TransformComponent* tc = go.GetTransform(); + + EXPECT_NE(tc, nullptr); +} + +TEST(GameObject_Test, SetParent_WithWorldPosition) { + GameObject parent("Parent"); + GameObject child("Child"); + + parent.GetTransform()->SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f)); + child.GetTransform()->SetLocalPosition(Vector3(2.0f, 0.0f, 0.0f)); + + child.SetParent(&parent, true); + + Vector3 childWorldPos = child.GetTransform()->GetPosition(); + EXPECT_NEAR(childWorldPos.x, 2.0f, 0.001f); +} + +TEST(GameObject_Test, SetParent_WithoutWorldPosition) { + GameObject parent("Parent"); + GameObject child("Child"); + + parent.GetTransform()->SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f)); + child.GetTransform()->SetLocalPosition(Vector3(2.0f, 0.0f, 0.0f)); + + child.SetParent(&parent, false); + + Vector3 childWorldPos = child.GetTransform()->GetPosition(); + EXPECT_NEAR(childWorldPos.x, 3.0f, 0.001f); +} + +TEST(GameObject_Test, GetChild_ValidIndex) { + GameObject parent("Parent"); + GameObject child("Child"); + + child.SetParent(&parent); + + GameObject* found = parent.GetChild(0); + + EXPECT_EQ(found, &child); +} + +TEST(GameObject_Test, GetChild_InvalidIndex) { + GameObject parent("Parent"); + + GameObject* found = parent.GetChild(0); + + EXPECT_EQ(found, nullptr); +} + +TEST(GameObject_Test, GetChildren_ReturnsAllChildren) { + GameObject parent("Parent"); + GameObject child1("Child1"); + GameObject child2("Child2"); + + child1.SetParent(&parent); + child2.SetParent(&parent); + + std::vector children = parent.GetChildren(); + + EXPECT_EQ(children.size(), 2u); +} + +TEST(GameObject_Test, DetachChildren) { + GameObject parent("Parent"); + GameObject child("Child"); + + child.SetParent(&parent); + EXPECT_EQ(parent.GetChildCount(), 1u); + + parent.DetachChildren(); + + EXPECT_EQ(parent.GetChildCount(), 0u); +} + +TEST(GameObject_Test, IsActive_DefaultTrue) { + GameObject go; + + EXPECT_TRUE(go.IsActive()); +} + +TEST(GameObject_Test, SetActive_False) { + GameObject go; + + go.SetActive(false); + + EXPECT_FALSE(go.IsActive()); +} + +TEST(GameObject_Test, IsActiveInHierarchy_True) { + GameObject parent("Parent"); + GameObject child("Child"); + + child.SetParent(&parent); + + EXPECT_TRUE(child.IsActiveInHierarchy()); +} + +TEST(GameObject_Test, IsActiveInHierarchy_FalseWhenParentInactive) { + GameObject parent("Parent"); + GameObject child("Child"); + + child.SetParent(&parent); + parent.SetActive(false); + + EXPECT_FALSE(child.IsActiveInHierarchy()); +} + +TEST(GameObject_Test, Lifecycle_Awake) { + GameObject go; + TestComponent* comp = go.AddComponent(); + + go.Awake(); + + EXPECT_TRUE(comp->m_awakeCalled); +} + +TEST(GameObject_Test, Lifecycle_Start) { + GameObject go; + TestComponent* comp = go.AddComponent(); + + go.Start(); + + EXPECT_TRUE(comp->m_startCalled); +} + +TEST(GameObject_Test, Lifecycle_Update) { + GameObject go; + TestComponent* comp = go.AddComponent(); + + go.Update(0.016f); + + EXPECT_TRUE(comp->m_updateCalled); +} + +TEST_F(GameObjectTest, Find_Exists) { + GameObject* go = testScene->CreateGameObject("TestObject"); + + GameObject* found = GameObject::Find("TestObject"); + + EXPECT_NE(found, nullptr); + EXPECT_EQ(found->GetName(), "TestObject"); +} + +TEST(GameObject_Test, Find_NotExists) { + GameObject* found = GameObject::Find("NonExistent"); + + EXPECT_EQ(found, nullptr); +} + +TEST(GameObject_Test, GetChildCount) { + GameObject parent("Parent"); + GameObject child1("Child1"); + GameObject child2("Child2"); + + child1.SetParent(&parent); + child2.SetParent(&parent); + + EXPECT_EQ(parent.GetChildCount(), 2u); +} + +} // namespace \ No newline at end of file diff --git a/tests/Components/test_transform_component.cpp b/tests/Components/test_transform_component.cpp new file mode 100644 index 00000000..88c26e7d --- /dev/null +++ b/tests/Components/test_transform_component.cpp @@ -0,0 +1,306 @@ +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Math; + +namespace { + +class TransformComponentTest : public ::testing::Test { +protected: + void SetUp() override { + transform = std::make_unique(); + } + + std::unique_ptr transform; +}; + +TEST(TransformComponent_Test, DefaultConstructor_IdentityValues) { + TransformComponent tc; + + EXPECT_EQ(tc.GetLocalPosition(), Vector3::Zero()); + EXPECT_TRUE(tc.GetLocalRotation().Dot(Quaternion::Identity()) > 0.99f); + EXPECT_EQ(tc.GetLocalScale(), Vector3::One()); +} + +TEST(TransformComponent_Test, LocalPosition_GetSet) { + TransformComponent tc; + Vector3 pos(1.0f, 2.0f, 3.0f); + + tc.SetLocalPosition(pos); + + EXPECT_EQ(tc.GetLocalPosition(), pos); +} + +TEST(TransformComponent_Test, LocalRotation_GetSet) { + TransformComponent tc; + Quaternion rot = Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f); + + tc.SetLocalRotation(rot); + + EXPECT_TRUE(tc.GetLocalRotation().Dot(rot) > 0.99f); +} + +TEST(TransformComponent_Test, LocalScale_GetSet) { + TransformComponent tc; + Vector3 scale(2.0f, 2.0f, 2.0f); + + tc.SetLocalScale(scale); + + EXPECT_EQ(tc.GetLocalScale(), scale); +} + +TEST(TransformComponent_Test, LocalEulerAngles_GetSet) { + TransformComponent tc; + Vector3 eulers(45.0f, 30.0f, 60.0f); + + tc.SetLocalEulerAngles(eulers); + Vector3 result = tc.GetLocalEulerAngles(); + + EXPECT_NEAR(result.x, eulers.x, 1.0f); + EXPECT_NEAR(result.y, eulers.y, 1.0f); + EXPECT_NEAR(result.z, eulers.z, 1.0f); +} + +TEST(TransformComponent_Test, WorldPosition_NoParent_EqualsLocal) { + TransformComponent tc; + Vector3 pos(1.0f, 2.0f, 3.0f); + + tc.SetLocalPosition(pos); + + EXPECT_EQ(tc.GetPosition(), pos); +} + +TEST(TransformComponent_Test, WorldPosition_WithParent) { + TransformComponent parent; + TransformComponent child; + + parent.SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f)); + child.SetParent(&parent); + child.SetLocalPosition(Vector3(2.0f, 0.0f, 0.0f)); + + Vector3 worldPos = child.GetPosition(); + + EXPECT_NEAR(worldPos.x, 3.0f, 0.001f); +} + +TEST(TransformComponent_Test, WorldRotation_WithParent) { + TransformComponent parent; + TransformComponent child; + + parent.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f)); + child.SetParent(&parent); + child.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f)); + + Quaternion worldRot = child.GetRotation(); + + EXPECT_TRUE(worldRot.Magnitude() > 0.0f); +} + +TEST(TransformComponent_Test, WorldScale_WithParent) { + TransformComponent parent; + TransformComponent child; + + parent.SetLocalScale(Vector3(2.0f, 2.0f, 2.0f)); + child.SetParent(&parent); + child.SetLocalScale(Vector3(2.0f, 2.0f, 2.0f)); + + Vector3 worldScale = child.GetScale(); + + EXPECT_NEAR(worldScale.x, 4.0f, 0.001f); + EXPECT_NEAR(worldScale.y, 4.0f, 0.001f); + EXPECT_NEAR(worldScale.z, 4.0f, 0.001f); +} + +TEST(TransformComponent_Test, DirectionVectors_Forward) { + TransformComponent tc; + + Vector3 forward = tc.GetForward(); + + EXPECT_NEAR(forward.z, 1.0f, 0.001f); +} + +TEST(TransformComponent_Test, DirectionVectors_Right) { + TransformComponent tc; + + Vector3 right = tc.GetRight(); + + EXPECT_NEAR(right.x, 1.0f, 0.001f); +} + +TEST(TransformComponent_Test, DirectionVectors_Up) { + TransformComponent tc; + + Vector3 up = tc.GetUp(); + + EXPECT_NEAR(up.y, 1.0f, 0.001f); +} + +TEST(TransformComponent_Test, LocalToWorldMatrix_Identity) { + TransformComponent tc; + + const Matrix4x4& matrix = tc.GetLocalToWorldMatrix(); + + EXPECT_EQ(matrix[0][0], 1.0f); + EXPECT_EQ(matrix[1][1], 1.0f); + EXPECT_EQ(matrix[2][2], 1.0f); + EXPECT_EQ(matrix[3][3], 1.0f); +} + +TEST(TransformComponent_Test, LocalToWorldMatrix_Translation) { + TransformComponent tc; + Vector3 pos(5.0f, 10.0f, 15.0f); + tc.SetLocalPosition(pos); + + const Matrix4x4& matrix = tc.GetLocalToWorldMatrix(); + + EXPECT_NEAR(matrix[0][3], pos.x, 0.001f); + EXPECT_NEAR(matrix[1][3], pos.y, 0.001f); + EXPECT_NEAR(matrix[2][3], pos.z, 0.001f); +} + +TEST(TransformComponent_Test, LocalToWorldMatrix_Rotation) { + TransformComponent tc; + tc.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f)); + + const Matrix4x4& matrix = tc.GetLocalToWorldMatrix(); + + EXPECT_NEAR(matrix[0][0], 0.0f, 0.001f); + EXPECT_NEAR(matrix[0][2], 1.0f, 0.001f); +} + +TEST(TransformComponent_Test, LookAt_Target) { + TransformComponent tc; + Vector3 target(10.0f, 0.0f, 0.0f); + + tc.SetPosition(Vector3::Zero()); + tc.LookAt(target); + + Vector3 forward = tc.GetForward(); + EXPECT_NEAR(forward.x, 1.0f, 0.1f); +} + +TEST(TransformComponent_Test, Rotate_Eulers) { + TransformComponent tc; + + tc.Rotate(Vector3(90.0f, 0.0f, 0.0f)); + + Vector3 eulers = tc.GetLocalEulerAngles(); + EXPECT_TRUE(eulers.x > 80.0f); +} + +TEST(TransformComponent_Test, Translate_Self) { + TransformComponent tc; + Vector3 initialPos = tc.GetLocalPosition(); + + tc.Translate(Vector3(1.0f, 2.0f, 3.0f), Space::Self); + + Vector3 newPos = tc.GetLocalPosition(); + EXPECT_NEAR(newPos.x, initialPos.x + 1.0f, 0.001f); + EXPECT_NEAR(newPos.y, initialPos.y + 2.0f, 0.001f); + EXPECT_NEAR(newPos.z, initialPos.z + 3.0f, 0.001f); +} + +TEST(TransformComponent_Test, TransformPoint_LocalToWorld) { + TransformComponent tc; + tc.SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f)); + + Vector3 localPoint(1.0f, 0.0f, 0.0f); + Vector3 worldPoint = tc.TransformPoint(localPoint); + + EXPECT_NEAR(worldPoint.x, 2.0f, 0.001f); + EXPECT_NEAR(worldPoint.y, 2.0f, 0.001f); + EXPECT_NEAR(worldPoint.z, 3.0f, 0.001f); +} + +TEST(TransformComponent_Test, InverseTransformPoint_WorldToLocal) { + TransformComponent tc; + tc.SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f)); + + Vector3 worldPoint(2.0f, 2.0f, 3.0f); + Vector3 localPoint = tc.InverseTransformPoint(worldPoint); + + EXPECT_NEAR(localPoint.x, 1.0f, 0.001f); + EXPECT_NEAR(localPoint.y, 0.0f, 0.001f); + EXPECT_NEAR(localPoint.z, 0.0f, 0.001f); +} + +TEST(TransformComponent_Test, TransformDirection) { + TransformComponent tc; + tc.SetLocalRotation(Quaternion::FromAxisAngle(Vector3::Up(), PI * 0.5f)); + + Vector3 localDir(1.0f, 0.0f, 0.0f); + Vector3 worldDir = tc.TransformDirection(localDir); + + EXPECT_NEAR(worldDir.z, -1.0f, 0.1f); +} + +TEST(TransformComponent_Test, SetDirty_PropagatesToChildren) { + TransformComponent parent; + TransformComponent child; + child.SetParent(&parent); + + parent.SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f)); + child.SetLocalPosition(Vector3(1.0f, 0.0f, 0.0f)); + + Vector3 childWorldPosBefore = child.GetPosition(); + + parent.SetLocalPosition(Vector3(10.0f, 0.0f, 0.0f)); + Vector3 childWorldPosAfter = child.GetPosition(); + + EXPECT_NE(childWorldPosBefore.x, childWorldPosAfter.x); +} + +TEST(TransformComponent_Test, GetChildCount_Empty) { + TransformComponent tc; + EXPECT_EQ(tc.GetChildCount(), 0u); +} + +TEST(TransformComponent_Test, GetChild_InvalidIndex) { + TransformComponent tc; + EXPECT_EQ(tc.GetChild(0), nullptr); +} + +TEST(TransformComponent_Test, DetachChildren) { + TransformComponent parent; + TransformComponent child1; + TransformComponent child2; + + child1.SetParent(&parent); + child2.SetParent(&parent); + + EXPECT_EQ(parent.GetChildCount(), 2u); + + parent.DetachChildren(); + + EXPECT_EQ(parent.GetChildCount(), 0u); +} + +TEST(TransformComponent_Test, SetAsFirstSibling) { + TransformComponent parent; + TransformComponent child1; + TransformComponent child2; + + child1.SetParent(&parent); + child2.SetParent(&parent); + + child2.SetAsFirstSibling(); + + EXPECT_EQ(child2.GetSiblingIndex(), 0); +} + +TEST(TransformComponent_Test, SetAsLastSibling) { + TransformComponent parent; + TransformComponent child1; + TransformComponent child2; + + child1.SetParent(&parent); + child2.SetParent(&parent); + + child1.SetAsLastSibling(); + + EXPECT_EQ(child1.GetSiblingIndex(), 1); +} + +} // namespace \ No newline at end of file diff --git a/tests/Scene/CMakeLists.txt b/tests/Scene/CMakeLists.txt new file mode 100644 index 00000000..705ad7a8 --- /dev/null +++ b/tests/Scene/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.15) + +project(XCEngine_SceneTests) + +set(SCENE_TEST_SOURCES + test_scene.cpp + test_scene_manager.cpp +) + +add_executable(xcengine_scene_tests ${SCENE_TEST_SOURCES}) + +if(MSVC) + set_target_properties(xcengine_scene_tests PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + ) +endif() + +target_link_libraries(xcengine_scene_tests PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main +) + +target_include_directories(xcengine_scene_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include +) + +add_test(NAME SceneTests COMMAND xcengine_scene_tests) \ No newline at end of file diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp new file mode 100644 index 00000000..29cb0732 --- /dev/null +++ b/tests/Scene/test_scene.cpp @@ -0,0 +1,217 @@ +#include +#include +#include +#include + +using namespace XCEngine::Components; + +namespace { + +class TestComponent : public Component { +public: + TestComponent() = default; + explicit TestComponent(const std::string& name) : m_customName(name) {} + + std::string GetName() const override { + return m_customName.empty() ? "TestComponent" : m_customName; + } + + bool m_awakeCalled = false; + bool m_updateCalled = false; + + void Awake() override { m_awakeCalled = true; } + void Update(float deltaTime) override { m_updateCalled = true; } + +private: + std::string m_customName; +}; + +class SceneTest : public ::testing::Test { +protected: + void SetUp() override { + testScene = std::make_unique("TestScene"); + } + + std::unique_ptr testScene; +}; + +TEST(Scene_Test, DefaultConstructor_DefaultName) { + Scene s; + + EXPECT_EQ(s.GetName(), "Untitled"); +} + +TEST(Scene_Test, NamedConstructor) { + Scene s("MyScene"); + + EXPECT_EQ(s.GetName(), "MyScene"); +} + +TEST_F(SceneTest, CreateGameObject_Simple) { + GameObject* go = testScene->CreateGameObject("TestObject"); + + EXPECT_NE(go, nullptr); + EXPECT_EQ(go->GetName(), "TestObject"); +} + +TEST_F(SceneTest, CreateGameObject_WithParent) { + GameObject* parent = testScene->CreateGameObject("Parent"); + GameObject* child = testScene->CreateGameObject("Child", parent); + + EXPECT_NE(child, nullptr); + EXPECT_EQ(child->GetParent(), parent); +} + +TEST_F(SceneTest, CreateGameObject_AssignsID) { + GameObject* go1 = testScene->CreateGameObject("Object1"); + GameObject* go2 = testScene->CreateGameObject("Object2"); + + EXPECT_NE(go1->GetID(), GameObject::INVALID_ID); + EXPECT_NE(go2->GetID(), GameObject::INVALID_ID); + EXPECT_NE(go1->GetID(), go2->GetID()); +} + +TEST_F(SceneTest, CreateGameObject_AddsToRoot) { + GameObject* go = testScene->CreateGameObject("RootObject"); + + auto roots = testScene->GetRootGameObjects(); + + EXPECT_EQ(roots.size(), 1u); + EXPECT_EQ(roots[0], go); +} + +TEST_F(SceneTest, DestroyGameObject_Simple) { + GameObject* go = testScene->CreateGameObject("TestObject"); + + testScene->DestroyGameObject(go); + + EXPECT_EQ(testScene->Find("TestObject"), nullptr); +} + +TEST_F(SceneTest, DestroyGameObject_WithChildren) { + GameObject* parent = testScene->CreateGameObject("Parent"); + testScene->CreateGameObject("Child", parent); + + testScene->DestroyGameObject(parent); + + EXPECT_EQ(testScene->Find("Parent"), nullptr); + EXPECT_EQ(testScene->Find("Child"), nullptr); +} + +TEST_F(SceneTest, Find_Exists) { + testScene->CreateGameObject("FindMe"); + + GameObject* found = testScene->Find("FindMe"); + + EXPECT_NE(found, nullptr); + EXPECT_EQ(found->GetName(), "FindMe"); +} + +TEST_F(SceneTest, Find_NotExists) { + GameObject* found = testScene->Find("NonExistent"); + + EXPECT_EQ(found, nullptr); +} + +TEST_F(SceneTest, GetRootGameObjects_ReturnsTopLevel) { + GameObject* parent = testScene->CreateGameObject("Parent"); + testScene->CreateGameObject("Child", parent); + + auto roots = testScene->GetRootGameObjects(); + + EXPECT_EQ(roots.size(), 1u); + EXPECT_EQ(roots[0], parent); +} + +TEST(Scene_Test, GetRootGameObjects_EmptyScene) { + Scene emptyScene; + + auto roots = emptyScene.GetRootGameObjects(); + + EXPECT_EQ(roots.size(), 0u); +} + +TEST_F(SceneTest, Update_UpdatesActiveObjects) { + GameObject* go = testScene->CreateGameObject("TestObject"); + TestComponent* comp = go->AddComponent(); + + testScene->Update(0.016f); + + EXPECT_TRUE(comp->m_updateCalled); +} + +TEST_F(SceneTest, Update_SkipsInactiveObjects) { + GameObject* go = testScene->CreateGameObject("InactiveObject"); + TestComponent* comp = go->AddComponent(); + go->SetActive(false); + + testScene->Update(0.016f); + + EXPECT_FALSE(comp->m_updateCalled); +} + +TEST(Scene_Test, IsActive_DefaultTrue) { + Scene s; + + EXPECT_TRUE(s.IsActive()); +} + +TEST(Scene_Test, SetActive) { + Scene s; + s.SetActive(false); + + EXPECT_FALSE(s.IsActive()); +} + +TEST_F(SceneTest, FindObjectOfType) { + GameObject* go = testScene->CreateGameObject("TestObject"); + go->AddComponent(); + + TestComponent* found = testScene->FindObjectOfType(); + + EXPECT_NE(found, nullptr); +} + +TEST_F(SceneTest, FindObjectsOfType) { + GameObject* go1 = testScene->CreateGameObject("Object1"); + GameObject* go2 = testScene->CreateGameObject("Object2"); + go1->AddComponent(); + go2->AddComponent(); + + auto found = testScene->FindObjectsOfType(); + + EXPECT_EQ(found.size(), 2u); +} + +TEST_F(SceneTest, FindGameObjectWithTag) { + GameObject* go = testScene->CreateGameObject("MyTag"); + + GameObject* found = testScene->FindGameObjectWithTag("MyTag"); + + EXPECT_NE(found, nullptr); +} + +TEST_F(SceneTest, OnGameObjectCreated_Event) { + bool eventFired = false; + testScene->OnGameObjectCreated().Subscribe([&eventFired](GameObject*) { + eventFired = true; + }); + + testScene->CreateGameObject("TestObject"); + + EXPECT_TRUE(eventFired); +} + +TEST_F(SceneTest, OnGameObjectDestroyed_Event) { + bool eventFired = false; + testScene->OnGameObjectDestroyed().Subscribe([&eventFired](GameObject*) { + eventFired = true; + }); + + GameObject* go = testScene->CreateGameObject("TestObject"); + testScene->DestroyGameObject(go); + + EXPECT_TRUE(eventFired); +} + +} // namespace \ No newline at end of file diff --git a/tests/Scene/test_scene_manager.cpp b/tests/Scene/test_scene_manager.cpp new file mode 100644 index 00000000..0f93c1f7 --- /dev/null +++ b/tests/Scene/test_scene_manager.cpp @@ -0,0 +1,172 @@ +#include +#include +#include + +using namespace XCEngine::Components; + +namespace { + +class SceneManagerTest : public ::testing::Test { +protected: + void TearDown() override { + auto scenes = SceneManager::Get().GetAllScenes(); + for (auto* scene : scenes) { + SceneManager::Get().UnloadScene(scene); + } + } +}; + +TEST(SceneManager_Test, Get_ReturnsSingleton) { + SceneManager& sm1 = SceneManager::Get(); + SceneManager& sm2 = SceneManager::Get(); + + EXPECT_EQ(&sm1, &sm2); +} + +TEST(SceneManager_Test, CreateScene_Simple) { + SceneManager& sm = SceneManager::Get(); + + Scene* scene = sm.CreateScene("TestScene"); + + EXPECT_NE(scene, nullptr); + EXPECT_EQ(scene->GetName(), "TestScene"); +} + +TEST(SceneManager_Test, CreateScene_AssignsName) { + SceneManager& sm = SceneManager::Get(); + + Scene* scene = sm.CreateScene("MyScene"); + + EXPECT_EQ(scene->GetName(), "MyScene"); +} + +TEST(SceneManager_Test, GetScene_Exists) { + SceneManager& sm = SceneManager::Get(); + sm.CreateScene("TestScene"); + + Scene* found = sm.GetScene("TestScene"); + + EXPECT_NE(found, nullptr); + EXPECT_EQ(found->GetName(), "TestScene"); +} + +TEST(SceneManager_Test, GetScene_NotExists) { + SceneManager& sm = SceneManager::Get(); + + Scene* found = sm.GetScene("NonExistent"); + + EXPECT_EQ(found, nullptr); +} + +TEST(SceneManager_Test, GetAllScenes_ReturnsAll) { + SceneManager& sm = SceneManager::Get(); + sm.CreateScene("Scene1"); + sm.CreateScene("Scene2"); + sm.CreateScene("Scene3"); + + auto scenes = sm.GetAllScenes(); + + EXPECT_EQ(scenes.size(), 3u); +} + +TEST(SceneManager_Test, SetActiveScene) { + SceneManager& sm = SceneManager::Get(); + Scene* scene1 = sm.CreateScene("Scene1"); + Scene* scene2 = sm.CreateScene("Scene2"); + + sm.SetActiveScene(scene2); + + EXPECT_EQ(sm.GetActiveScene(), scene2); +} + +TEST(SceneManager_Test, SetActiveScene_ByName) { + SceneManager& sm = SceneManager::Get(); + sm.CreateScene("Scene1"); + sm.CreateScene("Scene2"); + + sm.SetActiveScene("Scene2"); + + EXPECT_EQ(sm.GetActiveScene()->GetName(), "Scene2"); +} + +TEST(SceneManager_Test, GetActiveScene_InitiallyFirst) { + SceneManager& sm = SceneManager::Get(); + auto scenes = sm.GetAllScenes(); + for (auto* scene : scenes) { + sm.UnloadScene(scene); + } + + Scene* scene1 = sm.CreateScene("First"); + + EXPECT_EQ(sm.GetActiveScene(), scene1); +} + +TEST(SceneManager_Test, UnloadScene) { + SceneManager& sm = SceneManager::Get(); + Scene* scene = sm.CreateScene("ToUnload"); + + sm.UnloadScene(scene); + + EXPECT_EQ(sm.GetScene("ToUnload"), nullptr); +} + +TEST(SceneManager_Test, UnloadScene_ByName) { + SceneManager& sm = SceneManager::Get(); + sm.CreateScene("ToUnload"); + + sm.UnloadScene("ToUnload"); + + EXPECT_EQ(sm.GetScene("ToUnload"), nullptr); +} + +TEST(SceneManager_Test, OnSceneLoaded_Event) { + SceneManager& sm = SceneManager::Get(); + bool eventFired = false; + sm.OnSceneLoaded().Subscribe([&eventFired](Scene*) { + eventFired = true; + }); + + sm.CreateScene("TestScene"); + + EXPECT_TRUE(eventFired); +} + +TEST(SceneManager_Test, OnActiveSceneChanged_Event) { + SceneManager& sm = SceneManager::Get(); + Scene* scene1 = sm.CreateScene("Scene1"); + Scene* scene2 = sm.CreateScene("Scene2"); + + bool eventFired = false; + sm.OnActiveSceneChanged().Subscribe([&eventFired](Scene*) { + eventFired = true; + }); + + sm.SetActiveScene(scene2); + + EXPECT_TRUE(eventFired); +} + +TEST(SceneManager_Test, CreateScene_MultipleScenes) { + SceneManager& sm = SceneManager::Get(); + + Scene* scene1 = sm.CreateScene("Scene1"); + Scene* scene2 = sm.CreateScene("Scene2"); + Scene* scene3 = sm.CreateScene("Scene3"); + + EXPECT_NE(scene1, scene2); + EXPECT_NE(scene2, scene3); + EXPECT_NE(scene1, scene3); +} + +TEST(SceneManager_Test, SetActiveScene_ToNullptr) { + SceneManager& sm = SceneManager::Get(); + Scene* scene1 = sm.CreateScene("Scene1"); + Scene* scene2 = sm.CreateScene("Scene2"); + sm.SetActiveScene(scene2); + + sm.SetActiveScene(nullptr); + + EXPECT_EQ(sm.GetActiveScene(), nullptr); +} + +} // namespace \ No newline at end of file