From 8e9fd9225fb19f876f012f1603dc0973b72ae829 Mon Sep 17 00:00:00 2001 From: Daniu Date: Tue, 7 Apr 2026 23:40:03 +0200 Subject: [PATCH] ECS System WIP --- .gitignore | 1 + CMakeLists.txt | 19 ++- application/application.cpp | 8 + application/application.h | 2 + application/uilayer.cpp | 173 ++++++++++++++++++++- application/uilayer.h | 4 + entitysystem/ECScene.cpp | 69 ++++++++ entitysystem/ECScene.h | 42 +++++ entitysystem/ECWorld.cpp | 54 +++++++ entitysystem/ECWorld.h | 135 ++++++++++++++++ entitysystem/SceneManager.cpp | 59 +++++++ entitysystem/SceneManager.h | 40 +++++ entitysystem/components/BasicComponents.h | 24 ++- entitysystem/components/RenderComponents.h | 53 ++++++- entitysystem/ecs.cpp | 69 -------- entitysystem/ecs.h | 153 ------------------ entitysystem/scenes/GameScene.cpp | 35 +++++ entitysystem/scenes/GameScene.h | 30 ++++ entitysystem/systems/BaseSystem.cpp | 5 + entitysystem/systems/BaseSystem.h | 25 +++ entitysystem/systems/MovementSystem.cpp | 29 ++++ entitysystem/systems/MovementSystem.h | 25 +++ entitysystem/systems/SystemManager.cpp | 33 ++++ entitysystem/systems/SystemManager.h | 46 ++++++ registry/FName.h | 63 ++++++++ registry/NameHash.h | 41 +++++ registry/NameLiteral.h | 11 ++ registry/NameRegistry.cpp | 52 +++++++ registry/NameRegistry.h | 18 +++ simulation/simulationstateserializer.cpp | 57 +++++-- 30 files changed, 1133 insertions(+), 242 deletions(-) create mode 100644 entitysystem/ECScene.cpp create mode 100644 entitysystem/ECScene.h create mode 100644 entitysystem/ECWorld.cpp create mode 100644 entitysystem/ECWorld.h create mode 100644 entitysystem/SceneManager.cpp create mode 100644 entitysystem/SceneManager.h delete mode 100644 entitysystem/ecs.cpp delete mode 100644 entitysystem/ecs.h create mode 100644 entitysystem/scenes/GameScene.cpp create mode 100644 entitysystem/scenes/GameScene.h create mode 100644 entitysystem/systems/BaseSystem.cpp create mode 100644 entitysystem/systems/BaseSystem.h create mode 100644 entitysystem/systems/MovementSystem.cpp create mode 100644 entitysystem/systems/MovementSystem.h create mode 100644 entitysystem/systems/SystemManager.cpp create mode 100644 entitysystem/systems/SystemManager.h create mode 100644 registry/FName.h create mode 100644 registry/NameHash.h create mode 100644 registry/NameLiteral.h create mode 100644 registry/NameRegistry.cpp create mode 100644 registry/NameRegistry.h diff --git a/.gitignore b/.gitignore index 6cd0a67b..0f35ca92 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ bin/ #Visual Studio [Code] folders .vscode/ .vs/ +.cache/ # JetBrains CLion folders cmake-build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e693e80..571cabaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -181,6 +181,7 @@ set(SOURCES "scripting/pythonscreenviewer.cpp" "utilities/dictionary.cpp" "rendering/particles.cpp" +"registry/NameRegistry.cpp" "utilities/headtrack.cpp" "scripting/ladderlogic.cpp" "rendering/geometrybank.cpp" @@ -236,8 +237,6 @@ set(SOURCES "vr/vr_interface.cpp" entitysystem/components/BasicComponents.h - entitysystem/ecs.cpp - entitysystem/ecs.h entitysystem/components/RenderComponents.h ) @@ -397,7 +396,21 @@ configure_file(${CMAKE_SOURCE_DIR}/eu07.rc.in if (${CMAKE_CXX_COMPILER_ID} STREQUAL MSVC) set(SOURCES ${SOURCES} ${CMAKE_BINARY_DIR}/eu07.rc) - set(SOURCES ${SOURCES} eu07.ico) + set(SOURCES ${SOURCES} eu07.ico + entitysystem/ECScene.cpp + entitysystem/ECScene.h + entitysystem/ECWorld.cpp + entitysystem/ECWorld.h + entitysystem/scenes/GameScene.cpp + entitysystem/scenes/GameScene.h + entitysystem/SceneManager.cpp + entitysystem/SceneManager.h + entitysystem/systems/SystemManager.cpp + entitysystem/systems/SystemManager.h + entitysystem/systems/BaseSystem.cpp + entitysystem/systems/BaseSystem.h + entitysystem/systems/MovementSystem.cpp + entitysystem/systems/MovementSystem.h) add_compile_options("/utf-8") endif() diff --git a/application/application.cpp b/application/application.cpp index 95cc1d52..5b256dd1 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -37,6 +37,9 @@ http://mozilla.org/MPL/2.0/. #include #endif +#include "entitysystem/SceneManager.h" +#include "entitysystem/scenes/GameScene.h" + #include #include "utilities/translation.h" @@ -452,6 +455,8 @@ bool eu07_application::is_client() const int eu07_application::run() { auto frame{0}; + + sceneManager.SetScene(std::make_unique()); // main application loop while (!glfwWindowShouldClose(m_windows.front()) && !m_modestack.empty()) { @@ -474,6 +479,9 @@ int eu07_application::run() double frameStartTime = Timer::GetTime(); + float dt = (float) Timer::GetDeltaTime(); + sceneManager.Update(dt); + if (m_modes[m_modestack.top()]->is_command_processor()) { // active mode is doing real calculations (e.g. drivermode) diff --git a/application/application.h b/application/application.h index d461a0c9..0841bb0d 100644 --- a/application/application.h +++ b/application/application.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "application/applicationmode.h" +#include "entitysystem/SceneManager.h" #include "scripting/PyInt.h" #include "network/manager.h" #include "utilities/headtrack.h" @@ -38,6 +39,7 @@ public: run(); // issues request for a worker thread to perform specified task. returns: true if task was scheduled + SceneManager sceneManager; #ifdef WITH_DISCORD_RPC void DiscordRPCService(); // discord rich presence service function (runs as separate thread) #endif diff --git a/application/uilayer.cpp b/application/uilayer.cpp index b1c40292..27bc2493 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -19,6 +19,9 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include "application/application.h" #include "application/editormode.h" +#include "entitysystem/ECScene.h" +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" #include "imgui/imgui_impl_glfw.h" @@ -383,6 +386,10 @@ void ui_layer::render() render_menu(); render_quit_widget(); render_hierarchy(); + if (auto* scene = Application.sceneManager.CurrentScene()) + { + render_entity_hierarchy(scene->World()); + } // template method implementation render_(); @@ -470,7 +477,171 @@ void ui_layer::render_hierarchy(){ ImGui::End(); } - + +void ui_layer::render_entity_hierarchy(ECWorld& world) +{ + ImGui::SetNextWindowSize(ImVec2(900, 500), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin(STR_C("Entity Hierarchy"), &m_entity_hierarchy)) + { + ImGui::End(); + return; + } + + const float left_panel_width = 280.0f; + + ImGui::BeginChild("entity_list_panel", ImVec2(left_panel_width, 0), true); + ImGui::Text("Registered entities: %zu", world.GetEntityCount()); + ImGui::Separator(); + + for (auto entity : world.GetEntities()) + { + char buf[64]; + std::snprintf(buf, sizeof(buf), "Entity %u", static_cast(entity)); + + const bool selected = (m_selected_entity == entity); + + if (ImGui::Selectable(buf, selected)) + { + m_selected_entity = entity; + } + + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Entity ID: %u", static_cast(entity)); + } + } + + ImGui::EndChild(); + + ImGui::SameLine(); + + ImGui::BeginChild("entity_inspector_panel", ImVec2(0, 0), true); + + if (m_selected_entity == entt::null) + { + ImGui::TextDisabled("No entity selected"); + ImGui::EndChild(); + ImGui::End(); + return; + } + + if (!world.IsAlive(m_selected_entity)) + { + ImGui::TextDisabled("Selected entity is no longer valid"); + ImGui::EndChild(); + ImGui::End(); + return; + } + + ImGui::Text("Selected entity: %u", static_cast(m_selected_entity)); + ImGui::Separator(); + + // Identification + if (auto* id = world.GetComponent(m_selected_entity)) + { + if (ImGui::CollapsingHeader("Identification", ImGuiTreeNodeFlags_DefaultOpen)) + { + char nameBuffer[256]; + std::snprintf(nameBuffer, sizeof(nameBuffer), "%s", id->Name.ToString().c_str()); + + if (ImGui::InputText("Name", nameBuffer, sizeof(nameBuffer))) + { + id->Name = nameBuffer; + } + } + } + + // Transform + if (auto* transform = world.GetComponent(m_selected_entity)) + { + if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) + { + + float pos[3] = { static_cast(transform->Position.x), static_cast(transform->Position.y), static_cast(transform->Position.z) }; + + if(ImGui::DragFloat3("Position", pos, 0.1f)){ + + } + + ImGui::DragFloat3("Rotation", + &transform->Rotation.x, 0.1, -360.0, 360.0); + + ImGui::DragFloat3("Scale", + &transform->Scale.x, 0.01, 0.0, 100000.0); + } + } + + // Velocity + if (auto* velocity = world.GetComponent(m_selected_entity)) + { + if (ImGui::CollapsingHeader("Velocity", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::DragFloat3("Value", + &velocity->Value.x, 0.1); + + if (ImGui::Button("Stop")) + { + velocity->Value = glm::dvec3(0.0); + } + } + } + + // LODController + if (auto* lod = world.GetComponent(m_selected_entity)) + { + if (ImGui::CollapsingHeader("LODController", ImGuiTreeNodeFlags_DefaultOpen)) + { + float range_min = lod->RangeMin; + float range_max = lod->RangeMax; + ImGui::DragFloat("Range Min", &range_min, 0.1, 0.0, 100000.0); + ImGui::DragFloat("Range Max", &range_max, 0.1, 0.0, 100000.0); + } + } + + // Sound + + if(auto* sound = world.GetComponent(m_selected_entity)){ + if(ImGui::CollapsingHeader("Sound", ImGuiTreeNodeFlags_DefaultOpen)){ + ImGui::Text("Sound file: %s", sound->sound.name().c_str()); + ImGui::DragFloat("Volume", &sound->volume, 0.01f, 0.0f, 1.0f); + ImGui::DragFloat("Pitch", &sound->pitch, 0.01f, 0.0f, 2.0f); + ImGui::Checkbox("Looping", &sound->loop); + ImGui::Checkbox("Playing", &sound->isPlaying); + ImGui::Checkbox("Play On Start", &sound->playOnStart); + } + } + + // MeshRenderer + + if(auto* mesh_renderer = world.GetComponent(m_selected_entity)) + { + if (ImGui::CollapsingHeader("MeshRenderer", ImGuiTreeNodeFlags_DefaultOpen)) + { + //ImGui::Text("Mesh: %s", mesh_renderer->meshHandle.ToString().c_str()); + ImGui::Checkbox("Visible", &mesh_renderer->visible); + } + } + + // SpotLight + + if (auto* light = world.GetComponent(m_selected_entity)) + { + if (ImGui::CollapsingHeader("SpotLight", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Checkbox("Enabled", &light->Enabled); + ImGui::ColorEdit3("Color", &light->Color.x); + ImGui::DragFloat("Intensity", &light->Intensity, 0.1f, 0.0f, 100.0f); + ImGui::DragFloat("Range", &light->Range, 0.5f, 0.0f, 500.0f); + ImGui::DragFloat("Inner Angle", &light->InnerAngle, 0.1f, 0.0f, light->OuterAngle); + ImGui::DragFloat("Outer Angle", &light->OuterAngle, 0.1f, light->InnerAngle, 90.0f); + ImGui::Checkbox("Cast Shadows", &light->CastShadows); + } + } + + ImGui::EndChild(); + ImGui::End(); +} void ui_layer::set_cursor(int const Mode) { glfwSetInputMode(m_window, GLFW_CURSOR, Mode); diff --git a/application/uilayer.h b/application/uilayer.h index 6d4019c6..abe9e1f2 100644 --- a/application/uilayer.h +++ b/application/uilayer.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include #include "model/Texture.h" #include "widgets/popup.h" +#include "entitysystem/ECWorld.h" // GuiLayer -- basic user interface class. draws requested information on top of openGL screen @@ -148,6 +149,7 @@ protected: void render_menu(); void render_quit_widget(); void render_hierarchy(); + void render_entity_hierarchy(ECWorld& world); // draws a quad between coordinates x,y and z,w with uv-coordinates spanning 0-1 void quad( glm::vec4 const &Coordinates, glm::vec4 const &Color ); // members @@ -161,4 +163,6 @@ protected: bool m_imgui_demo = false; bool m_editor_hierarchy = false; bool m_editor_change_history = false; + entt::entity m_selected_entity = entt::null; + bool m_entity_hierarchy = true; }; diff --git a/entitysystem/ECScene.cpp b/entitysystem/ECScene.cpp new file mode 100644 index 00000000..6e94ef79 --- /dev/null +++ b/entitysystem/ECScene.cpp @@ -0,0 +1,69 @@ +// +// Created by Daniu +// + +#include "ECScene.h" +#include "utilities/Logs.h" + +ECScene::ECScene() + : m_loaded(false) +{ +} + +ECScene::~ECScene() +{ + Unload(); +} + +void ECScene::Load() +{ + if (m_loaded) + return; + + m_systems.Create(m_world); + OnLoad(); + m_loaded = true; +} +void ECScene::Unload() +{ + if (!m_loaded) + return; + + OnUnload(); + m_world.Clear(); + m_loaded = false; +} + +void ECScene::Update(float dt) +{ + if (!m_loaded){ + + return; + } + m_systems.Update(m_world, dt); + OnUpdate(dt); +} + + +ECWorld& ECScene::World() +{ + return m_world; +} + +const ECWorld& ECScene::World() const +{ + return m_world; +} + +void ECScene::OnLoad() +{ +} + +void ECScene::OnUnload() +{; +} + +void ECScene::OnUpdate(float dt) +{ + (void)dt; +} \ No newline at end of file diff --git a/entitysystem/ECScene.h b/entitysystem/ECScene.h new file mode 100644 index 00000000..a723a2d6 --- /dev/null +++ b/entitysystem/ECScene.h @@ -0,0 +1,42 @@ +// +// Created by Daniu +// + +#ifndef EU07_ECSCENE_H +#define EU07_ECSCENE_H + + + +#pragma once + +#include "ECWorld.h" +#include "systems/SystemManager.h" + +class ECScene +{ +public: + ECScene(); + virtual ~ECScene(); + + void Load(); + void Unload(); + void Update(float dt); + + ECWorld& World(); + const ECWorld& World() const; + +protected: + virtual void OnLoad(); + virtual void OnUnload(); + virtual void OnUpdate(float dt); + +protected: + ECWorld m_world; + SystemManager m_systems; + +private: + bool m_loaded; +}; + + +#endif //EU07_ECSCENE_H diff --git a/entitysystem/ECWorld.cpp b/entitysystem/ECWorld.cpp new file mode 100644 index 00000000..48ad91c8 --- /dev/null +++ b/entitysystem/ECWorld.cpp @@ -0,0 +1,54 @@ +// +// Created by Daniu +// + +#include "ECWorld.h" +#include "entitysystem/components/BasicComponents.h" + +entt::entity ECWorld::CreateEntity() +{ + return m_registry.create(); +} + +void ECWorld::DestroyEntity(entt::entity entity) +{ + if (m_registry.valid(entity)) + m_registry.destroy(entity); +} + +void ECWorld::Clear() +{ + m_registry.clear(); +} + +bool ECWorld::IsAlive(entt::entity entity) const +{ + return m_registry.valid(entity); +} + + +entt::entity ECWorld::FindEntityByName(const std::string& name) const +{ + auto view = m_registry.view(); + + for (auto entity : view) + { + const auto& id = view.get(entity); + if (id.Name.ToString() == name) + { + return entity; + } + } + + return entt::null; +} + +entt::registry& ECWorld::Registry() +{ + return m_registry; +} + +const entt::registry& ECWorld::Registry() const +{ + return m_registry; +} diff --git a/entitysystem/ECWorld.h b/entitysystem/ECWorld.h new file mode 100644 index 00000000..f1a8ee11 --- /dev/null +++ b/entitysystem/ECWorld.h @@ -0,0 +1,135 @@ +// +// Created by Daniu +// + +#ifndef EU07_ECWORLD_H +#define EU07_ECWORLD_H + + + +#pragma once + +#include +#include +#include + +class ECWorld +{ +public: + ECWorld() = default; + ~ECWorld() = default; + + entt::entity CreateEntity(); + void DestroyEntity(entt::entity entity); + void Clear(); + + bool IsAlive(entt::entity entity) const; + + entt::registry& Registry(); + const entt::registry& Registry() const; + + template + T& AddComponent(entt::entity entity, Args&&... args); + + template + bool HasComponent(entt::entity entity) const; + + template + T* GetComponent(entt::entity entity); + + template + const T* GetComponent(entt::entity entity) const; + + template + void RemoveComponent(entt::entity entity); + + entt::entity FindEntityByName(const std::string& name) const; + + template + auto View() + { + return m_registry.view(); + } + + template + void Each(Func&& func) + { + auto view = m_registry.view(); + + for (auto entity : view) + { + auto components = std::forward_as_tuple(view.get(entity)...); + + std::apply([&](Components&... comps) + { + func(entity, comps...); + }, components); + } + } + + std::vector GetEntities() const + { + std::vector entities; + auto *storage = m_registry.storage(); + + for (auto entity : *storage) + { + + entities.push_back(entity); + } + return entities; + } + + std::size_t GetEntityCount() const + { + return m_registry.storage()->size(); + } + +private: + entt::registry m_registry; +}; + +template +T& ECWorld::AddComponent(entt::entity entity, Args&&... args) +{ + static_assert(std::is_constructible_v, + "Component must be constructible with provided arguments"); + + if (m_registry.all_of(entity)) + { + return m_registry.replace(entity, std::forward(args)...); + } + + return m_registry.emplace(entity, std::forward(args)...); +} + +template +bool ECWorld::HasComponent(entt::entity entity) const +{ + return m_registry.all_of(entity); +} + +template +T* ECWorld::GetComponent(entt::entity entity) +{ + return m_registry.try_get(entity); +} + +template +const T* ECWorld::GetComponent(entt::entity entity) const +{ + return m_registry.try_get(entity); +} + +template +void ECWorld::RemoveComponent(entt::entity entity) +{ + if (m_registry.all_of(entity)) + m_registry.remove(entity); +} + + + +extern ECWorld& GetComponentSystem(); +#define ECS GetComponentSystem() +#endif //EU07_ECWORLD_H diff --git a/entitysystem/SceneManager.cpp b/entitysystem/SceneManager.cpp new file mode 100644 index 00000000..564e7eec --- /dev/null +++ b/entitysystem/SceneManager.cpp @@ -0,0 +1,59 @@ +// +// Created by Daniu +// + +#include "SceneManager.h" +#include "ECScene.h" + +SceneManager::SceneManager() = default; + +SceneManager::~SceneManager() +{ + if (m_currentScene) + m_currentScene->Unload(); +} + +void SceneManager::SetScene(std::unique_ptr scene) +{ + m_pendingScene = std::move(scene); + if (m_currentScene) + m_currentScene->Load(); +} + +void SceneManager::Update(float dt) +{ + if (m_pendingScene) + SwitchScene(); + + if (m_currentScene){ + m_currentScene->Update(dt); + } + + +} + +void SceneManager::SwitchScene() +{ + if (m_currentScene) + m_currentScene->Unload(); + + m_currentScene = std::move(m_pendingScene); + + if (m_currentScene) + m_currentScene->Load(); +} + +ECScene* SceneManager::CurrentScene() +{ + return m_currentScene.get(); +} + +const ECScene* SceneManager::CurrentScene() const +{ + return m_currentScene.get(); +} + +bool SceneManager::HasScene() const +{ + return m_currentScene != nullptr; +} \ No newline at end of file diff --git a/entitysystem/SceneManager.h b/entitysystem/SceneManager.h new file mode 100644 index 00000000..9a2af564 --- /dev/null +++ b/entitysystem/SceneManager.h @@ -0,0 +1,40 @@ +// +// Created by Daniu +// + +#ifndef EU07_SCENEMANAGER_H +#define EU07_SCENEMANAGER_H + + +#pragma once + +#include +#include + +class ECScene; + +class SceneManager +{ +public: + SceneManager(); + ~SceneManager(); + + void SetScene(std::unique_ptr scene); + void Update(float dt); + + ECScene* CurrentScene(); + const ECScene* CurrentScene() const; + + bool HasScene() const; + +private: + void SwitchScene(); + +private: + std::unique_ptr m_currentScene; + std::unique_ptr m_pendingScene; +}; + + + +#endif //EU07_SCENEMANAGER_H diff --git a/entitysystem/components/BasicComponents.h b/entitysystem/components/BasicComponents.h index 1555d193..60ead447 100644 --- a/entitysystem/components/BasicComponents.h +++ b/entitysystem/components/BasicComponents.h @@ -3,11 +3,14 @@ */ #ifndef EU07_BASICCOMPONENTS_H #define EU07_BASICCOMPONENTS_H +#include "audio/sound.h" #include "entt/entity/entity.hpp" #include "glm/vec3.hpp" #include "glm/gtc/quaternion.hpp" -#include +#include "registry/FName.h" +#include "utils/uuid.hpp" #include +#include namespace ECSComponent { @@ -16,18 +19,23 @@ namespace ECSComponent /// struct Transform { - glm::vec3 Position{0.f}; // object position + glm::dvec3 Position{0.f}; // object position glm::quat Rotation{1.f, 0.f, 0.f, 0.f}; // object rotation glm::vec3 Scale{1.f}; // object scale }; +struct Velocity +{ + glm::vec3 Value{0.f}; // object velocity +}; + ///< summary> /// Basic component for naming entities /// in future for scenery hierarchy /// struct Identification { - std::string Name; // object name - may contain slashes for editor hierarchy "directories" + FName Name; // object name - may contain slashes for editor hierarchy "directories" std::uint64_t Id{0}; // id in scene }; @@ -39,6 +47,16 @@ struct Parent entt::entity value{entt::null}; }; +struct SoundComponent +{ + sound_source sound; // sound source + float volume = 1.0f; // 0–1 + float pitch = 1.0f; // speed / pitch + float range = 1.0f; // range + bool loop = false; + bool playOnStart = false; + bool isPlaying = false; +}; ///< summary> /// Empty component for entities that are disabled and should not be processed by systems /// diff --git a/entitysystem/components/RenderComponents.h b/entitysystem/components/RenderComponents.h index ef8cf7f6..8ae0473e 100644 --- a/entitysystem/components/RenderComponents.h +++ b/entitysystem/components/RenderComponents.h @@ -1,6 +1,9 @@ #ifndef EU07_RENDERCOMPONENTS_H #define EU07_RENDERCOMPONENTS_H +#include "registry/FName.h" +#include "rendering/geometrybank.h" + namespace ECSComponent { /// @@ -11,6 +14,8 @@ namespace ECSComponent /// struct MeshRenderer { + gfx::geometry_handle meshHandle; + bool visible = true; }; /// @@ -19,7 +24,53 @@ struct MeshRenderer /// Currently empty /// TODO: Add component members /// -struct SpotLight{}; +struct SpotLight +{ + glm::vec3 Color{ 1.0f, 1.0f, 1.0f }; + + float Intensity = 1.0f; + float Range = 25.0f; + + float InnerAngle = 15.0f; + float OuterAngle = 25.0f; + + bool CastShadows = false; + bool Enabled = true; +}; + +// TODO: AreaLight like blender +// TODO: SunLight for scene + + +struct ParticleEmitter +{ + FName particleEffectPath = (""); + bool active = true; +}; + +struct Decal +{ + FName decalTexturePath = (""); + float size = 1.0f; + bool active = true; +}; + +struct Billboard +{ + FName texturePath = (""); + float size = 1.0f; + bool active = true; +}; + +struct Line { + glm::vec3 start{ 0.0f }; + glm::vec3 end{ 1.0f, 0.0f, 0.0f }; + glm::vec3 color{ 1.0f, 1.0f, 1.0f }; + float thickness = 1.0f; + bool active = true; +}; + + /// /// Component for entities that can be rendered with LOD diff --git a/entitysystem/ecs.cpp b/entitysystem/ecs.cpp deleted file mode 100644 index 8de653c0..00000000 --- a/entitysystem/ecs.cpp +++ /dev/null @@ -1,69 +0,0 @@ -// -// Created by Hirek on 3/14/2026. -// - -#include "ecs.h" - -void ECS::ClearWorld() -{ - world_.clear(); -} - -entt::entity ECS::CreateObject() -{ - const auto e = world_.create(); - world_.emplace(e); - // add UID - auto id = world_.emplace(e); - id.Id = nextId_++; - return e; -} - -void ECS::DestroyObject(entt::entity Entity) -{ - if (Entity == entt::null) // check if Entity is not null - return; - if (!world_.valid(Entity)) // check if exist - return; - world_.destroy(Entity); -} - -bool ECS::ValidObject(entt::entity entity) const -{ - return entity != entt::null && world_.valid(entity); -} - -entt::entity ECS::FindById(std::uint64_t id) -{ - auto view = world_.view(); - for (auto e : view) - { - const auto &ident = view.get(e); - if (ident.Id == id) - return e; - } - return entt::null; -} - -std::vector ECS::FindByName(std::string_view name) -{ - std::vector result; - - auto view = world_.view(); - for (auto e : view) - { - const auto &ident = view.get(e); - if (ident.Name == name) - { - result.push_back(e); - } - } - - return result; -} - -ECS &GetComponentSystem() -{ - static ECS _ecs; - return _ecs; -} \ No newline at end of file diff --git a/entitysystem/ecs.h b/entitysystem/ecs.h deleted file mode 100644 index 76f15e7f..00000000 --- a/entitysystem/ecs.h +++ /dev/null @@ -1,153 +0,0 @@ -// -// Created by Hirek on 3/14/2026. -// - -#ifndef EU07_ECS_H -#define EU07_ECS_H -#include "components/BasicComponents.h" -#include "entt/entity/registry.hpp" -#include -#include -#include -#include -class ECS final -{ - private: - entt::registry world_; // scene registry - std::uint64_t nextId_ = 1; // 0 is invalid/none - - - public: - // - // World lifecycle - // - - /// - /// Clears the world, removing all entities and components - /// - void ClearWorld(); - - // - // Objects - // - - /// - /// Creates new object with basic components (Transform, Identification) and returns its entity handle. - /// - /// Entity handle of created object - entt::entity CreateObject(); - - /// - /// Destroys object with it's all components - /// - /// Entity to be removed - void DestroyObject(entt::entity Entity); - - /// - /// Checks if object with given entity handle exists in the registry - /// - /// Entity handle to check - /// true if object exists, false otherwise - bool ValidObject(entt::entity entity) const; - - // - // Identification lookups - // - - /// - /// Finds object with given UID. Returns null entity if not found. - /// - /// UID of object - entt::entity FindById(std::uint64_t id); - - /// - /// Finds objects with given name. Returns empty vector if not found. - /// - /// Name of object - /// May return more than 1 as many objects can have the same name - std::vector FindByName(std::string_view name); - - // - // Components - // - - /// - /// Adds component of type T to entity, forwarding provided arguments to component constructor. If component already exists, it will be replaced. - /// - /// Component type - /// Entity to which component will be added - /// Arguments forwarded to component constructor - /// Reference to added component - template T &AddComponent(entt::entity entity, Args &&...args) - { - return world_.emplace(entity, std::forward(args)...); - } - - /// - /// Returns entity's component - /// - /// type - /// Entity to which component will be added - /// Reference to added component - template T &GetComponent(entt::entity entity) - { - return world_.get(entity); - } - - /// - /// Returns entity's component - /// - /// type - /// Entity to which component will be added - /// Reference to added component - template const T &GetComponent(entt::entity entity) const - { - return world_.get(entity); - } - - /// - /// Tries to get component of type T from entity. Returns nullptr if component does not exist. - /// - /// Component type - /// Entity from which component will be retrieved - /// Pointer to component if exists, nullptr otherwise - template T *TryGetComponent(entt::entity entity) - { - return world_.try_get(entity); - } - - /// - /// Tries to get component of type T from entity. Returns nullptr if component does not exist. - /// - /// Component type - /// Entity from which component will be retrieved - /// Pointer to component if exists, nullptr otherwise - template const T *TryGetComponent(entt::entity entity) const - { - return world_.try_get(entity); - } - - /// - /// Checks if entity has component of type T. - /// - /// Component type - /// Entity to check - /// true if entity has component, false otherwise - template bool HasComponent(entt::entity entity) const - { - return world_.all_of(entity); - } - - /// - /// Removes component of type T from entity. Does nothing if component does not exist. - /// Component type - /// Entity from which component will be removed - template void RemoveComponent(entt::entity entity) - { - world_.remove(entity); - } -}; - -extern ECS& GetComponentSystem(); -#define CS GetComponentSystem() -#endif // EU07_ECS_H diff --git a/entitysystem/scenes/GameScene.cpp b/entitysystem/scenes/GameScene.cpp new file mode 100644 index 00000000..711f124d --- /dev/null +++ b/entitysystem/scenes/GameScene.cpp @@ -0,0 +1,35 @@ +// +// Created by Daniu +// + +#include "GameScene.h" + + +#include "entitysystem/components/BasicComponents.h" +#include "entitysystem/components/RenderComponents.h" +#include "entitysystem/systems/MovementSystem.h" +#include "utilities/Logs.h" + +GameScene::GameScene() = default; +GameScene::~GameScene() = default ; + +void GameScene::OnLoad() +{ + m_systems.AddSystem(); + +} + +void GameScene::OnUpdate(float dt) +{ + + //auto& world = World(); + + //auto view = world.View(); + + +} + +void GameScene::OnUnload() +{ + World().Clear(); +} \ No newline at end of file diff --git a/entitysystem/scenes/GameScene.h b/entitysystem/scenes/GameScene.h new file mode 100644 index 00000000..fe90acab --- /dev/null +++ b/entitysystem/scenes/GameScene.h @@ -0,0 +1,30 @@ +// +// Created by Daniu +// + +#ifndef EU07_GAMESCENE_H +#define EU07_GAMESCENE_H + + + +#pragma once + +#include "entitysystem/ECScene.h" + +class GameScene final : public ECScene +{ +public: + GameScene(); + ~GameScene() override; + +private: + void OnLoad() override; + void OnUnload() override; + void OnUpdate(float dt) override; + +private: + void CreateTestEntities(); +}; + + +#endif //EU07_GAMESCENE_H diff --git a/entitysystem/systems/BaseSystem.cpp b/entitysystem/systems/BaseSystem.cpp new file mode 100644 index 00000000..229a53e2 --- /dev/null +++ b/entitysystem/systems/BaseSystem.cpp @@ -0,0 +1,5 @@ +// +// Created by Daniu +// + +#include "BaseSystem.h" diff --git a/entitysystem/systems/BaseSystem.h b/entitysystem/systems/BaseSystem.h new file mode 100644 index 00000000..4587af5b --- /dev/null +++ b/entitysystem/systems/BaseSystem.h @@ -0,0 +1,25 @@ +// +// Created by Daniu +// + +#ifndef EU07_BASICSYSTEM_H +#define EU07_BASICSYSTEM_H + + +#pragma once + +class ECWorld; + +class BaseSystem +{ +public: + virtual ~BaseSystem() = default; + + virtual void OnCreate(ECWorld& world) {} + virtual void OnDestroy(ECWorld& world) {} + + virtual void Update(ECWorld& world, float dt) {} +}; + + +#endif //EU07_BASICSYSTEM_H diff --git a/entitysystem/systems/MovementSystem.cpp b/entitysystem/systems/MovementSystem.cpp new file mode 100644 index 00000000..6fe3bf61 --- /dev/null +++ b/entitysystem/systems/MovementSystem.cpp @@ -0,0 +1,29 @@ +// +// Created by Daniu +// + +#include "MovementSystem.h" +#include "entitysystem/ECWorld.h" +#include "entitysystem/components/BasicComponents.h" +#include "utilities/Logs.h" + + +void MovementSystem::Update(ECWorld& world, float dt) +{ + constexpr double damping = 0.90; // 0.0 = instant stop, 1.0 = without damping + + world.Each( + [dt](auto entity, auto& transform, auto& velocity) + { + + transform.Position += velocity.Value * dt; + + velocity.Value *= damping; + + if (glm::length(velocity.Value) < 0.001) + { + velocity.Value = glm::dvec3(0.0); + } + } + ); +} \ No newline at end of file diff --git a/entitysystem/systems/MovementSystem.h b/entitysystem/systems/MovementSystem.h new file mode 100644 index 00000000..bffb824c --- /dev/null +++ b/entitysystem/systems/MovementSystem.h @@ -0,0 +1,25 @@ +// +// Created by Daniu +// + +#ifndef EU07_MOVEMENTSYSTEM_H +#define EU07_MOVEMENTSYSTEM_H + + +#pragma once +#include "BaseSystem.h" + +namespace ECSComponent +{ +struct Transform; +struct Velocity; +} + +class MovementSystem : public BaseSystem +{ +public: + void Update(ECWorld& world, float dt) override; +}; + + +#endif //EU07_MOVEMENTSYSTEM_H diff --git a/entitysystem/systems/SystemManager.cpp b/entitysystem/systems/SystemManager.cpp new file mode 100644 index 00000000..35d7cd51 --- /dev/null +++ b/entitysystem/systems/SystemManager.cpp @@ -0,0 +1,33 @@ +// +// Created by Daniu +// + +#include "SystemManager.h" +#include "entitysystem/systems/BaseSystem.h" +#include "entitysystem/ECWorld.h" +#include "utilities/Logs.h" + +SystemManager::~SystemManager() = default; + +void SystemManager::Create(ECWorld& world) +{ + WriteLog("[SystemManager] Creating systems"); + for (auto& system : m_systems){ + system->OnCreate(world); + } + +} + +void SystemManager::Destroy(ECWorld& world) +{ + WriteLog("[SystemManager] Destroying systems"); + for (auto& system : m_systems) + system->OnDestroy(world); +} + +void SystemManager::Update(ECWorld& world, float dt) +{ + + for (auto& system : m_systems) + system->Update(world, dt); +} diff --git a/entitysystem/systems/SystemManager.h b/entitysystem/systems/SystemManager.h new file mode 100644 index 00000000..fd26be3e --- /dev/null +++ b/entitysystem/systems/SystemManager.h @@ -0,0 +1,46 @@ +// +// Created by Daniu +// + +#ifndef EU07_SYSTEMMANAGER_H +#define EU07_SYSTEMMANAGER_H + + + +#pragma once + +#include "BaseSystem.h" + +#include +#include + +class ECWorld; + +class SystemManager +{ +public: + SystemManager() = default; + ~SystemManager(); + + template + T& AddSystem(Args&&... args) + { + static_assert(std::is_base_of_v); + auto system = std::make_unique(std::forward(args)...); + T& ref = *system; + m_systems.emplace_back(std::move(system)); + return ref; + } + + void Create(ECWorld& world); + void Destroy(ECWorld& world); + + void Update(ECWorld& world, float dt); + +private: + std::vector> m_systems; +}; + + + +#endif //EU07_SYSTEMMANAGER_H diff --git a/registry/FName.h b/registry/FName.h new file mode 100644 index 00000000..22b36b87 --- /dev/null +++ b/registry/FName.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include "NameRegistry.h" + +struct FName +{ + using value_type = uint64_t; + + static constexpr value_type INVALID_ID = 0; + + value_type id = INVALID_ID; + + constexpr FName() noexcept = default; + + constexpr explicit FName(value_type value) noexcept + : id(value) + { + } + + FName(const char* text) + : id(text ? NameRegistry::GetOrCreate(text) : INVALID_ID) + { + } + + FName(const std::string& text) + : id(text.empty() ? INVALID_ID : NameRegistry::GetOrCreate(text)) + { + } + + FName(std::string_view text) + : id(text.empty() ? INVALID_ID : NameRegistry::GetOrCreate(text)) + { + } + + constexpr bool IsValid() const noexcept + { + return id != INVALID_ID; + } + + constexpr value_type GetId() const noexcept + { + return id; + } + + const std::string& ToString() const + { + return NameRegistry::GetString(id); + } + + constexpr bool operator==(const FName& other) const noexcept + { + return id == other.id; + } + + constexpr bool operator!=(const FName& other) const noexcept + { + return id != other.id; + } +}; \ No newline at end of file diff --git a/registry/NameHash.h b/registry/NameHash.h new file mode 100644 index 00000000..bff47a2e --- /dev/null +++ b/registry/NameHash.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +constexpr uint64_t FNV1A_OFFSET_BASIS_64 = 14695981039346656037ull; +constexpr uint64_t FNV1A_PRIME_64 = 1099511628211ull; + +constexpr uint64_t HashLiteral(const char* str, std::size_t len) noexcept +{ + uint64_t hash = FNV1A_OFFSET_BASIS_64; + for (std::size_t i = 0; i < len; ++i) + { + hash ^= static_cast(str[i]); + hash *= FNV1A_PRIME_64; + } + return hash; +} + +constexpr uint64_t HashLiteral(const char* str) noexcept +{ + uint64_t hash = FNV1A_OFFSET_BASIS_64; + while (*str) + { + hash ^= static_cast(*str++); + hash *= FNV1A_PRIME_64; + } + return hash; +} + +inline uint64_t HashRuntime(std::string_view str) noexcept +{ + uint64_t hash = FNV1A_OFFSET_BASIS_64; + for (char c : str) + { + hash ^= static_cast(c); + hash *= FNV1A_PRIME_64; + } + return hash; +} \ No newline at end of file diff --git a/registry/NameLiteral.h b/registry/NameLiteral.h new file mode 100644 index 00000000..58b01d23 --- /dev/null +++ b/registry/NameLiteral.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "FName.h" +#include "NameHash.h" + +consteval FName operator"" _name(const char* str, std::size_t len) +{ + return FName{ HashLiteral(str, len) }; +} \ No newline at end of file diff --git a/registry/NameRegistry.cpp b/registry/NameRegistry.cpp new file mode 100644 index 00000000..9daf0744 --- /dev/null +++ b/registry/NameRegistry.cpp @@ -0,0 +1,52 @@ +#include "NameRegistry.h" +#include "NameHash.h" + +#include + +uint64_t NameRegistry::GetOrCreate(std::string_view name) +{ + if (name.empty()) + return 0; + + const uint64_t hash = HashRuntime(name); + + std::lock_guard lock(GetMutex()); + auto& map = GetMap(); + + auto [it, inserted] = map.emplace(hash, std::string(name)); + if (!inserted) + { + assert(it->second == name && "Name hash collision detected"); + } + + return hash; +} + +const std::string& NameRegistry::GetString(uint64_t id) +{ + static const std::string Empty; + + if (id == 0) + return Empty; + + std::lock_guard lock(GetMutex()); + auto& map = GetMap(); + + auto it = map.find(id); + if (it != map.end()) + return it->second; + + return Empty; +} + +std::unordered_map& NameRegistry::GetMap() +{ + static std::unordered_map map; + return map; +} + +std::mutex& NameRegistry::GetMutex() +{ + static std::mutex mutex; + return mutex; +} \ No newline at end of file diff --git a/registry/NameRegistry.h b/registry/NameRegistry.h new file mode 100644 index 00000000..f9961d29 --- /dev/null +++ b/registry/NameRegistry.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include +#include + +class NameRegistry +{ +public: + static uint64_t GetOrCreate(std::string_view name); + static const std::string& GetString(uint64_t id); + +private: + static std::unordered_map& GetMap(); + static std::mutex& GetMutex(); +}; \ No newline at end of file diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 0a694336..a8a9342c 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -25,7 +25,8 @@ http://mozilla.org/MPL/2.0/. #include "rendering/lightarray.h" #include "world/TractionPower.h" #include "application/application.h" -#include "entitysystem/ecs.h" +#include "entitysystem/ECScene.h" +#include "entitysystem/components/BasicComponents.h" #include "entitysystem/components/RenderComponents.h" #include "rendering/renderer.h" #include "utilities/Logs.h" @@ -429,17 +430,18 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch >> nodedata.name >> nodedata.type; - // Add node to ComponentSystem - entt::entity CSEntity = CS.CreateObject(); - - auto nodeTransform = CS.GetComponent(CSEntity); - nodeTransform.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top(); - nodeTransform.Rotation = Scratchpad.location.rotation; + ECWorld& world = Application.sceneManager.CurrentScene()->World(); + + entt::entity entity = world.CreateEntity(); + + auto transformComponent = world.AddComponent(entity); + transformComponent.Position = Scratchpad.location.offset.empty() ? glm::dvec3(0.0) : Scratchpad.location.offset.top(); + transformComponent.Rotation = Scratchpad.location.rotation; //nodeTransform.Scale = Scratchpad.location.scale; - auto lodController = CS.AddComponent(CSEntity); - lodController.RangeMax = nodedata.range_max; - lodController.RangeMin = nodedata.range_min; + // auto LODComponent = world.AddComponent(entity); + // LODComponent.RangeMax = nodedata.range_max; + // LODComponent.RangeMin = nodedata.range_min; if( nodedata.name == "none" ) { @@ -447,11 +449,12 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } else { - CS.GetComponent(CSEntity).Name = nodedata.name; + world.AddComponent(entity).Name = nodedata.name; + } // type-based deserialization. not elegant but it'll do if( nodedata.type == "dynamic" ) { - + auto *vehicle { deserialize_dynamic( Input, Scratchpad, nodedata ) }; // vehicle import can potentially fail if( vehicle == nullptr ) { return; } @@ -471,6 +474,9 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch ErrorLog( "Bad scenario: duplicate vehicle name \"" + vehicle->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); } + auto velocityComponent = world.AddComponent(entity); + velocityComponent.Value = glm::dvec3(0.0); + if( ( vehicle->MoverParameters->CategoryFlag == 1 ) // trains only && ( ( ( vehicle->LightList( end::front ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) || ( ( vehicle->LightList( end::rear ) & ( light::headlight_left | light::headlight_right | light::headlight_upper ) ) != 0 ) ) ) { @@ -572,7 +578,25 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } scene::Groups.insert( scene::Groups.handle(), instance ); simulation::Region->insert( instance ); + + ECWorld& world = Application.sceneManager.CurrentScene()->World(); + + entt::entity entity = world.FindEntityByName(instance->name()); + + if(entity != entt::null) { + + auto meshComponent = world.AddComponent(entity); + //meshComponent.meshHandle = instance->Model()->MeshHandle(); + meshComponent.visible = true; + + auto* transformComponent = world.GetComponent(entity); + transformComponent->Position = {instance->location().x, instance->location().y, instance->location().z}; + transformComponent->Rotation = {instance->Angles().x, instance->Angles().y, instance->Angles().z, 1.0f}; + //transformComponent->Scale = {instance->scale().x, instance->scale().y, instance->scale().z}; + } + scene::basic_node *hierarchy_node = instance; + if (hierarchy_node) { scene::Hierarchy[hierarchy_node->uuid.to_string()] = hierarchy_node; } @@ -650,6 +674,15 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch else if( nodedata.type == "sound" ) { auto *sound { deserialize_sound( Input, Scratchpad, nodedata ) }; + + auto soundComponent = world.AddComponent(entity); + + soundComponent.sound = *sound; + soundComponent.volume = 1; + soundComponent.pitch = 1; + soundComponent.range = sound->range(); + soundComponent.isPlaying = sound->is_playing(); + if( false == simulation::Sounds.insert( sound ) ) { ErrorLog( "Bad scenario: duplicate sound node name \"" + sound->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); }