2026-03-20 19:59:06 +08:00
|
|
|
#include "Scene/Scene.h"
|
2026-03-26 02:24:11 +08:00
|
|
|
#include "Components/ComponentFactoryRegistry.h"
|
2026-03-20 19:59:06 +08:00
|
|
|
#include "Components/GameObject.h"
|
2026-03-26 01:26:26 +08:00
|
|
|
#include "Components/TransformComponent.h"
|
2026-03-22 03:42:40 +08:00
|
|
|
#include <sstream>
|
2026-03-26 01:26:26 +08:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <unordered_map>
|
2026-03-20 19:59:06 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Components {
|
|
|
|
|
|
2026-03-26 01:26:26 +08:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
struct PendingComponentData {
|
|
|
|
|
std::string type;
|
|
|
|
|
std::string payload;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct PendingGameObjectData {
|
|
|
|
|
GameObject::ID id = GameObject::INVALID_ID;
|
2026-03-26 20:14:58 +08:00
|
|
|
uint64_t uuid = 0;
|
2026-03-26 01:26:26 +08:00
|
|
|
std::string name = "GameObject";
|
|
|
|
|
bool active = true;
|
|
|
|
|
GameObject::ID parentId = GameObject::INVALID_ID;
|
|
|
|
|
std::string transformPayload;
|
|
|
|
|
std::vector<PendingComponentData> components;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
std::string EscapeString(const std::string& value) {
|
|
|
|
|
std::string escaped;
|
|
|
|
|
escaped.reserve(value.size());
|
|
|
|
|
for (char ch : value) {
|
|
|
|
|
if (ch == '\\' || ch == '\n' || ch == '\r') {
|
|
|
|
|
escaped.push_back('\\');
|
|
|
|
|
if (ch == '\n') {
|
|
|
|
|
escaped.push_back('n');
|
|
|
|
|
} else if (ch == '\r') {
|
|
|
|
|
escaped.push_back('r');
|
|
|
|
|
} else {
|
|
|
|
|
escaped.push_back(ch);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
escaped.push_back(ch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return escaped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string UnescapeString(const std::string& value) {
|
|
|
|
|
std::string unescaped;
|
|
|
|
|
unescaped.reserve(value.size());
|
|
|
|
|
for (size_t i = 0; i < value.size(); ++i) {
|
|
|
|
|
if (value[i] == '\\' && i + 1 < value.size()) {
|
|
|
|
|
++i;
|
|
|
|
|
switch (value[i]) {
|
|
|
|
|
case 'n': unescaped.push_back('\n'); break;
|
|
|
|
|
case 'r': unescaped.push_back('\r'); break;
|
|
|
|
|
default: unescaped.push_back(value[i]); break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
unescaped.push_back(value[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return unescaped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SerializeGameObjectRecursive(std::ostream& os, GameObject* gameObject) {
|
|
|
|
|
if (!gameObject) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
os << "gameobject_begin\n";
|
|
|
|
|
os << "id=" << gameObject->GetID() << "\n";
|
2026-03-26 20:14:58 +08:00
|
|
|
os << "uuid=" << gameObject->GetUUID() << "\n";
|
2026-03-26 01:26:26 +08:00
|
|
|
os << "name=" << EscapeString(gameObject->GetName()) << "\n";
|
|
|
|
|
os << "active=" << (gameObject->IsActive() ? 1 : 0) << "\n";
|
|
|
|
|
os << "parent=" << (gameObject->GetParent() ? gameObject->GetParent()->GetID() : GameObject::INVALID_ID) << "\n";
|
|
|
|
|
os << "transform=";
|
|
|
|
|
gameObject->GetTransform()->Serialize(os);
|
|
|
|
|
os << "\n";
|
|
|
|
|
|
|
|
|
|
auto components = gameObject->GetComponents<Component>();
|
|
|
|
|
for (Component* component : components) {
|
|
|
|
|
if (!component || component == gameObject->GetTransform()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
os << "component=" << component->GetName() << ";";
|
|
|
|
|
component->Serialize(os);
|
|
|
|
|
os << "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
os << "gameobject_end\n";
|
|
|
|
|
|
|
|
|
|
for (GameObject* child : gameObject->GetChildren()) {
|
|
|
|
|
SerializeGameObjectRecursive(os, child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-03-20 19:59:06 +08:00
|
|
|
Scene::Scene()
|
|
|
|
|
: m_name("Untitled") {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scene::Scene(const std::string& name)
|
|
|
|
|
: m_name(name) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Scene::~Scene() {
|
2026-03-31 21:25:59 +08:00
|
|
|
auto& registry = GameObject::GetGlobalRegistry();
|
|
|
|
|
for (const auto& entry : m_gameObjects) {
|
|
|
|
|
registry.erase(entry.first);
|
|
|
|
|
}
|
2026-03-20 19:59:06 +08:00
|
|
|
m_gameObjects.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameObject* Scene::CreateGameObject(const std::string& name, GameObject* parent) {
|
|
|
|
|
auto gameObject = std::make_unique<GameObject>(name);
|
|
|
|
|
GameObject* ptr = gameObject.get();
|
|
|
|
|
|
2026-03-21 12:12:32 +08:00
|
|
|
GameObject::GetGlobalRegistry()[ptr->m_id] = ptr;
|
2026-03-20 19:59:06 +08:00
|
|
|
m_gameObjectIDs.insert(ptr->m_id);
|
|
|
|
|
m_gameObjects.emplace(ptr->m_id, std::move(gameObject));
|
2026-03-26 01:26:26 +08:00
|
|
|
ptr->m_scene = this;
|
2026-03-20 19:59:06 +08:00
|
|
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
ptr->SetParent(parent);
|
|
|
|
|
} else {
|
|
|
|
|
m_rootGameObjects.push_back(ptr->m_id);
|
|
|
|
|
}
|
|
|
|
|
ptr->Awake();
|
|
|
|
|
|
|
|
|
|
m_onGameObjectCreated.Invoke(ptr);
|
|
|
|
|
|
|
|
|
|
return ptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Scene::DestroyGameObject(GameObject* gameObject) {
|
|
|
|
|
if (!gameObject || gameObject->m_scene != this) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (auto* child : gameObject->GetChildren()) {
|
|
|
|
|
DestroyGameObject(child);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (gameObject->m_parent) {
|
|
|
|
|
auto& siblings = gameObject->m_parent->m_children;
|
|
|
|
|
siblings.erase(std::remove(siblings.begin(), siblings.end(), gameObject), siblings.end());
|
|
|
|
|
} else {
|
|
|
|
|
m_rootGameObjects.erase(
|
|
|
|
|
std::remove(m_rootGameObjects.begin(), m_rootGameObjects.end(), gameObject->m_id),
|
|
|
|
|
m_rootGameObjects.end()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_onGameObjectDestroyed.Invoke(gameObject);
|
|
|
|
|
|
|
|
|
|
gameObject->OnDestroy();
|
|
|
|
|
|
|
|
|
|
m_gameObjectIDs.erase(gameObject->m_id);
|
2026-03-31 21:25:59 +08:00
|
|
|
GameObject::GetGlobalRegistry().erase(gameObject->m_id);
|
2026-03-20 19:59:06 +08:00
|
|
|
m_gameObjects.erase(gameObject->m_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameObject* Scene::Find(const std::string& name) const {
|
|
|
|
|
for (auto* go : GetRootGameObjects()) {
|
|
|
|
|
if (go->GetName() == name) {
|
|
|
|
|
return go;
|
|
|
|
|
}
|
|
|
|
|
GameObject* found = FindInChildren(go, name);
|
|
|
|
|
if (found) {
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 17:51:15 +08:00
|
|
|
GameObject* Scene::FindByID(GameObjectID id) const {
|
|
|
|
|
auto it = m_gameObjects.find(id);
|
|
|
|
|
if (it != m_gameObjects.end()) {
|
|
|
|
|
return it->second.get();
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:59:06 +08:00
|
|
|
GameObject* Scene::FindInChildren(GameObject* parent, const std::string& name) const {
|
|
|
|
|
for (size_t i = 0; i < parent->GetChildCount(); ++i) {
|
|
|
|
|
GameObject* child = parent->GetChild(i);
|
|
|
|
|
if (child->GetName() == name) {
|
|
|
|
|
return child;
|
|
|
|
|
}
|
|
|
|
|
GameObject* found = FindInChildren(child, name);
|
|
|
|
|
if (found) {
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameObject* Scene::FindGameObjectWithTag(const std::string& tag) const {
|
|
|
|
|
for (auto* go : GetRootGameObjects()) {
|
|
|
|
|
if (go->GetName() == tag) {
|
|
|
|
|
return go;
|
|
|
|
|
}
|
|
|
|
|
GameObject* found = FindInChildren(go, tag);
|
|
|
|
|
if (found) {
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<GameObject*> Scene::GetRootGameObjects() const {
|
|
|
|
|
std::vector<GameObject*> result;
|
|
|
|
|
for (auto id : m_rootGameObjects) {
|
|
|
|
|
auto it = m_gameObjects.find(id);
|
|
|
|
|
if (it != m_gameObjects.end()) {
|
|
|
|
|
result.push_back(it->second.get());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Scene::Update(float deltaTime) {
|
|
|
|
|
for (auto* go : GetRootGameObjects()) {
|
|
|
|
|
if (go->IsActiveInHierarchy()) {
|
2026-03-26 20:14:58 +08:00
|
|
|
go->Start();
|
2026-03-20 19:59:06 +08:00
|
|
|
go->Update(deltaTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Scene::FixedUpdate(float fixedDeltaTime) {
|
|
|
|
|
for (auto* go : GetRootGameObjects()) {
|
|
|
|
|
if (go->IsActiveInHierarchy()) {
|
|
|
|
|
go->FixedUpdate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Scene::LateUpdate(float deltaTime) {
|
|
|
|
|
for (auto* go : GetRootGameObjects()) {
|
|
|
|
|
if (go->IsActiveInHierarchy()) {
|
|
|
|
|
go->LateUpdate(deltaTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 01:59:14 +08:00
|
|
|
void Scene::DeserializeFromString(const std::string& data) {
|
2026-03-31 21:25:59 +08:00
|
|
|
auto& registry = GameObject::GetGlobalRegistry();
|
|
|
|
|
for (const auto& entry : m_gameObjects) {
|
|
|
|
|
registry.erase(entry.first);
|
|
|
|
|
}
|
2026-03-20 19:59:06 +08:00
|
|
|
m_gameObjects.clear();
|
|
|
|
|
m_rootGameObjects.clear();
|
2026-03-22 03:42:40 +08:00
|
|
|
m_gameObjectIDs.clear();
|
|
|
|
|
|
2026-03-26 01:26:26 +08:00
|
|
|
std::vector<PendingGameObjectData> pendingObjects;
|
2026-03-26 01:59:14 +08:00
|
|
|
std::istringstream input(data);
|
2026-03-22 03:42:40 +08:00
|
|
|
std::string line;
|
2026-03-26 01:26:26 +08:00
|
|
|
PendingGameObjectData* currentObject = nullptr;
|
|
|
|
|
GameObject::ID maxId = 0;
|
|
|
|
|
|
2026-03-26 01:59:14 +08:00
|
|
|
while (std::getline(input, line)) {
|
2026-03-22 03:42:40 +08:00
|
|
|
if (line.empty() || line[0] == '#') continue;
|
2026-03-26 01:26:26 +08:00
|
|
|
|
|
|
|
|
if (line == "gameobject_begin") {
|
|
|
|
|
pendingObjects.emplace_back();
|
|
|
|
|
currentObject = &pendingObjects.back();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (line == "gameobject_end") {
|
|
|
|
|
currentObject = nullptr;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t eqPos = line.find('=');
|
|
|
|
|
if (eqPos == std::string::npos) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const std::string key = line.substr(0, eqPos);
|
|
|
|
|
const std::string value = line.substr(eqPos + 1);
|
|
|
|
|
|
|
|
|
|
if (!currentObject) {
|
|
|
|
|
if (key == "scene") {
|
|
|
|
|
m_name = UnescapeString(value);
|
|
|
|
|
} else if (key == "active") {
|
|
|
|
|
m_active = (value == "1");
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (key == "id") {
|
|
|
|
|
currentObject->id = static_cast<GameObject::ID>(std::stoull(value));
|
|
|
|
|
maxId = std::max(maxId, currentObject->id);
|
2026-03-26 20:14:58 +08:00
|
|
|
} else if (key == "uuid") {
|
|
|
|
|
currentObject->uuid = std::stoull(value);
|
2026-03-26 01:26:26 +08:00
|
|
|
} else if (key == "name") {
|
|
|
|
|
currentObject->name = UnescapeString(value);
|
|
|
|
|
} else if (key == "active") {
|
|
|
|
|
currentObject->active = (value == "1");
|
|
|
|
|
} else if (key == "parent") {
|
|
|
|
|
currentObject->parentId = static_cast<GameObject::ID>(std::stoull(value));
|
|
|
|
|
} else if (key == "transform") {
|
|
|
|
|
currentObject->transformPayload = value;
|
|
|
|
|
} else if (key == "component") {
|
|
|
|
|
const size_t typeEnd = value.find(';');
|
|
|
|
|
PendingComponentData componentData;
|
|
|
|
|
if (typeEnd == std::string::npos) {
|
|
|
|
|
componentData.type = value;
|
|
|
|
|
} else {
|
|
|
|
|
componentData.type = value.substr(0, typeEnd);
|
|
|
|
|
componentData.payload = value.substr(typeEnd + 1);
|
|
|
|
|
}
|
|
|
|
|
currentObject->components.push_back(std::move(componentData));
|
2026-03-22 03:42:40 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-26 01:26:26 +08:00
|
|
|
|
|
|
|
|
std::unordered_map<GameObject::ID, GameObject*> createdObjects;
|
|
|
|
|
createdObjects.reserve(pendingObjects.size());
|
|
|
|
|
|
|
|
|
|
for (const PendingGameObjectData& pending : pendingObjects) {
|
|
|
|
|
auto go = std::make_unique<GameObject>(pending.name);
|
|
|
|
|
go->m_id = pending.id;
|
2026-03-26 20:14:58 +08:00
|
|
|
if (pending.uuid != 0) {
|
|
|
|
|
go->m_uuid = pending.uuid;
|
|
|
|
|
}
|
2026-03-26 01:26:26 +08:00
|
|
|
go->m_activeSelf = pending.active;
|
|
|
|
|
go->m_scene = this;
|
|
|
|
|
|
|
|
|
|
if (!pending.transformPayload.empty()) {
|
|
|
|
|
std::istringstream transformStream(pending.transformPayload);
|
|
|
|
|
go->m_transform->Deserialize(transformStream);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const PendingComponentData& componentData : pending.components) {
|
2026-03-26 02:24:11 +08:00
|
|
|
if (Component* component = ComponentFactoryRegistry::Get().CreateComponent(go.get(), componentData.type)) {
|
2026-03-26 01:26:26 +08:00
|
|
|
if (!componentData.payload.empty()) {
|
|
|
|
|
std::istringstream componentStream(componentData.payload);
|
|
|
|
|
component->Deserialize(componentStream);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameObject* ptr = go.get();
|
|
|
|
|
GameObject::GetGlobalRegistry()[ptr->m_id] = ptr;
|
|
|
|
|
m_gameObjectIDs.insert(ptr->m_id);
|
|
|
|
|
createdObjects[ptr->m_id] = ptr;
|
|
|
|
|
m_gameObjects.emplace(ptr->m_id, std::move(go));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_rootGameObjects.clear();
|
|
|
|
|
for (const PendingGameObjectData& pending : pendingObjects) {
|
|
|
|
|
auto it = createdObjects.find(pending.id);
|
|
|
|
|
if (it == createdObjects.end()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameObject* gameObject = it->second;
|
|
|
|
|
if (pending.parentId == GameObject::INVALID_ID) {
|
|
|
|
|
m_rootGameObjects.push_back(gameObject->GetID());
|
|
|
|
|
} else {
|
|
|
|
|
auto parentIt = createdObjects.find(pending.parentId);
|
|
|
|
|
if (parentIt != createdObjects.end()) {
|
|
|
|
|
gameObject->SetParent(parentIt->second, false);
|
|
|
|
|
} else {
|
|
|
|
|
m_rootGameObjects.push_back(gameObject->GetID());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (maxId != GameObject::INVALID_ID && GameObject::s_nextID <= maxId) {
|
|
|
|
|
GameObject::s_nextID = maxId + 1;
|
|
|
|
|
}
|
2026-03-20 19:59:06 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 01:59:14 +08:00
|
|
|
std::string Scene::SerializeToString() const {
|
|
|
|
|
std::ostringstream output;
|
|
|
|
|
|
|
|
|
|
output << "# XCEngine Scene File\n";
|
|
|
|
|
output << "scene=" << EscapeString(m_name) << "\n";
|
|
|
|
|
output << "active=" << (m_active ? "1" : "0") << "\n\n";
|
|
|
|
|
|
|
|
|
|
for (auto* go : GetRootGameObjects()) {
|
|
|
|
|
SerializeGameObjectRecursive(output, go);
|
|
|
|
|
output << "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return output.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Scene::Load(const std::string& filePath) {
|
|
|
|
|
std::ifstream file(filePath);
|
2026-03-22 03:42:40 +08:00
|
|
|
if (!file.is_open()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-26 01:59:14 +08:00
|
|
|
std::stringstream buffer;
|
|
|
|
|
buffer << file.rdbuf();
|
|
|
|
|
DeserializeFromString(buffer.str());
|
|
|
|
|
}
|
2026-03-22 03:42:40 +08:00
|
|
|
|
2026-03-26 01:59:14 +08:00
|
|
|
void Scene::Save(const std::string& filePath) {
|
|
|
|
|
std::ofstream file(filePath);
|
|
|
|
|
if (!file.is_open()) {
|
|
|
|
|
return;
|
2026-03-22 03:42:40 +08:00
|
|
|
}
|
2026-03-26 01:59:14 +08:00
|
|
|
|
|
|
|
|
file << SerializeToString();
|
2026-03-20 19:59:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace Components
|
2026-03-26 01:26:26 +08:00
|
|
|
} // namespace XCEngine
|