Prepare script lifecycle and data layer
This commit is contained in:
@@ -180,6 +180,7 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/OpenGL/OpenGLDescriptorSet.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/OpenGL/OpenGLDescriptorSet.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/OpenGL/OpenGLPipelineLayout.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/GLAD/src/glad.c
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/third_party/stb/stb_image.cpp
|
||||||
|
|
||||||
# RHI Factory
|
# RHI Factory
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/RHI/RHIFactory.cpp
|
${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/AudioClip.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.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
|
# Components
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h
|
${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/TransformComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/GameObject.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/CameraComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/LightComponent.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/AudioSourceComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioListenerComponent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioListenerComponent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/ComponentFactoryRegistry.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/GameObject.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/CameraComponent.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/CameraComponent.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/LightComponent.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/AudioSourceComponent.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioListenerComponent.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioListenerComponent.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/ComponentFactoryRegistry.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
|
# Scene
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h
|
||||||
|
|||||||
@@ -34,16 +34,7 @@ public:
|
|||||||
Scene* GetScene() const;
|
Scene* GetScene() const;
|
||||||
|
|
||||||
bool IsEnabled() const { return m_enabled; }
|
bool IsEnabled() const { return m_enabled; }
|
||||||
void SetEnabled(bool enabled) {
|
void SetEnabled(bool enabled);
|
||||||
if (m_enabled != enabled) {
|
|
||||||
m_enabled = enabled;
|
|
||||||
if (m_enabled) {
|
|
||||||
OnEnable();
|
|
||||||
} else {
|
|
||||||
OnDisable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Component() = default;
|
Component() = default;
|
||||||
|
|||||||
@@ -208,10 +208,15 @@ public:
|
|||||||
void Deserialize(std::istream& is);
|
void Deserialize(std::istream& is);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void NotifyComponentsBecameActive();
|
||||||
|
void NotifyComponentsBecameInactive();
|
||||||
|
void PropagateActiveInHierarchyChange(bool oldParentActiveInHierarchy, bool newParentActiveInHierarchy);
|
||||||
|
|
||||||
ID m_id = INVALID_ID;
|
ID m_id = INVALID_ID;
|
||||||
uint64_t m_uuid = 0;
|
uint64_t m_uuid = 0;
|
||||||
std::string m_name;
|
std::string m_name;
|
||||||
bool m_activeSelf = true;
|
bool m_activeSelf = true;
|
||||||
|
bool m_started = false;
|
||||||
|
|
||||||
GameObject* m_parent = nullptr;
|
GameObject* m_parent = nullptr;
|
||||||
std::vector<GameObject*> m_children;
|
std::vector<GameObject*> m_children;
|
||||||
|
|||||||
51
engine/include/XCEngine/Scripting/ScriptComponent.h
Normal file
51
engine/include/XCEngine/Scripting/ScriptComponent.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Components/Component.h>
|
||||||
|
#include <XCEngine/Scripting/ScriptFieldStorage.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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
|
||||||
65
engine/include/XCEngine/Scripting/ScriptField.h
Normal file
65
engine/include/XCEngine/Scripting/ScriptField.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Math/Vector2.h>
|
||||||
|
#include <XCEngine/Core/Math/Vector3.h>
|
||||||
|
#include <XCEngine/Core/Math/Vector4.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
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
|
||||||
124
engine/include/XCEngine/Scripting/ScriptFieldStorage.h
Normal file
124
engine/include/XCEngine/Scripting/ScriptFieldStorage.h
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Scripting/ScriptField.h>
|
||||||
|
|
||||||
|
#include <istream>
|
||||||
|
#include <ostream>
|
||||||
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Scripting {
|
||||||
|
|
||||||
|
struct StoredScriptField {
|
||||||
|
ScriptFieldType type = ScriptFieldType::None;
|
||||||
|
ScriptFieldValue value = std::monostate{};
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct ScriptFieldTypeResolver;
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<float> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::Float;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<double> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::Double;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<bool> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::Bool;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<int32_t> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::Int32;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<uint64_t> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::UInt64;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<std::string> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::String;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<Math::Vector2> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::Vector2;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<Math::Vector3> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::Vector3;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<Math::Vector4> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::Vector4;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct ScriptFieldTypeResolver<GameObjectReference> {
|
||||||
|
static constexpr ScriptFieldType value = ScriptFieldType::GameObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ScriptFieldStorage {
|
||||||
|
public:
|
||||||
|
template<typename T>
|
||||||
|
bool SetFieldValue(const std::string& fieldName, const T& value) {
|
||||||
|
using ValueType = std::decay_t<T>;
|
||||||
|
return SetFieldValue(fieldName, ScriptFieldTypeResolver<ValueType>::value, ScriptFieldValue(ValueType(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetFieldValue(const std::string& fieldName, const char* value) {
|
||||||
|
return SetFieldValue<std::string>(fieldName, value ? std::string(value) : std::string());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetFieldValue(const std::string& fieldName, ScriptFieldType type, const ScriptFieldValue& value);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
bool TryGetFieldValue(const std::string& fieldName, T& outValue) const {
|
||||||
|
const StoredScriptField* field = FindField(fieldName);
|
||||||
|
if (!field) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* storedValue = std::get_if<std::decay_t<T>>(&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<std::string> 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<std::string, StoredScriptField> m_fields;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Scripting
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -8,5 +8,35 @@ TransformComponent& Component::transform() const {
|
|||||||
return *m_gameObject->GetTransform();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
#include "Components/CameraComponent.h"
|
#include "Components/CameraComponent.h"
|
||||||
#include "Components/GameObject.h"
|
#include "Components/GameObject.h"
|
||||||
#include "Components/LightComponent.h"
|
#include "Components/LightComponent.h"
|
||||||
|
#include "Components/MeshFilterComponent.h"
|
||||||
|
#include "Components/MeshRendererComponent.h"
|
||||||
|
#include "Scripting/ScriptComponent.h"
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
namespace Components {
|
namespace Components {
|
||||||
@@ -28,6 +31,9 @@ ComponentFactoryRegistry::ComponentFactoryRegistry() {
|
|||||||
RegisterFactory("Light", &CreateBuiltInComponent<LightComponent>);
|
RegisterFactory("Light", &CreateBuiltInComponent<LightComponent>);
|
||||||
RegisterFactory("AudioSource", &CreateBuiltInComponent<AudioSourceComponent>);
|
RegisterFactory("AudioSource", &CreateBuiltInComponent<AudioSourceComponent>);
|
||||||
RegisterFactory("AudioListener", &CreateBuiltInComponent<AudioListenerComponent>);
|
RegisterFactory("AudioListener", &CreateBuiltInComponent<AudioListenerComponent>);
|
||||||
|
RegisterFactory("MeshFilter", &CreateBuiltInComponent<MeshFilterComponent>);
|
||||||
|
RegisterFactory("MeshRenderer", &CreateBuiltInComponent<MeshRendererComponent>);
|
||||||
|
RegisterFactory("ScriptComponent", &CreateBuiltInComponent<XCEngine::Scripting::ScriptComponent>);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComponentFactoryRegistry::RegisterFactory(const std::string& typeName, CreateComponentFn createFn) {
|
void ComponentFactoryRegistry::RegisterFactory(const std::string& typeName, CreateComponentFn createFn) {
|
||||||
|
|||||||
@@ -43,6 +43,41 @@ std::unordered_map<GameObject::ID, GameObject*>& GameObject::GetGlobalRegistry()
|
|||||||
return registry;
|
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) {
|
void GameObject::SetParent(GameObject* parent) {
|
||||||
SetParent(parent, true);
|
SetParent(parent, true);
|
||||||
}
|
}
|
||||||
@@ -52,6 +87,9 @@ void GameObject::SetParent(GameObject* parent, bool worldPositionStays) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool oldParentActiveInHierarchy = m_parent ? m_parent->IsActiveInHierarchy() : true;
|
||||||
|
const bool wasActiveInHierarchy = oldParentActiveInHierarchy && m_activeSelf;
|
||||||
|
|
||||||
if (m_parent) {
|
if (m_parent) {
|
||||||
auto& siblings = m_parent->m_children;
|
auto& siblings = m_parent->m_children;
|
||||||
siblings.erase(std::remove(siblings.begin(), siblings.end(), this), siblings.end());
|
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);
|
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 {
|
GameObject* GameObject::GetChild(size_t index) const {
|
||||||
@@ -101,9 +154,26 @@ void GameObject::DetachFromParent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameObject::SetActive(bool active) {
|
void GameObject::SetActive(bool active) {
|
||||||
if (m_activeSelf != active) {
|
if (m_activeSelf == active) {
|
||||||
m_activeSelf = active;
|
return;
|
||||||
if (m_parent == nullptr || m_parent->IsActiveInHierarchy()) {
|
}
|
||||||
|
|
||||||
|
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() {
|
void GameObject::Start() {
|
||||||
for (auto& comp : m_components) {
|
if (!IsActiveInHierarchy()) {
|
||||||
if (comp->IsEnabled()) {
|
return;
|
||||||
comp->Start();
|
}
|
||||||
|
|
||||||
|
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) {
|
void GameObject::Update(float deltaTime) {
|
||||||
|
if (!IsActiveInHierarchy()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (auto& comp : m_components) {
|
for (auto& comp : m_components) {
|
||||||
if (comp->IsEnabled()) {
|
if (comp->IsEnabled()) {
|
||||||
comp->Update(deltaTime);
|
comp->Update(deltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto* child : m_children) {
|
||||||
|
if (child) {
|
||||||
|
child->Update(deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameObject::FixedUpdate() {
|
void GameObject::FixedUpdate() {
|
||||||
|
if (!IsActiveInHierarchy()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (auto& comp : m_components) {
|
for (auto& comp : m_components) {
|
||||||
if (comp->IsEnabled()) {
|
if (comp->IsEnabled()) {
|
||||||
comp->FixedUpdate();
|
comp->FixedUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto* child : m_children) {
|
||||||
|
if (child) {
|
||||||
|
child->FixedUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameObject::LateUpdate(float deltaTime) {
|
void GameObject::LateUpdate(float deltaTime) {
|
||||||
|
if (!IsActiveInHierarchy()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (auto& comp : m_components) {
|
for (auto& comp : m_components) {
|
||||||
if (comp->IsEnabled()) {
|
if (comp->IsEnabled()) {
|
||||||
comp->LateUpdate(deltaTime);
|
comp->LateUpdate(deltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto* child : m_children) {
|
||||||
|
if (child) {
|
||||||
|
child->LateUpdate(deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameObject::OnDestroy() {
|
void GameObject::OnDestroy() {
|
||||||
@@ -193,9 +306,10 @@ void GameObject::OnDestroy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameObject::Destroy() {
|
void GameObject::Destroy() {
|
||||||
OnDestroy();
|
|
||||||
if (m_scene) {
|
if (m_scene) {
|
||||||
m_scene->DestroyGameObject(this);
|
m_scene->DestroyGameObject(this);
|
||||||
|
} else {
|
||||||
|
OnDestroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +317,7 @@ void GameObject::Serialize(std::ostream& os) const {
|
|||||||
os << "name=" << m_name << ";";
|
os << "name=" << m_name << ";";
|
||||||
os << "active=" << (m_activeSelf ? "1" : "0") << ";";
|
os << "active=" << (m_activeSelf ? "1" : "0") << ";";
|
||||||
os << "id=" << m_id << ";";
|
os << "id=" << m_id << ";";
|
||||||
|
os << "uuid=" << m_uuid << ";";
|
||||||
os << "transform=";
|
os << "transform=";
|
||||||
m_transform->Serialize(os);
|
m_transform->Serialize(os);
|
||||||
os << ";";
|
os << ";";
|
||||||
@@ -233,6 +348,9 @@ void GameObject::Deserialize(std::istream& is) {
|
|||||||
} else if (strcmp(key, "id") == 0) {
|
} else if (strcmp(key, "id") == 0) {
|
||||||
is >> m_id;
|
is >> m_id;
|
||||||
if (is.peek() == ';') is.get();
|
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) {
|
} else if (strcmp(key, "transform") == 0) {
|
||||||
m_transform->Deserialize(is);
|
m_transform->Deserialize(is);
|
||||||
if (is.peek() == ';') is.get();
|
if (is.peek() == ';') is.get();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct PendingComponentData {
|
|||||||
|
|
||||||
struct PendingGameObjectData {
|
struct PendingGameObjectData {
|
||||||
GameObject::ID id = GameObject::INVALID_ID;
|
GameObject::ID id = GameObject::INVALID_ID;
|
||||||
|
uint64_t uuid = 0;
|
||||||
std::string name = "GameObject";
|
std::string name = "GameObject";
|
||||||
bool active = true;
|
bool active = true;
|
||||||
GameObject::ID parentId = GameObject::INVALID_ID;
|
GameObject::ID parentId = GameObject::INVALID_ID;
|
||||||
@@ -70,6 +71,7 @@ void SerializeGameObjectRecursive(std::ostream& os, GameObject* gameObject) {
|
|||||||
|
|
||||||
os << "gameobject_begin\n";
|
os << "gameobject_begin\n";
|
||||||
os << "id=" << gameObject->GetID() << "\n";
|
os << "id=" << gameObject->GetID() << "\n";
|
||||||
|
os << "uuid=" << gameObject->GetUUID() << "\n";
|
||||||
os << "name=" << EscapeString(gameObject->GetName()) << "\n";
|
os << "name=" << EscapeString(gameObject->GetName()) << "\n";
|
||||||
os << "active=" << (gameObject->IsActive() ? 1 : 0) << "\n";
|
os << "active=" << (gameObject->IsActive() ? 1 : 0) << "\n";
|
||||||
os << "parent=" << (gameObject->GetParent() ? gameObject->GetParent()->GetID() : GameObject::INVALID_ID) << "\n";
|
os << "parent=" << (gameObject->GetParent() ? gameObject->GetParent()->GetID() : GameObject::INVALID_ID) << "\n";
|
||||||
@@ -219,6 +221,7 @@ std::vector<GameObject*> Scene::GetRootGameObjects() const {
|
|||||||
void Scene::Update(float deltaTime) {
|
void Scene::Update(float deltaTime) {
|
||||||
for (auto* go : GetRootGameObjects()) {
|
for (auto* go : GetRootGameObjects()) {
|
||||||
if (go->IsActiveInHierarchy()) {
|
if (go->IsActiveInHierarchy()) {
|
||||||
|
go->Start();
|
||||||
go->Update(deltaTime);
|
go->Update(deltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,6 +288,8 @@ void Scene::DeserializeFromString(const std::string& data) {
|
|||||||
if (key == "id") {
|
if (key == "id") {
|
||||||
currentObject->id = static_cast<GameObject::ID>(std::stoull(value));
|
currentObject->id = static_cast<GameObject::ID>(std::stoull(value));
|
||||||
maxId = std::max(maxId, currentObject->id);
|
maxId = std::max(maxId, currentObject->id);
|
||||||
|
} else if (key == "uuid") {
|
||||||
|
currentObject->uuid = std::stoull(value);
|
||||||
} else if (key == "name") {
|
} else if (key == "name") {
|
||||||
currentObject->name = UnescapeString(value);
|
currentObject->name = UnescapeString(value);
|
||||||
} else if (key == "active") {
|
} else if (key == "active") {
|
||||||
@@ -312,6 +317,9 @@ void Scene::DeserializeFromString(const std::string& data) {
|
|||||||
for (const PendingGameObjectData& pending : pendingObjects) {
|
for (const PendingGameObjectData& pending : pendingObjects) {
|
||||||
auto go = std::make_unique<GameObject>(pending.name);
|
auto go = std::make_unique<GameObject>(pending.name);
|
||||||
go->m_id = pending.id;
|
go->m_id = pending.id;
|
||||||
|
if (pending.uuid != 0) {
|
||||||
|
go->m_uuid = pending.uuid;
|
||||||
|
}
|
||||||
go->m_activeSelf = pending.active;
|
go->m_activeSelf = pending.active;
|
||||||
go->m_scene = this;
|
go->m_scene = this;
|
||||||
|
|
||||||
|
|||||||
89
engine/src/Scripting/ScriptComponent.cpp
Normal file
89
engine/src/Scripting/ScriptComponent.cpp
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#include "Scripting/ScriptComponent.h"
|
||||||
|
|
||||||
|
#include <random>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Scripting {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
uint64_t GenerateScriptComponentUUID() {
|
||||||
|
static std::random_device rd;
|
||||||
|
static std::mt19937_64 gen(rd());
|
||||||
|
static std::uniform_int_distribution<uint64_t> 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<uint64_t>(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
|
||||||
284
engine/src/Scripting/ScriptField.cpp
Normal file
284
engine/src/Scripting/ScriptField.cpp
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#include "Scripting/ScriptField.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <limits>
|
||||||
|
#include <sstream>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
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<typename T>
|
||||||
|
std::string SerializeScalar(T value) {
|
||||||
|
std::ostringstream os;
|
||||||
|
os << std::setprecision(std::numeric_limits<T>::max_digits10) << value;
|
||||||
|
return os.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename TVector>
|
||||||
|
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<TVector, Math::Vector2>) {
|
||||||
|
outVector = Math::Vector2(components[0], components[1]);
|
||||||
|
} else if constexpr (std::is_same_v<TVector, Math::Vector3>) {
|
||||||
|
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<std::monostate>(value);
|
||||||
|
case ScriptFieldType::Float: return std::holds_alternative<float>(value);
|
||||||
|
case ScriptFieldType::Double: return std::holds_alternative<double>(value);
|
||||||
|
case ScriptFieldType::Bool: return std::holds_alternative<bool>(value);
|
||||||
|
case ScriptFieldType::Int32: return std::holds_alternative<int32_t>(value);
|
||||||
|
case ScriptFieldType::UInt64: return std::holds_alternative<uint64_t>(value);
|
||||||
|
case ScriptFieldType::String: return std::holds_alternative<std::string>(value);
|
||||||
|
case ScriptFieldType::Vector2: return std::holds_alternative<Math::Vector2>(value);
|
||||||
|
case ScriptFieldType::Vector3: return std::holds_alternative<Math::Vector3>(value);
|
||||||
|
case ScriptFieldType::Vector4: return std::holds_alternative<Math::Vector4>(value);
|
||||||
|
case ScriptFieldType::GameObject: return std::holds_alternative<GameObjectReference>(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<float>(value));
|
||||||
|
case ScriptFieldType::Double:
|
||||||
|
return SerializeScalar(std::get<double>(value));
|
||||||
|
case ScriptFieldType::Bool:
|
||||||
|
return std::get<bool>(value) ? "1" : "0";
|
||||||
|
case ScriptFieldType::Int32:
|
||||||
|
return std::to_string(std::get<int32_t>(value));
|
||||||
|
case ScriptFieldType::UInt64:
|
||||||
|
return std::to_string(std::get<uint64_t>(value));
|
||||||
|
case ScriptFieldType::String:
|
||||||
|
return EscapeScriptString(std::get<std::string>(value));
|
||||||
|
case ScriptFieldType::Vector2: {
|
||||||
|
const Math::Vector2& vector = std::get<Math::Vector2>(value);
|
||||||
|
return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y);
|
||||||
|
}
|
||||||
|
case ScriptFieldType::Vector3: {
|
||||||
|
const Math::Vector3& vector = std::get<Math::Vector3>(value);
|
||||||
|
return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y) + "," + SerializeScalar(vector.z);
|
||||||
|
}
|
||||||
|
case ScriptFieldType::Vector4: {
|
||||||
|
const Math::Vector4& vector = std::get<Math::Vector4>(value);
|
||||||
|
return SerializeScalar(vector.x) + "," + SerializeScalar(vector.y) + "," + SerializeScalar(vector.z) + "," + SerializeScalar(vector.w);
|
||||||
|
}
|
||||||
|
case ScriptFieldType::GameObject:
|
||||||
|
return std::to_string(std::get<GameObjectReference>(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<int32_t>(std::stoi(value));
|
||||||
|
return true;
|
||||||
|
case ScriptFieldType::UInt64:
|
||||||
|
outValue = static_cast<uint64_t>(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<uint64_t>(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<char>(ch);
|
||||||
|
} else {
|
||||||
|
os << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(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<char>((high << 4) | low));
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back(value[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Scripting
|
||||||
|
} // namespace XCEngine
|
||||||
125
engine/src/Scripting/ScriptFieldStorage.cpp
Normal file
125
engine/src/Scripting/ScriptFieldStorage.cpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#include "Scripting/ScriptFieldStorage.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
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<std::string> ScriptFieldStorage::GetFieldNames() const {
|
||||||
|
std::vector<std::string> 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<std::string> 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
|
||||||
@@ -39,6 +39,8 @@ add_subdirectory(threading)
|
|||||||
add_subdirectory(debug)
|
add_subdirectory(debug)
|
||||||
add_subdirectory(components)
|
add_subdirectory(components)
|
||||||
add_subdirectory(scene)
|
add_subdirectory(scene)
|
||||||
|
add_subdirectory(scripting)
|
||||||
|
add_subdirectory(Rendering)
|
||||||
add_subdirectory(rhi)
|
add_subdirectory(rhi)
|
||||||
add_subdirectory(resources)
|
add_subdirectory(resources)
|
||||||
add_subdirectory(input)
|
add_subdirectory(input)
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ public:
|
|||||||
bool m_onEnableCalled = false;
|
bool m_onEnableCalled = false;
|
||||||
bool m_onDisableCalled = false;
|
bool m_onDisableCalled = false;
|
||||||
bool m_onDestroyCalled = false;
|
bool m_onDestroyCalled = false;
|
||||||
|
int m_onEnableCount = 0;
|
||||||
|
int m_onDisableCount = 0;
|
||||||
|
|
||||||
void Awake() override { m_awakeCalled = true; }
|
void Awake() override { m_awakeCalled = true; }
|
||||||
void Start() override { m_startCalled = true; }
|
void Start() override { m_startCalled = true; }
|
||||||
void Update(float deltaTime) override { m_updateCalled = true; }
|
void Update(float deltaTime) override { m_updateCalled = true; }
|
||||||
void OnEnable() override { m_onEnableCalled = true; }
|
void OnEnable() override { m_onEnableCalled = true; ++m_onEnableCount; }
|
||||||
void OnDisable() override { m_onDisableCalled = true; }
|
void OnDisable() override { m_onDisableCalled = true; ++m_onDisableCount; }
|
||||||
void OnDestroy() override { m_onDestroyCalled = true; }
|
void OnDestroy() override { m_onDestroyCalled = true; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -107,4 +109,24 @@ TEST(Component_Test, SetEnabled_NoCallback_WhenStateUnchanged) {
|
|||||||
EXPECT_FALSE(comp.m_onDisableCalled);
|
EXPECT_FALSE(comp.m_onDisableCalled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Component_Test, SetEnabled_AttachedInactiveGameObject_DelaysOnEnableUntilActivation) {
|
||||||
|
GameObject go;
|
||||||
|
TestComponent* comp = go.AddComponent<TestComponent>();
|
||||||
|
|
||||||
|
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
|
} // namespace
|
||||||
@@ -21,10 +21,21 @@ public:
|
|||||||
bool m_awakeCalled = false;
|
bool m_awakeCalled = false;
|
||||||
bool m_startCalled = false;
|
bool m_startCalled = false;
|
||||||
bool m_updateCalled = 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 Awake() override { m_awakeCalled = true; }
|
||||||
void Start() override { m_startCalled = true; }
|
void Start() override { m_startCalled = true; ++m_startCount; }
|
||||||
void Update(float deltaTime) override { m_updateCalled = true; }
|
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:
|
private:
|
||||||
std::string m_customName;
|
std::string m_customName;
|
||||||
@@ -45,7 +56,6 @@ TEST(GameObject_Test, DefaultConstructor_DefaultValues) {
|
|||||||
EXPECT_EQ(go.GetName(), "GameObject");
|
EXPECT_EQ(go.GetName(), "GameObject");
|
||||||
EXPECT_TRUE(go.IsActive());
|
EXPECT_TRUE(go.IsActive());
|
||||||
EXPECT_NE(go.GetID(), GameObject::INVALID_ID);
|
EXPECT_NE(go.GetID(), GameObject::INVALID_ID);
|
||||||
EXPECT_EQ(go.GetID(), 1u);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(GameObject_Test, NamedConstructor) {
|
TEST(GameObject_Test, NamedConstructor) {
|
||||||
@@ -203,6 +213,34 @@ TEST(GameObject_Test, SetActive_False) {
|
|||||||
EXPECT_FALSE(go.IsActive());
|
EXPECT_FALSE(go.IsActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(GameObject_Test, SetActive_PropagatesEnableDisableToChildren) {
|
||||||
|
GameObject parent("Parent");
|
||||||
|
GameObject child("Child");
|
||||||
|
TestComponent* comp = child.AddComponent<TestComponent>();
|
||||||
|
|
||||||
|
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<TestComponent>();
|
||||||
|
|
||||||
|
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) {
|
TEST(GameObject_Test, IsActiveInHierarchy_True) {
|
||||||
GameObject parent("Parent");
|
GameObject parent("Parent");
|
||||||
GameObject child("Child");
|
GameObject child("Child");
|
||||||
@@ -238,6 +276,17 @@ TEST(GameObject_Test, Lifecycle_Start) {
|
|||||||
go.Start();
|
go.Start();
|
||||||
|
|
||||||
EXPECT_TRUE(comp->m_startCalled);
|
EXPECT_TRUE(comp->m_startCalled);
|
||||||
|
EXPECT_EQ(comp->m_startCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(GameObject_Test, Lifecycle_Start_IsOnlyCalledOnce) {
|
||||||
|
GameObject go;
|
||||||
|
TestComponent* comp = go.AddComponent<TestComponent>();
|
||||||
|
|
||||||
|
go.Start();
|
||||||
|
go.Start();
|
||||||
|
|
||||||
|
EXPECT_EQ(comp->m_startCount, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(GameObject_Test, Lifecycle_Update) {
|
TEST(GameObject_Test, Lifecycle_Update) {
|
||||||
@@ -247,6 +296,17 @@ TEST(GameObject_Test, Lifecycle_Update) {
|
|||||||
go.Update(0.016f);
|
go.Update(0.016f);
|
||||||
|
|
||||||
EXPECT_TRUE(comp->m_updateCalled);
|
EXPECT_TRUE(comp->m_updateCalled);
|
||||||
|
EXPECT_EQ(comp->m_updateCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(GameObject_Test, Destroy_CallsOnDestroyOnce_WhenStandalone) {
|
||||||
|
GameObject go;
|
||||||
|
TestComponent* comp = go.AddComponent<TestComponent>();
|
||||||
|
|
||||||
|
go.Destroy();
|
||||||
|
|
||||||
|
EXPECT_TRUE(comp->m_onDestroyCalled);
|
||||||
|
EXPECT_EQ(comp->m_onDestroyCount, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(GameObjectTest, Find_Exists) {
|
TEST_F(GameObjectTest, Find_Exists) {
|
||||||
|
|||||||
@@ -27,9 +27,22 @@ public:
|
|||||||
|
|
||||||
bool m_awakeCalled = false;
|
bool m_awakeCalled = false;
|
||||||
bool m_updateCalled = 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 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:
|
private:
|
||||||
std::string m_customName;
|
std::string m_customName;
|
||||||
@@ -111,6 +124,17 @@ TEST_F(SceneTest, DestroyGameObject_WithChildren) {
|
|||||||
EXPECT_EQ(testScene->Find("Child"), nullptr);
|
EXPECT_EQ(testScene->Find("Child"), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(SceneTest, DestroyGameObject_CallsOnDestroyOnce) {
|
||||||
|
GameObject* go = testScene->CreateGameObject("DestroyMe");
|
||||||
|
TestComponent* comp = go->AddComponent<TestComponent>();
|
||||||
|
int destroyCount = 0;
|
||||||
|
comp->m_externalOnDestroyCount = &destroyCount;
|
||||||
|
|
||||||
|
testScene->DestroyGameObject(go);
|
||||||
|
|
||||||
|
EXPECT_EQ(destroyCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(SceneTest, Find_Exists) {
|
TEST_F(SceneTest, Find_Exists) {
|
||||||
testScene->CreateGameObject("FindMe");
|
testScene->CreateGameObject("FindMe");
|
||||||
|
|
||||||
@@ -163,6 +187,28 @@ TEST_F(SceneTest, Update_SkipsInactiveObjects) {
|
|||||||
EXPECT_FALSE(comp->m_updateCalled);
|
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<TestComponent>();
|
||||||
|
|
||||||
|
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<TestComponent>();
|
||||||
|
|
||||||
|
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) {
|
TEST(Scene_Test, IsActive_DefaultTrue) {
|
||||||
Scene s;
|
Scene s;
|
||||||
|
|
||||||
@@ -415,6 +461,27 @@ TEST_F(SceneTest, SerializeToString_And_DeserializeFromString_PreservesHierarchy
|
|||||||
EXPECT_TRUE(loadedCamera->IsPrimary());
|
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) {
|
TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) {
|
||||||
GameObject* parent = testScene->CreateGameObject("Parent");
|
GameObject* parent = testScene->CreateGameObject("Parent");
|
||||||
GameObject* child = testScene->CreateGameObject("Child", parent);
|
GameObject* child = testScene->CreateGameObject("Child", parent);
|
||||||
|
|||||||
29
tests/scripting/CMakeLists.txt
Normal file
29
tests/scripting/CMakeLists.txt
Normal file
@@ -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)
|
||||||
143
tests/scripting/test_script_component.cpp
Normal file
143
tests/scripting/test_script_component.cpp
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Components/ComponentFactoryRegistry.h>
|
||||||
|
#include <XCEngine/Components/GameObject.h>
|
||||||
|
#include <XCEngine/Scene/Scene.h>
|
||||||
|
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
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<ScriptComponent*>(registry.CreateComponent(&gameObject, "ScriptComponent"));
|
||||||
|
ASSERT_NE(created, nullptr);
|
||||||
|
EXPECT_EQ(gameObject.GetComponents<ScriptComponent>().size(), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScriptComponent_Test, GameObjectCanOwnMultipleScriptComponents) {
|
||||||
|
GameObject gameObject("ScriptHost");
|
||||||
|
|
||||||
|
ScriptComponent* first = gameObject.AddComponent<ScriptComponent>();
|
||||||
|
ScriptComponent* second = gameObject.AddComponent<ScriptComponent>();
|
||||||
|
|
||||||
|
ASSERT_NE(first, nullptr);
|
||||||
|
ASSERT_NE(second, nullptr);
|
||||||
|
EXPECT_NE(first, second);
|
||||||
|
EXPECT_EQ(gameObject.GetComponents<ScriptComponent>().size(), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScriptComponent_Test, SceneRoundTripPreservesMultipleScriptComponentsAndFields) {
|
||||||
|
Scene scene("Script Scene");
|
||||||
|
GameObject* host = scene.CreateGameObject("Host");
|
||||||
|
GameObject* targetObject = scene.CreateGameObject("Target");
|
||||||
|
|
||||||
|
ScriptComponent* movement = host->AddComponent<ScriptComponent>();
|
||||||
|
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<ScriptComponent>();
|
||||||
|
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<ScriptComponent*> loadedScripts = loadedHost->GetComponents<ScriptComponent>();
|
||||||
|
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
|
||||||
92
tests/scripting/test_script_field_storage.cpp
Normal file
92
tests/scripting/test_script_field_storage.cpp
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Scripting/ScriptFieldStorage.h>
|
||||||
|
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user