diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 0a9fc65d..65c8debd 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -180,6 +180,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/OpenGL/OpenGLDescriptorSet.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/OpenGL/OpenGLPipelineLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/third_party/GLAD/src/glad.c + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/stb/stb_image.cpp # RHI Factory ${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/RHIFactory.cpp @@ -239,12 +240,22 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioClip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp + # Scripting + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptField.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptFieldStorage.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptField.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptFieldStorage.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptComponent.cpp + # Components ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/TransformComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/GameObject.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/CameraComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/LightComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/MeshFilterComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/MeshRendererComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioSourceComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioListenerComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/ComponentFactoryRegistry.h @@ -253,10 +264,29 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/GameObject.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/CameraComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/LightComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/MeshFilterComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/MeshRendererComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioSourceComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioListenerComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/ComponentFactoryRegistry.cpp + # Rendering + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderContext.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderCameraData.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/VisibleRenderObject.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderSceneExtractor.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderPipeline.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderPipelineAsset.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderSurface.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/RenderResourceCache.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/SceneRenderer.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSurface.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderSceneExtractor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/RenderResourceCache.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/SceneRenderer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp + # Scene ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h diff --git a/engine/include/XCEngine/Components/Component.h b/engine/include/XCEngine/Components/Component.h index 4cc62aad..a50f6165 100644 --- a/engine/include/XCEngine/Components/Component.h +++ b/engine/include/XCEngine/Components/Component.h @@ -34,16 +34,7 @@ public: Scene* GetScene() const; bool IsEnabled() const { return m_enabled; } - void SetEnabled(bool enabled) { - if (m_enabled != enabled) { - m_enabled = enabled; - if (m_enabled) { - OnEnable(); - } else { - OnDisable(); - } - } - } + void SetEnabled(bool enabled); protected: Component() = default; @@ -58,4 +49,4 @@ protected: }; } // namespace Components -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/include/XCEngine/Components/GameObject.h b/engine/include/XCEngine/Components/GameObject.h index 30fba2ae..67627021 100644 --- a/engine/include/XCEngine/Components/GameObject.h +++ b/engine/include/XCEngine/Components/GameObject.h @@ -208,10 +208,15 @@ public: void Deserialize(std::istream& is); private: + void NotifyComponentsBecameActive(); + void NotifyComponentsBecameInactive(); + void PropagateActiveInHierarchyChange(bool oldParentActiveInHierarchy, bool newParentActiveInHierarchy); + ID m_id = INVALID_ID; uint64_t m_uuid = 0; std::string m_name; bool m_activeSelf = true; + bool m_started = false; GameObject* m_parent = nullptr; std::vector m_children; diff --git a/engine/include/XCEngine/Scripting/ScriptComponent.h b/engine/include/XCEngine/Scripting/ScriptComponent.h new file mode 100644 index 00000000..9b5e3c5d --- /dev/null +++ b/engine/include/XCEngine/Scripting/ScriptComponent.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include +#include + +namespace XCEngine { +namespace Scripting { + +class ScriptComponent : public Components::Component { +public: + ScriptComponent(); + + std::string GetName() const override { return "ScriptComponent"; } + + uint64_t GetScriptComponentUUID() const { return m_scriptComponentUUID; } + + const std::string& GetAssemblyName() const { return m_assemblyName; } + void SetAssemblyName(const std::string& assemblyName) { m_assemblyName = assemblyName; } + + const std::string& GetNamespaceName() const { return m_namespaceName; } + void SetNamespaceName(const std::string& namespaceName) { m_namespaceName = namespaceName; } + + const std::string& GetClassName() const { return m_className; } + void SetClassName(const std::string& className) { m_className = className; } + + void SetScriptClass(const std::string& namespaceName, const std::string& className); + void SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className); + + bool HasScriptClass() const { return !m_className.empty(); } + std::string GetFullClassName() const; + + ScriptFieldStorage& GetFieldStorage() { return m_fieldStorage; } + const ScriptFieldStorage& GetFieldStorage() const { return m_fieldStorage; } + void SetFieldStorage(const ScriptFieldStorage& fieldStorage) { m_fieldStorage = fieldStorage; } + + void Serialize(std::ostream& os) const override; + void Deserialize(std::istream& is) override; + +private: + uint64_t m_scriptComponentUUID = 0; + std::string m_assemblyName = "GameScripts"; + std::string m_namespaceName; + std::string m_className; + ScriptFieldStorage m_fieldStorage; +}; + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/include/XCEngine/Scripting/ScriptField.h b/engine/include/XCEngine/Scripting/ScriptField.h new file mode 100644 index 00000000..a1b7050f --- /dev/null +++ b/engine/include/XCEngine/Scripting/ScriptField.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Scripting { + +enum class ScriptFieldType { + None = 0, + Float, + Double, + Bool, + Int32, + UInt64, + String, + Vector2, + Vector3, + Vector4, + GameObject +}; + +struct GameObjectReference { + uint64_t gameObjectUUID = 0; + + bool operator==(const GameObjectReference& other) const { + return gameObjectUUID == other.gameObjectUUID; + } + + bool operator!=(const GameObjectReference& other) const { + return !(*this == other); + } +}; + +using ScriptFieldValue = std::variant< + std::monostate, + float, + double, + bool, + int32_t, + uint64_t, + std::string, + Math::Vector2, + Math::Vector3, + Math::Vector4, + GameObjectReference>; + +std::string ScriptFieldTypeToString(ScriptFieldType type); +bool TryParseScriptFieldType(const std::string& value, ScriptFieldType& outType); + +bool IsScriptFieldValueCompatible(ScriptFieldType type, const ScriptFieldValue& value); +ScriptFieldValue CreateDefaultScriptFieldValue(ScriptFieldType type); +std::string SerializeScriptFieldValue(ScriptFieldType type, const ScriptFieldValue& value); +bool TryDeserializeScriptFieldValue(ScriptFieldType type, const std::string& value, ScriptFieldValue& outValue); + +std::string EscapeScriptString(const std::string& value); +std::string UnescapeScriptString(const std::string& value); + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/include/XCEngine/Scripting/ScriptFieldStorage.h b/engine/include/XCEngine/Scripting/ScriptFieldStorage.h new file mode 100644 index 00000000..e7b23589 --- /dev/null +++ b/engine/include/XCEngine/Scripting/ScriptFieldStorage.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Scripting { + +struct StoredScriptField { + ScriptFieldType type = ScriptFieldType::None; + ScriptFieldValue value = std::monostate{}; +}; + +template +struct ScriptFieldTypeResolver; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Float; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Double; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Bool; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Int32; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::UInt64; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::String; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Vector2; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Vector3; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::Vector4; +}; + +template<> +struct ScriptFieldTypeResolver { + static constexpr ScriptFieldType value = ScriptFieldType::GameObject; +}; + +class ScriptFieldStorage { +public: + template + bool SetFieldValue(const std::string& fieldName, const T& value) { + using ValueType = std::decay_t; + return SetFieldValue(fieldName, ScriptFieldTypeResolver::value, ScriptFieldValue(ValueType(value))); + } + + bool SetFieldValue(const std::string& fieldName, const char* value) { + return SetFieldValue(fieldName, value ? std::string(value) : std::string()); + } + + bool SetFieldValue(const std::string& fieldName, ScriptFieldType type, const ScriptFieldValue& value); + + template + bool TryGetFieldValue(const std::string& fieldName, T& outValue) const { + const StoredScriptField* field = FindField(fieldName); + if (!field) { + return false; + } + + const auto* storedValue = std::get_if>(&field->value); + if (!storedValue) { + return false; + } + + outValue = *storedValue; + return true; + } + + const StoredScriptField* FindField(const std::string& fieldName) const; + StoredScriptField* FindField(const std::string& fieldName); + + bool Contains(const std::string& fieldName) const; + bool Remove(const std::string& fieldName); + void Clear(); + + size_t GetFieldCount() const { return m_fields.size(); } + std::vector GetFieldNames() const; + + std::string SerializeToString() const; + void DeserializeFromString(const std::string& data); + + void Serialize(std::ostream& os) const; + void Deserialize(std::istream& is); + +private: + std::unordered_map m_fields; +}; + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/src/Components/Component.cpp b/engine/src/Components/Component.cpp index 1840f82f..6deb675f 100644 --- a/engine/src/Components/Component.cpp +++ b/engine/src/Components/Component.cpp @@ -8,5 +8,35 @@ TransformComponent& Component::transform() const { return *m_gameObject->GetTransform(); } +Scene* Component::GetScene() const { + return m_gameObject ? m_gameObject->GetScene() : nullptr; +} + +void Component::SetEnabled(bool enabled) { + if (m_enabled == enabled) { + return; + } + + bool wasEffectivelyEnabled = m_enabled; + bool isEffectivelyEnabled = enabled; + + if (m_gameObject) { + wasEffectivelyEnabled = m_enabled && m_gameObject->IsActiveInHierarchy(); + isEffectivelyEnabled = enabled && m_gameObject->IsActiveInHierarchy(); + } + + m_enabled = enabled; + + if (wasEffectivelyEnabled == isEffectivelyEnabled) { + return; + } + + if (isEffectivelyEnabled) { + OnEnable(); + } else { + OnDisable(); + } +} + +} } -} \ No newline at end of file diff --git a/engine/src/Components/ComponentFactoryRegistry.cpp b/engine/src/Components/ComponentFactoryRegistry.cpp index 56d460cd..953b914a 100644 --- a/engine/src/Components/ComponentFactoryRegistry.cpp +++ b/engine/src/Components/ComponentFactoryRegistry.cpp @@ -5,6 +5,9 @@ #include "Components/CameraComponent.h" #include "Components/GameObject.h" #include "Components/LightComponent.h" +#include "Components/MeshFilterComponent.h" +#include "Components/MeshRendererComponent.h" +#include "Scripting/ScriptComponent.h" namespace XCEngine { namespace Components { @@ -28,6 +31,9 @@ ComponentFactoryRegistry::ComponentFactoryRegistry() { RegisterFactory("Light", &CreateBuiltInComponent); RegisterFactory("AudioSource", &CreateBuiltInComponent); RegisterFactory("AudioListener", &CreateBuiltInComponent); + RegisterFactory("MeshFilter", &CreateBuiltInComponent); + RegisterFactory("MeshRenderer", &CreateBuiltInComponent); + RegisterFactory("ScriptComponent", &CreateBuiltInComponent); } void ComponentFactoryRegistry::RegisterFactory(const std::string& typeName, CreateComponentFn createFn) { diff --git a/engine/src/Components/GameObject.cpp b/engine/src/Components/GameObject.cpp index f8dfa7ae..da9180fb 100644 --- a/engine/src/Components/GameObject.cpp +++ b/engine/src/Components/GameObject.cpp @@ -43,6 +43,41 @@ std::unordered_map& GameObject::GetGlobalRegistry() return registry; } +void GameObject::NotifyComponentsBecameActive() { + for (auto& comp : m_components) { + if (comp->IsEnabled()) { + comp->OnEnable(); + } + } +} + +void GameObject::NotifyComponentsBecameInactive() { + for (auto& comp : m_components) { + if (comp->IsEnabled()) { + comp->OnDisable(); + } + } +} + +void GameObject::PropagateActiveInHierarchyChange(bool oldParentActiveInHierarchy, bool newParentActiveInHierarchy) { + const bool wasActiveInHierarchy = oldParentActiveInHierarchy && m_activeSelf; + const bool isActiveInHierarchy = newParentActiveInHierarchy && m_activeSelf; + + if (wasActiveInHierarchy != isActiveInHierarchy) { + if (isActiveInHierarchy) { + NotifyComponentsBecameActive(); + } else { + NotifyComponentsBecameInactive(); + } + } + + for (auto* child : m_children) { + if (child) { + child->PropagateActiveInHierarchyChange(wasActiveInHierarchy, isActiveInHierarchy); + } + } +} + void GameObject::SetParent(GameObject* parent) { SetParent(parent, true); } @@ -52,6 +87,9 @@ void GameObject::SetParent(GameObject* parent, bool worldPositionStays) { return; } + const bool oldParentActiveInHierarchy = m_parent ? m_parent->IsActiveInHierarchy() : true; + const bool wasActiveInHierarchy = oldParentActiveInHierarchy && m_activeSelf; + if (m_parent) { auto& siblings = m_parent->m_children; siblings.erase(std::remove(siblings.begin(), siblings.end(), this), siblings.end()); @@ -72,6 +110,21 @@ void GameObject::SetParent(GameObject* parent, bool worldPositionStays) { } GetTransform()->SetParent(parent ? parent->GetTransform() : nullptr, worldPositionStays); + + const bool newParentActiveInHierarchy = m_parent ? m_parent->IsActiveInHierarchy() : true; + const bool isActiveInHierarchy = newParentActiveInHierarchy && m_activeSelf; + if (wasActiveInHierarchy != isActiveInHierarchy) { + if (isActiveInHierarchy) { + NotifyComponentsBecameActive(); + } else { + NotifyComponentsBecameInactive(); + } + for (auto* child : m_children) { + if (child) { + child->PropagateActiveInHierarchyChange(wasActiveInHierarchy, isActiveInHierarchy); + } + } + } } GameObject* GameObject::GetChild(size_t index) const { @@ -101,9 +154,26 @@ void GameObject::DetachFromParent() { } void GameObject::SetActive(bool active) { - if (m_activeSelf != active) { - m_activeSelf = active; - if (m_parent == nullptr || m_parent->IsActiveInHierarchy()) { + if (m_activeSelf == active) { + return; + } + + const bool parentActiveInHierarchy = m_parent ? m_parent->IsActiveInHierarchy() : true; + const bool wasActiveInHierarchy = parentActiveInHierarchy && m_activeSelf; + + m_activeSelf = active; + + const bool isActiveInHierarchy = parentActiveInHierarchy && m_activeSelf; + if (wasActiveInHierarchy != isActiveInHierarchy) { + if (isActiveInHierarchy) { + NotifyComponentsBecameActive(); + } else { + NotifyComponentsBecameInactive(); + } + for (auto* child : m_children) { + if (child) { + child->PropagateActiveInHierarchyChange(wasActiveInHierarchy, isActiveInHierarchy); + } } } } @@ -155,35 +225,78 @@ void GameObject::Awake() { } void GameObject::Start() { - for (auto& comp : m_components) { - if (comp->IsEnabled()) { - comp->Start(); + if (!IsActiveInHierarchy()) { + return; + } + + if (!m_started) { + for (auto& comp : m_components) { + if (comp->IsEnabled()) { + comp->Start(); + } + } + m_started = true; + } + + for (auto* child : m_children) { + if (child) { + child->Start(); } } } void GameObject::Update(float deltaTime) { + if (!IsActiveInHierarchy()) { + return; + } + for (auto& comp : m_components) { if (comp->IsEnabled()) { comp->Update(deltaTime); } } + + for (auto* child : m_children) { + if (child) { + child->Update(deltaTime); + } + } } void GameObject::FixedUpdate() { + if (!IsActiveInHierarchy()) { + return; + } + for (auto& comp : m_components) { if (comp->IsEnabled()) { comp->FixedUpdate(); } } + + for (auto* child : m_children) { + if (child) { + child->FixedUpdate(); + } + } } void GameObject::LateUpdate(float deltaTime) { + if (!IsActiveInHierarchy()) { + return; + } + for (auto& comp : m_components) { if (comp->IsEnabled()) { comp->LateUpdate(deltaTime); } } + + for (auto* child : m_children) { + if (child) { + child->LateUpdate(deltaTime); + } + } } void GameObject::OnDestroy() { @@ -193,9 +306,10 @@ void GameObject::OnDestroy() { } void GameObject::Destroy() { - OnDestroy(); if (m_scene) { m_scene->DestroyGameObject(this); + } else { + OnDestroy(); } } @@ -203,6 +317,7 @@ void GameObject::Serialize(std::ostream& os) const { os << "name=" << m_name << ";"; os << "active=" << (m_activeSelf ? "1" : "0") << ";"; os << "id=" << m_id << ";"; + os << "uuid=" << m_uuid << ";"; os << "transform="; m_transform->Serialize(os); os << ";"; @@ -233,6 +348,9 @@ void GameObject::Deserialize(std::istream& is) { } else if (strcmp(key, "id") == 0) { is >> m_id; if (is.peek() == ';') is.get(); + } else if (strcmp(key, "uuid") == 0) { + is >> m_uuid; + if (is.peek() == ';') is.get(); } else if (strcmp(key, "transform") == 0) { m_transform->Deserialize(is); if (is.peek() == ';') is.get(); diff --git a/engine/src/Scene/Scene.cpp b/engine/src/Scene/Scene.cpp index 4f2abfca..c8bf774a 100644 --- a/engine/src/Scene/Scene.cpp +++ b/engine/src/Scene/Scene.cpp @@ -18,6 +18,7 @@ struct PendingComponentData { struct PendingGameObjectData { GameObject::ID id = GameObject::INVALID_ID; + uint64_t uuid = 0; std::string name = "GameObject"; bool active = true; GameObject::ID parentId = GameObject::INVALID_ID; @@ -70,6 +71,7 @@ void SerializeGameObjectRecursive(std::ostream& os, GameObject* gameObject) { os << "gameobject_begin\n"; os << "id=" << gameObject->GetID() << "\n"; + os << "uuid=" << gameObject->GetUUID() << "\n"; os << "name=" << EscapeString(gameObject->GetName()) << "\n"; os << "active=" << (gameObject->IsActive() ? 1 : 0) << "\n"; os << "parent=" << (gameObject->GetParent() ? gameObject->GetParent()->GetID() : GameObject::INVALID_ID) << "\n"; @@ -219,6 +221,7 @@ std::vector Scene::GetRootGameObjects() const { void Scene::Update(float deltaTime) { for (auto* go : GetRootGameObjects()) { if (go->IsActiveInHierarchy()) { + go->Start(); go->Update(deltaTime); } } @@ -285,6 +288,8 @@ void Scene::DeserializeFromString(const std::string& data) { if (key == "id") { currentObject->id = static_cast(std::stoull(value)); maxId = std::max(maxId, currentObject->id); + } else if (key == "uuid") { + currentObject->uuid = std::stoull(value); } else if (key == "name") { currentObject->name = UnescapeString(value); } else if (key == "active") { @@ -312,6 +317,9 @@ void Scene::DeserializeFromString(const std::string& data) { for (const PendingGameObjectData& pending : pendingObjects) { auto go = std::make_unique(pending.name); go->m_id = pending.id; + if (pending.uuid != 0) { + go->m_uuid = pending.uuid; + } go->m_activeSelf = pending.active; go->m_scene = this; diff --git a/engine/src/Scripting/ScriptComponent.cpp b/engine/src/Scripting/ScriptComponent.cpp new file mode 100644 index 00000000..8c76f9ce --- /dev/null +++ b/engine/src/Scripting/ScriptComponent.cpp @@ -0,0 +1,89 @@ +#include "Scripting/ScriptComponent.h" + +#include +#include + +namespace XCEngine { +namespace Scripting { + +namespace { + +uint64_t GenerateScriptComponentUUID() { + static std::random_device rd; + static std::mt19937_64 gen(rd()); + static std::uniform_int_distribution dis(1, UINT64_MAX); + return dis(gen); +} + +} // namespace + +ScriptComponent::ScriptComponent() + : m_scriptComponentUUID(GenerateScriptComponentUUID()) { +} + +void ScriptComponent::SetScriptClass(const std::string& namespaceName, const std::string& className) { + m_namespaceName = namespaceName; + m_className = className; +} + +void ScriptComponent::SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className) { + m_assemblyName = assemblyName; + m_namespaceName = namespaceName; + m_className = className; +} + +std::string ScriptComponent::GetFullClassName() const { + if (m_className.empty()) { + return std::string(); + } + + if (m_namespaceName.empty()) { + return m_className; + } + + return m_namespaceName + "." + m_className; +} + +void ScriptComponent::Serialize(std::ostream& os) const { + os << "scriptComponentUUID=" << m_scriptComponentUUID << ";"; + os << "assembly=" << EscapeScriptString(m_assemblyName) << ";"; + os << "namespace=" << EscapeScriptString(m_namespaceName) << ";"; + os << "class=" << EscapeScriptString(m_className) << ";"; + os << "fields=" << EscapeScriptString(m_fieldStorage.SerializeToString()) << ";"; +} + +void ScriptComponent::Deserialize(std::istream& is) { + std::ostringstream buffer; + buffer << is.rdbuf(); + + std::istringstream input(buffer.str()); + std::string token; + while (std::getline(input, token, ';')) { + if (token.empty()) { + continue; + } + + const size_t eqPos = token.find('='); + if (eqPos == std::string::npos) { + continue; + } + + const std::string key = token.substr(0, eqPos); + const std::string value = token.substr(eqPos + 1); + + if (key == "scriptComponentUUID") { + m_scriptComponentUUID = static_cast(std::stoull(value)); + } else if (key == "assembly") { + m_assemblyName = UnescapeScriptString(value); + } else if (key == "namespace") { + m_namespaceName = UnescapeScriptString(value); + } else if (key == "class") { + m_className = UnescapeScriptString(value); + } else if (key == "fields") { + m_fieldStorage.DeserializeFromString(UnescapeScriptString(value)); + } + } +} + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/src/Scripting/ScriptField.cpp b/engine/src/Scripting/ScriptField.cpp new file mode 100644 index 00000000..744a3b5b --- /dev/null +++ b/engine/src/Scripting/ScriptField.cpp @@ -0,0 +1,284 @@ +#include "Scripting/ScriptField.h" + +#include +#include +#include +#include +#include +#include + +namespace XCEngine { +namespace Scripting { + +namespace { + +bool IsSafeScriptChar(unsigned char ch) { + return std::isalnum(ch) != 0 || ch == '_' || ch == '-' || ch == '.' || ch == '/' || ch == ' '; +} + +int HexToInt(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'A' && ch <= 'F') { + return 10 + (ch - 'A'); + } + if (ch >= 'a' && ch <= 'f') { + return 10 + (ch - 'a'); + } + return -1; +} + +template +std::string SerializeScalar(T value) { + std::ostringstream os; + os << std::setprecision(std::numeric_limits::max_digits10) << value; + return os.str(); +} + +template +bool TryDeserializeVector(const std::string& value, TVector& outVector, int componentCount) { + std::string normalized = value; + std::replace(normalized.begin(), normalized.end(), ',', ' '); + + std::istringstream stream(normalized); + float components[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + for (int i = 0; i < componentCount; ++i) { + if (!(stream >> components[i])) { + return false; + } + } + + if constexpr (std::is_same_v) { + outVector = Math::Vector2(components[0], components[1]); + } else if constexpr (std::is_same_v) { + outVector = Math::Vector3(components[0], components[1], components[2]); + } else { + outVector = Math::Vector4(components[0], components[1], components[2], components[3]); + } + + return true; +} + +} // namespace + +std::string ScriptFieldTypeToString(ScriptFieldType type) { + switch (type) { + case ScriptFieldType::None: return "None"; + case ScriptFieldType::Float: return "Float"; + case ScriptFieldType::Double: return "Double"; + case ScriptFieldType::Bool: return "Bool"; + case ScriptFieldType::Int32: return "Int32"; + case ScriptFieldType::UInt64: return "UInt64"; + case ScriptFieldType::String: return "String"; + case ScriptFieldType::Vector2: return "Vector2"; + case ScriptFieldType::Vector3: return "Vector3"; + case ScriptFieldType::Vector4: return "Vector4"; + case ScriptFieldType::GameObject: return "GameObject"; + } + + return "None"; +} + +bool TryParseScriptFieldType(const std::string& value, ScriptFieldType& outType) { + if (value == "None") { + outType = ScriptFieldType::None; + } else if (value == "Float") { + outType = ScriptFieldType::Float; + } else if (value == "Double") { + outType = ScriptFieldType::Double; + } else if (value == "Bool") { + outType = ScriptFieldType::Bool; + } else if (value == "Int32") { + outType = ScriptFieldType::Int32; + } else if (value == "UInt64") { + outType = ScriptFieldType::UInt64; + } else if (value == "String") { + outType = ScriptFieldType::String; + } else if (value == "Vector2") { + outType = ScriptFieldType::Vector2; + } else if (value == "Vector3") { + outType = ScriptFieldType::Vector3; + } else if (value == "Vector4") { + outType = ScriptFieldType::Vector4; + } else if (value == "GameObject") { + outType = ScriptFieldType::GameObject; + } else { + return false; + } + + return true; +} + +bool IsScriptFieldValueCompatible(ScriptFieldType type, const ScriptFieldValue& value) { + switch (type) { + case ScriptFieldType::None: return std::holds_alternative(value); + case ScriptFieldType::Float: return std::holds_alternative(value); + case ScriptFieldType::Double: return std::holds_alternative(value); + case ScriptFieldType::Bool: return std::holds_alternative(value); + case ScriptFieldType::Int32: return std::holds_alternative(value); + case ScriptFieldType::UInt64: return std::holds_alternative(value); + case ScriptFieldType::String: return std::holds_alternative(value); + case ScriptFieldType::Vector2: return std::holds_alternative(value); + case ScriptFieldType::Vector3: return std::holds_alternative(value); + case ScriptFieldType::Vector4: return std::holds_alternative(value); + case ScriptFieldType::GameObject: return std::holds_alternative(value); + } + + return false; +} + +ScriptFieldValue CreateDefaultScriptFieldValue(ScriptFieldType type) { + switch (type) { + case ScriptFieldType::None: return std::monostate{}; + case ScriptFieldType::Float: return 0.0f; + case ScriptFieldType::Double: return 0.0; + case ScriptFieldType::Bool: return false; + case ScriptFieldType::Int32: return int32_t(0); + case ScriptFieldType::UInt64: return uint64_t(0); + case ScriptFieldType::String: return std::string(); + case ScriptFieldType::Vector2: return Math::Vector2::Zero(); + case ScriptFieldType::Vector3: return Math::Vector3::Zero(); + case ScriptFieldType::Vector4: return Math::Vector4::Zero(); + case ScriptFieldType::GameObject: return GameObjectReference{}; + } + + return std::monostate{}; +} + +std::string SerializeScriptFieldValue(ScriptFieldType type, const ScriptFieldValue& value) { + if (!IsScriptFieldValueCompatible(type, value)) { + return std::string(); + } + + switch (type) { + case ScriptFieldType::None: + return std::string(); + case ScriptFieldType::Float: + return SerializeScalar(std::get(value)); + case ScriptFieldType::Double: + return SerializeScalar(std::get(value)); + case ScriptFieldType::Bool: + return std::get(value) ? "1" : "0"; + case ScriptFieldType::Int32: + return std::to_string(std::get(value)); + case ScriptFieldType::UInt64: + return std::to_string(std::get(value)); + case ScriptFieldType::String: + return EscapeScriptString(std::get(value)); + case ScriptFieldType::Vector2: { + const Math::Vector2& vector = std::get(value); + return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y); + } + case ScriptFieldType::Vector3: { + const Math::Vector3& vector = std::get(value); + return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y) + "," + SerializeScalar(vector.z); + } + case ScriptFieldType::Vector4: { + const Math::Vector4& vector = std::get(value); + return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y) + "," + SerializeScalar(vector.z) + "," + SerializeScalar(vector.w); + } + case ScriptFieldType::GameObject: + return std::to_string(std::get(value).gameObjectUUID); + } + + return std::string(); +} + +bool TryDeserializeScriptFieldValue(ScriptFieldType type, const std::string& value, ScriptFieldValue& outValue) { + try { + switch (type) { + case ScriptFieldType::None: + outValue = std::monostate{}; + return true; + case ScriptFieldType::Float: + outValue = std::stof(value); + return true; + case ScriptFieldType::Double: + outValue = std::stod(value); + return true; + case ScriptFieldType::Bool: + outValue = (value == "1" || value == "true" || value == "True"); + return true; + case ScriptFieldType::Int32: + outValue = static_cast(std::stoi(value)); + return true; + case ScriptFieldType::UInt64: + outValue = static_cast(std::stoull(value)); + return true; + case ScriptFieldType::String: + outValue = UnescapeScriptString(value); + return true; + case ScriptFieldType::Vector2: { + Math::Vector2 parsedValue; + if (!TryDeserializeVector(value, parsedValue, 2)) { + return false; + } + outValue = parsedValue; + return true; + } + case ScriptFieldType::Vector3: { + Math::Vector3 parsedValue; + if (!TryDeserializeVector(value, parsedValue, 3)) { + return false; + } + outValue = parsedValue; + return true; + } + case ScriptFieldType::Vector4: { + Math::Vector4 parsedValue; + if (!TryDeserializeVector(value, parsedValue, 4)) { + return false; + } + outValue = parsedValue; + return true; + } + case ScriptFieldType::GameObject: + outValue = GameObjectReference{static_cast(std::stoull(value))}; + return true; + } + } catch (...) { + return false; + } + + return false; +} + +std::string EscapeScriptString(const std::string& value) { + std::ostringstream os; + os << std::uppercase << std::hex; + + for (unsigned char ch : value) { + if (IsSafeScriptChar(ch)) { + os << static_cast(ch); + } else { + os << '%' << std::setw(2) << std::setfill('0') << static_cast(ch); + } + } + + return os.str(); +} + +std::string UnescapeScriptString(const std::string& value) { + std::string result; + result.reserve(value.size()); + + for (size_t i = 0; i < value.size(); ++i) { + if (value[i] == '%' && i + 2 < value.size()) { + const int high = HexToInt(value[i + 1]); + const int low = HexToInt(value[i + 2]); + if (high >= 0 && low >= 0) { + result.push_back(static_cast((high << 4) | low)); + i += 2; + continue; + } + } + + result.push_back(value[i]); + } + + return result; +} + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/src/Scripting/ScriptFieldStorage.cpp b/engine/src/Scripting/ScriptFieldStorage.cpp new file mode 100644 index 00000000..b072954d --- /dev/null +++ b/engine/src/Scripting/ScriptFieldStorage.cpp @@ -0,0 +1,125 @@ +#include "Scripting/ScriptFieldStorage.h" + +#include +#include + +namespace XCEngine { +namespace Scripting { + +bool ScriptFieldStorage::SetFieldValue(const std::string& fieldName, ScriptFieldType type, const ScriptFieldValue& value) { + if (fieldName.empty() || !IsScriptFieldValueCompatible(type, value)) { + return false; + } + + m_fields[fieldName] = StoredScriptField{type, value}; + return true; +} + +const StoredScriptField* ScriptFieldStorage::FindField(const std::string& fieldName) const { + const auto it = m_fields.find(fieldName); + return it != m_fields.end() ? &it->second : nullptr; +} + +StoredScriptField* ScriptFieldStorage::FindField(const std::string& fieldName) { + const auto it = m_fields.find(fieldName); + return it != m_fields.end() ? &it->second : nullptr; +} + +bool ScriptFieldStorage::Contains(const std::string& fieldName) const { + return m_fields.find(fieldName) != m_fields.end(); +} + +bool ScriptFieldStorage::Remove(const std::string& fieldName) { + return m_fields.erase(fieldName) > 0; +} + +void ScriptFieldStorage::Clear() { + m_fields.clear(); +} + +std::vector ScriptFieldStorage::GetFieldNames() const { + std::vector names; + names.reserve(m_fields.size()); + for (const auto& entry : m_fields) { + names.push_back(entry.first); + } + + std::sort(names.begin(), names.end()); + return names; +} + +std::string ScriptFieldStorage::SerializeToString() const { + std::ostringstream os; + const std::vector fieldNames = GetFieldNames(); + + for (const std::string& fieldName : fieldNames) { + const StoredScriptField* field = FindField(fieldName); + if (!field) { + continue; + } + + os << "field=" + << EscapeScriptString(fieldName) + << "|" + << ScriptFieldTypeToString(field->type) + << "|" + << SerializeScriptFieldValue(field->type, field->value) + << "\n"; + } + + return os.str(); +} + +void ScriptFieldStorage::DeserializeFromString(const std::string& data) { + m_fields.clear(); + + std::istringstream input(data); + std::string line; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + + static constexpr const char* fieldPrefix = "field="; + if (line.rfind(fieldPrefix, 0) != 0) { + continue; + } + + const std::string payload = line.substr(6); + const size_t firstSeparator = payload.find('|'); + const size_t secondSeparator = payload.find('|', firstSeparator == std::string::npos ? firstSeparator : firstSeparator + 1); + + if (firstSeparator == std::string::npos || secondSeparator == std::string::npos) { + continue; + } + + const std::string fieldName = UnescapeScriptString(payload.substr(0, firstSeparator)); + const std::string typeName = payload.substr(firstSeparator + 1, secondSeparator - firstSeparator - 1); + const std::string serializedValue = payload.substr(secondSeparator + 1); + + ScriptFieldType type = ScriptFieldType::None; + if (!TryParseScriptFieldType(typeName, type)) { + continue; + } + + ScriptFieldValue value = CreateDefaultScriptFieldValue(type); + if (!TryDeserializeScriptFieldValue(type, serializedValue, value)) { + continue; + } + + m_fields[fieldName] = StoredScriptField{type, value}; + } +} + +void ScriptFieldStorage::Serialize(std::ostream& os) const { + os << SerializeToString(); +} + +void ScriptFieldStorage::Deserialize(std::istream& is) { + std::ostringstream buffer; + buffer << is.rdbuf(); + DeserializeFromString(buffer.str()); +} + +} // namespace Scripting +} // namespace XCEngine diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f9e42cec..9603d16f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,8 @@ add_subdirectory(threading) add_subdirectory(debug) add_subdirectory(components) add_subdirectory(scene) +add_subdirectory(scripting) +add_subdirectory(Rendering) add_subdirectory(rhi) add_subdirectory(resources) add_subdirectory(input) diff --git a/tests/Components/test_component.cpp b/tests/Components/test_component.cpp index 6952ecc3..816116fb 100644 --- a/tests/Components/test_component.cpp +++ b/tests/Components/test_component.cpp @@ -21,12 +21,14 @@ public: bool m_onEnableCalled = false; bool m_onDisableCalled = false; bool m_onDestroyCalled = false; + int m_onEnableCount = 0; + int m_onDisableCount = 0; 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 OnEnable() override { m_onEnableCalled = true; ++m_onEnableCount; } + void OnDisable() override { m_onDisableCalled = true; ++m_onDisableCount; } void OnDestroy() override { m_onDestroyCalled = true; } private: @@ -107,4 +109,24 @@ TEST(Component_Test, SetEnabled_NoCallback_WhenStateUnchanged) { EXPECT_FALSE(comp.m_onDisableCalled); } -} // namespace \ No newline at end of file +TEST(Component_Test, SetEnabled_AttachedInactiveGameObject_DelaysOnEnableUntilActivation) { + GameObject go; + TestComponent* comp = go.AddComponent(); + + go.SetActive(false); + comp->m_onEnableCalled = false; + comp->m_onDisableCalled = false; + comp->m_onEnableCount = 0; + comp->m_onDisableCount = 0; + + comp->SetEnabled(false); + EXPECT_EQ(comp->m_onDisableCount, 0); + + comp->SetEnabled(true); + EXPECT_EQ(comp->m_onEnableCount, 0); + + go.SetActive(true); + EXPECT_EQ(comp->m_onEnableCount, 1); +} + +} // namespace diff --git a/tests/Components/test_game_object.cpp b/tests/Components/test_game_object.cpp index 1665ba26..60d6c33f 100644 --- a/tests/Components/test_game_object.cpp +++ b/tests/Components/test_game_object.cpp @@ -21,10 +21,21 @@ public: bool m_awakeCalled = false; bool m_startCalled = false; bool m_updateCalled = false; + bool m_onEnableCalled = false; + bool m_onDisableCalled = false; + bool m_onDestroyCalled = false; + int m_startCount = 0; + int m_updateCount = 0; + int m_onEnableCount = 0; + int m_onDisableCount = 0; + int m_onDestroyCount = 0; void Awake() override { m_awakeCalled = true; } - void Start() override { m_startCalled = true; } - void Update(float deltaTime) override { m_updateCalled = true; } + void Start() override { m_startCalled = true; ++m_startCount; } + void Update(float deltaTime) override { m_updateCalled = true; ++m_updateCount; } + void OnEnable() override { m_onEnableCalled = true; ++m_onEnableCount; } + void OnDisable() override { m_onDisableCalled = true; ++m_onDisableCount; } + void OnDestroy() override { m_onDestroyCalled = true; ++m_onDestroyCount; } private: std::string m_customName; @@ -45,7 +56,6 @@ TEST(GameObject_Test, DefaultConstructor_DefaultValues) { EXPECT_EQ(go.GetName(), "GameObject"); EXPECT_TRUE(go.IsActive()); EXPECT_NE(go.GetID(), GameObject::INVALID_ID); - EXPECT_EQ(go.GetID(), 1u); } TEST(GameObject_Test, NamedConstructor) { @@ -203,6 +213,34 @@ TEST(GameObject_Test, SetActive_False) { EXPECT_FALSE(go.IsActive()); } +TEST(GameObject_Test, SetActive_PropagatesEnableDisableToChildren) { + GameObject parent("Parent"); + GameObject child("Child"); + TestComponent* comp = child.AddComponent(); + + child.SetParent(&parent); + + parent.SetActive(false); + EXPECT_EQ(comp->m_onDisableCount, 1); + + parent.SetActive(true); + EXPECT_EQ(comp->m_onEnableCount, 1); +} + +TEST(GameObject_Test, SetParent_PropagatesActiveHierarchyChanges) { + GameObject activeParent("ActiveParent"); + GameObject inactiveParent("InactiveParent"); + GameObject child("Child"); + TestComponent* comp = child.AddComponent(); + + inactiveParent.SetActive(false); + child.SetParent(&inactiveParent); + EXPECT_EQ(comp->m_onDisableCount, 1); + + child.SetParent(&activeParent); + EXPECT_EQ(comp->m_onEnableCount, 1); +} + TEST(GameObject_Test, IsActiveInHierarchy_True) { GameObject parent("Parent"); GameObject child("Child"); @@ -238,6 +276,17 @@ TEST(GameObject_Test, Lifecycle_Start) { go.Start(); EXPECT_TRUE(comp->m_startCalled); + EXPECT_EQ(comp->m_startCount, 1); +} + +TEST(GameObject_Test, Lifecycle_Start_IsOnlyCalledOnce) { + GameObject go; + TestComponent* comp = go.AddComponent(); + + go.Start(); + go.Start(); + + EXPECT_EQ(comp->m_startCount, 1); } TEST(GameObject_Test, Lifecycle_Update) { @@ -247,6 +296,17 @@ TEST(GameObject_Test, Lifecycle_Update) { go.Update(0.016f); EXPECT_TRUE(comp->m_updateCalled); + EXPECT_EQ(comp->m_updateCount, 1); +} + +TEST(GameObject_Test, Destroy_CallsOnDestroyOnce_WhenStandalone) { + GameObject go; + TestComponent* comp = go.AddComponent(); + + go.Destroy(); + + EXPECT_TRUE(comp->m_onDestroyCalled); + EXPECT_EQ(comp->m_onDestroyCount, 1); } TEST_F(GameObjectTest, Find_Exists) { diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp index 22689fca..ff0ee75e 100644 --- a/tests/Scene/test_scene.cpp +++ b/tests/Scene/test_scene.cpp @@ -27,9 +27,22 @@ public: bool m_awakeCalled = false; bool m_updateCalled = false; + bool m_onDestroyCalled = false; + int m_startCount = 0; + int m_updateCount = 0; + int m_onDestroyCount = 0; + int* m_externalOnDestroyCount = nullptr; void Awake() override { m_awakeCalled = true; } - void Update(float deltaTime) override { m_updateCalled = true; } + void Start() override { ++m_startCount; } + void Update(float deltaTime) override { m_updateCalled = true; ++m_updateCount; } + void OnDestroy() override { + m_onDestroyCalled = true; + ++m_onDestroyCount; + if (m_externalOnDestroyCount) { + ++(*m_externalOnDestroyCount); + } + } private: std::string m_customName; @@ -111,6 +124,17 @@ TEST_F(SceneTest, DestroyGameObject_WithChildren) { EXPECT_EQ(testScene->Find("Child"), nullptr); } +TEST_F(SceneTest, DestroyGameObject_CallsOnDestroyOnce) { + GameObject* go = testScene->CreateGameObject("DestroyMe"); + TestComponent* comp = go->AddComponent(); + int destroyCount = 0; + comp->m_externalOnDestroyCount = &destroyCount; + + testScene->DestroyGameObject(go); + + EXPECT_EQ(destroyCount, 1); +} + TEST_F(SceneTest, Find_Exists) { testScene->CreateGameObject("FindMe"); @@ -163,6 +187,28 @@ TEST_F(SceneTest, Update_SkipsInactiveObjects) { EXPECT_FALSE(comp->m_updateCalled); } +TEST_F(SceneTest, Update_RecursivelyUpdatesChildObjects) { + GameObject* parent = testScene->CreateGameObject("Parent"); + GameObject* child = testScene->CreateGameObject("Child", parent); + TestComponent* comp = child->AddComponent(); + + testScene->Update(0.016f); + + EXPECT_TRUE(comp->m_updateCalled); + EXPECT_EQ(comp->m_updateCount, 1); +} + +TEST_F(SceneTest, Update_CallsStartOnlyOnceBeforeUpdate) { + GameObject* go = testScene->CreateGameObject("TestObject"); + TestComponent* comp = go->AddComponent(); + + testScene->Update(0.016f); + testScene->Update(0.016f); + + EXPECT_EQ(comp->m_startCount, 1); + EXPECT_EQ(comp->m_updateCount, 2); +} + TEST(Scene_Test, IsActive_DefaultTrue) { Scene s; @@ -415,6 +461,27 @@ TEST_F(SceneTest, SerializeToString_And_DeserializeFromString_PreservesHierarchy EXPECT_TRUE(loadedCamera->IsPrimary()); } +TEST_F(SceneTest, SerializeToString_And_DeserializeFromString_PreservesUUIDs) { + GameObject* parent = testScene->CreateGameObject("Parent"); + GameObject* child = testScene->CreateGameObject("Child", parent); + + const uint64_t parentUUID = parent->GetUUID(); + const uint64_t childUUID = child->GetUUID(); + + const std::string serialized = testScene->SerializeToString(); + + Scene loadedScene; + loadedScene.DeserializeFromString(serialized); + + GameObject* loadedParent = loadedScene.Find("Parent"); + GameObject* loadedChild = loadedScene.Find("Child"); + ASSERT_NE(loadedParent, nullptr); + ASSERT_NE(loadedChild, nullptr); + + EXPECT_EQ(loadedParent->GetUUID(), parentUUID); + EXPECT_EQ(loadedChild->GetUUID(), childUUID); +} + TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) { GameObject* parent = testScene->CreateGameObject("Parent"); GameObject* child = testScene->CreateGameObject("Child", parent); diff --git a/tests/scripting/CMakeLists.txt b/tests/scripting/CMakeLists.txt new file mode 100644 index 00000000..966d559e --- /dev/null +++ b/tests/scripting/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.15) + +project(XCEngine_ScriptingTests) + +set(SCRIPTING_TEST_SOURCES + test_script_field_storage.cpp + test_script_component.cpp +) + +add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES}) + +if(MSVC) + set_target_properties(scripting_tests PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + ) +endif() + +target_link_libraries(scripting_tests PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main +) + +target_include_directories(scripting_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include +) + +include(GoogleTest) +gtest_discover_tests(scripting_tests) diff --git a/tests/scripting/test_script_component.cpp b/tests/scripting/test_script_component.cpp new file mode 100644 index 00000000..61907594 --- /dev/null +++ b/tests/scripting/test_script_component.cpp @@ -0,0 +1,143 @@ +#include + +#include +#include +#include +#include + +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Scripting; + +namespace { + +TEST(ScriptComponent_Test, DefaultsAreInitializedForNativeDataLayer) { + ScriptComponent component; + + EXPECT_EQ(component.GetName(), "ScriptComponent"); + EXPECT_NE(component.GetScriptComponentUUID(), 0u); + EXPECT_EQ(component.GetAssemblyName(), "GameScripts"); + EXPECT_TRUE(component.GetNamespaceName().empty()); + EXPECT_TRUE(component.GetClassName().empty()); + EXPECT_FALSE(component.HasScriptClass()); + EXPECT_TRUE(component.GetFullClassName().empty()); + EXPECT_EQ(component.GetFieldStorage().GetFieldCount(), 0u); +} + +TEST(ScriptComponent_Test, FullClassNameComposesNamespaceAndClassName) { + ScriptComponent component; + + component.SetScriptClass("Gameplay.Characters", "PlayerController"); + EXPECT_EQ(component.GetFullClassName(), "Gameplay.Characters.PlayerController"); + + component.SetScriptClass("", "Mover"); + EXPECT_EQ(component.GetFullClassName(), "Mover"); +} + +TEST(ScriptComponent_Test, SerializeRoundTripPreservesMetadataAndFields) { + ScriptComponent original; + original.SetScriptClass("GameScripts.Runtime", "Gameplay.Characters", "PlayerController"); + ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("DisplayName", std::string("Hero=One;Ready|100%"))); + ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("MoveSpeed", 6.25f)); + ASSERT_TRUE(original.GetFieldStorage().SetFieldValue("Target", GameObjectReference{9988})); + + std::ostringstream stream; + original.Serialize(stream); + + ScriptComponent restored; + std::istringstream input(stream.str()); + restored.Deserialize(input); + + std::string displayName; + float moveSpeed = 0.0f; + GameObjectReference target; + + EXPECT_EQ(restored.GetScriptComponentUUID(), original.GetScriptComponentUUID()); + EXPECT_EQ(restored.GetAssemblyName(), "GameScripts.Runtime"); + EXPECT_EQ(restored.GetNamespaceName(), "Gameplay.Characters"); + EXPECT_EQ(restored.GetClassName(), "PlayerController"); + EXPECT_EQ(restored.GetFullClassName(), "Gameplay.Characters.PlayerController"); + EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("DisplayName", displayName)); + EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("MoveSpeed", moveSpeed)); + EXPECT_TRUE(restored.GetFieldStorage().TryGetFieldValue("Target", target)); + EXPECT_EQ(displayName, "Hero=One;Ready|100%"); + EXPECT_FLOAT_EQ(moveSpeed, 6.25f); + EXPECT_EQ(target, GameObjectReference{9988}); +} + +TEST(ScriptComponent_Test, ComponentFactoryRegistryCreatesScriptComponents) { + GameObject gameObject("ScriptHost"); + auto& registry = ComponentFactoryRegistry::Get(); + + EXPECT_TRUE(registry.IsRegistered("ScriptComponent")); + + ScriptComponent* created = dynamic_cast(registry.CreateComponent(&gameObject, "ScriptComponent")); + ASSERT_NE(created, nullptr); + EXPECT_EQ(gameObject.GetComponents().size(), 1u); +} + +TEST(ScriptComponent_Test, GameObjectCanOwnMultipleScriptComponents) { + GameObject gameObject("ScriptHost"); + + ScriptComponent* first = gameObject.AddComponent(); + ScriptComponent* second = gameObject.AddComponent(); + + ASSERT_NE(first, nullptr); + ASSERT_NE(second, nullptr); + EXPECT_NE(first, second); + EXPECT_EQ(gameObject.GetComponents().size(), 2u); +} + +TEST(ScriptComponent_Test, SceneRoundTripPreservesMultipleScriptComponentsAndFields) { + Scene scene("Script Scene"); + GameObject* host = scene.CreateGameObject("Host"); + GameObject* targetObject = scene.CreateGameObject("Target"); + + ScriptComponent* movement = host->AddComponent(); + movement->SetScriptClass("GameScripts", "Gameplay", "MovementController"); + ASSERT_TRUE(movement->GetFieldStorage().SetFieldValue("Speed", 4.25f)); + ASSERT_TRUE(movement->GetFieldStorage().SetFieldValue("Target", GameObjectReference{targetObject->GetUUID()})); + + ScriptComponent* inventory = host->AddComponent(); + inventory->SetScriptClass("GameScripts", "Gameplay.Inventory", "InventoryWatcher"); + ASSERT_TRUE(inventory->GetFieldStorage().SetFieldValue("Label", std::string("Player|One;Ready=1"))); + ASSERT_TRUE(inventory->GetFieldStorage().SetFieldValue("Slots", int32_t(24))); + + const uint64_t movementUUID = movement->GetScriptComponentUUID(); + const uint64_t inventoryUUID = inventory->GetScriptComponentUUID(); + + Scene loadedScene; + loadedScene.DeserializeFromString(scene.SerializeToString()); + + GameObject* loadedHost = loadedScene.Find("Host"); + GameObject* loadedTarget = loadedScene.Find("Target"); + ASSERT_NE(loadedHost, nullptr); + ASSERT_NE(loadedTarget, nullptr); + + std::vector loadedScripts = loadedHost->GetComponents(); + ASSERT_EQ(loadedScripts.size(), 2u); + + float speed = 0.0f; + GameObjectReference loadedReference; + std::string label; + int32_t slots = 0; + + EXPECT_EQ(loadedScripts[0]->GetScriptComponentUUID(), movementUUID); + EXPECT_EQ(loadedScripts[0]->GetAssemblyName(), "GameScripts"); + EXPECT_EQ(loadedScripts[0]->GetFullClassName(), "Gameplay.MovementController"); + EXPECT_TRUE(loadedScripts[0]->GetFieldStorage().TryGetFieldValue("Speed", speed)); + EXPECT_TRUE(loadedScripts[0]->GetFieldStorage().TryGetFieldValue("Target", loadedReference)); + EXPECT_FLOAT_EQ(speed, 4.25f); + EXPECT_EQ(loadedReference, GameObjectReference{loadedTarget->GetUUID()}); + + EXPECT_EQ(loadedScripts[1]->GetScriptComponentUUID(), inventoryUUID); + EXPECT_EQ(loadedScripts[1]->GetAssemblyName(), "GameScripts"); + EXPECT_EQ(loadedScripts[1]->GetFullClassName(), "Gameplay.Inventory.InventoryWatcher"); + EXPECT_TRUE(loadedScripts[1]->GetFieldStorage().TryGetFieldValue("Label", label)); + EXPECT_TRUE(loadedScripts[1]->GetFieldStorage().TryGetFieldValue("Slots", slots)); + EXPECT_EQ(label, "Player|One;Ready=1"); + EXPECT_EQ(slots, 24); +} + +} // namespace diff --git a/tests/scripting/test_script_field_storage.cpp b/tests/scripting/test_script_field_storage.cpp new file mode 100644 index 00000000..ec0b145c --- /dev/null +++ b/tests/scripting/test_script_field_storage.cpp @@ -0,0 +1,92 @@ +#include + +#include + +using namespace XCEngine::Math; +using namespace XCEngine::Scripting; + +namespace { + +TEST(ScriptFieldStorage_Test, StoresRetrievesAndRemovesSupportedValues) { + ScriptFieldStorage storage; + + EXPECT_TRUE(storage.SetFieldValue("Speed", 3.5f)); + EXPECT_TRUE(storage.SetFieldValue("Accuracy", 0.875)); + EXPECT_TRUE(storage.SetFieldValue("IsAlive", true)); + EXPECT_TRUE(storage.SetFieldValue("Health", int32_t(125))); + EXPECT_TRUE(storage.SetFieldValue("Score", uint64_t(9001))); + EXPECT_TRUE(storage.SetFieldValue("DisplayName", std::string("Player One"))); + EXPECT_TRUE(storage.SetFieldValue("Offset2D", Vector2(1.0f, 2.0f))); + EXPECT_TRUE(storage.SetFieldValue("Velocity", Vector3(3.0f, 4.0f, 5.0f))); + EXPECT_TRUE(storage.SetFieldValue("Tint", Vector4(0.1f, 0.2f, 0.3f, 1.0f))); + EXPECT_TRUE(storage.SetFieldValue("Target", GameObjectReference{42})); + + float speed = 0.0f; + double accuracy = 0.0; + bool isAlive = false; + int32_t health = 0; + uint64_t score = 0; + std::string displayName; + Vector2 offset2D; + Vector3 velocity; + Vector4 tint; + GameObjectReference target; + + EXPECT_EQ(storage.GetFieldCount(), 10u); + EXPECT_TRUE(storage.Contains("Velocity")); + EXPECT_TRUE(storage.TryGetFieldValue("Speed", speed)); + EXPECT_TRUE(storage.TryGetFieldValue("Accuracy", accuracy)); + EXPECT_TRUE(storage.TryGetFieldValue("IsAlive", isAlive)); + EXPECT_TRUE(storage.TryGetFieldValue("Health", health)); + EXPECT_TRUE(storage.TryGetFieldValue("Score", score)); + EXPECT_TRUE(storage.TryGetFieldValue("DisplayName", displayName)); + EXPECT_TRUE(storage.TryGetFieldValue("Offset2D", offset2D)); + EXPECT_TRUE(storage.TryGetFieldValue("Velocity", velocity)); + EXPECT_TRUE(storage.TryGetFieldValue("Tint", tint)); + EXPECT_TRUE(storage.TryGetFieldValue("Target", target)); + + EXPECT_FLOAT_EQ(speed, 3.5f); + EXPECT_DOUBLE_EQ(accuracy, 0.875); + EXPECT_TRUE(isAlive); + EXPECT_EQ(health, 125); + EXPECT_EQ(score, 9001u); + EXPECT_EQ(displayName, "Player One"); + EXPECT_EQ(offset2D, Vector2(1.0f, 2.0f)); + EXPECT_EQ(velocity, Vector3(3.0f, 4.0f, 5.0f)); + EXPECT_EQ(tint, Vector4(0.1f, 0.2f, 0.3f, 1.0f)); + EXPECT_EQ(target, GameObjectReference{42}); + + EXPECT_TRUE(storage.Remove("IsAlive")); + EXPECT_FALSE(storage.Contains("IsAlive")); + EXPECT_EQ(storage.GetFieldCount(), 9u); +} + +TEST(ScriptFieldStorage_Test, SerializeRoundTripPreservesFieldValues) { + ScriptFieldStorage original; + ASSERT_TRUE(original.SetFieldValue("Message", std::string("Ready;Set=Go|100%\nLine2"))); + ASSERT_TRUE(original.SetFieldValue("Scale", Vector3(2.0f, 3.0f, 4.0f))); + ASSERT_TRUE(original.SetFieldValue("Owner", GameObjectReference{778899})); + ASSERT_TRUE(original.SetFieldValue("Counter", int32_t(-17))); + + const std::string serialized = original.SerializeToString(); + + ScriptFieldStorage restored; + restored.DeserializeFromString(serialized); + + std::string message; + Vector3 scale; + GameObjectReference owner; + int32_t counter = 0; + + EXPECT_TRUE(restored.TryGetFieldValue("Message", message)); + EXPECT_TRUE(restored.TryGetFieldValue("Scale", scale)); + EXPECT_TRUE(restored.TryGetFieldValue("Owner", owner)); + EXPECT_TRUE(restored.TryGetFieldValue("Counter", counter)); + + EXPECT_EQ(message, "Ready;Set=Go|100%\nLine2"); + EXPECT_EQ(scale, Vector3(2.0f, 3.0f, 4.0f)); + EXPECT_EQ(owner, GameObjectReference{778899}); + EXPECT_EQ(counter, -17); +} + +} // namespace