Component workflow

This commit is contained in:
2026-08-15 06:52:14 +03:00
parent 2d60b20cca
commit c38ac8eff3
17 changed files with 523 additions and 517 deletions
+11 -3
View File
@@ -196,15 +196,23 @@ restored to the spawner name so the character respawns correctly.
## Adding a New Component
1. Define the component struct in `components/MyComponent.hpp`.
2. Register it for UI editing in `components/MyComponentModule.cpp` using
`REGISTER_COMPONENT_GROUP(...)` and `registry.registerComponent<T>(...)`.
2. Create `components/MyComponentModule.cpp` with a
`REGISTER_COMPONENT_GROUP(...)` block that calls
`registry.registerComponent<T>(name, group, editor, adder, remover,
onModified, order)`. The registry builds a type-erased renderer from this,
so the property panel and Add/Remove menus pick it up automatically — no
changes to `EditorUISystem.cpp` are required.
3. Implement an editor in `ui/MyComponentEditor.hpp` / `.cpp` deriving from
`ComponentEditor`.
4. If the component should be saved/loaded, add serialization support in
`systems/SceneSerializer.cpp`.
`systems/SceneSerializer.cpp` (see `ComponentArchitectureImprovement.md`,
step C, for the planned registry-driven serializer).
5. Add a Lua binding in `lua/LuaComponentApi.cpp` if needed.
6. Add a component-module test case in `tests/component_lua_test.cpp`.
See `ComponentArchitectureImprovement.md` for the long-term plan to remove the
remaining per-component hardcoding in serialization, lifecycle hooks and Lua.
## Adding a New System
1. Create `systems/MySystem.hpp` and `systems/MySystem.cpp`.
+6
View File
@@ -168,6 +168,12 @@ set(EDITSCENE_SOURCES
components/TerrainModule.cpp
components/SunModule.cpp
components/SkyboxModule.cpp
components/TransformModule.cpp
components/RenderableModule.cpp
components/RigidBodyModule.cpp
components/PhysicsColliderModule.cpp
components/PrefabInstanceModule.cpp
components/CharacterIdentityModule.cpp
camera/EditorCamera.cpp
gizmo/Gizmo.cpp
gizmo/Cursor3D.cpp
@@ -0,0 +1,256 @@
# Component Architecture Improvement — Implementation Specification
This document is the working specification for removing the remaining
per-component hardcoding in `src/features/editScene`. It records what has
already been implemented and, step by step, what remains.
## Goal
Adding a new component to the editor should require **one** place to describe
the component (name, group, editor, add/remove, render, serialize, Lua) instead
of editing six or seven files with scattered special-case code.
## Status
| Step | Description | Status |
|------|-------------|--------|
| A | Type-erased editor renderer (`ComponentRenderer`) | Done |
| B | Unified registration (extract hardcoded components into `*Module.cpp`) | Done |
| C | Serialization hooks (drive `SceneSerializer` from the registry) | To do |
| D | Lifecycle hooks (hierarchy badges, delete/duplicate cleanup) | To do |
| E | Unified Lua component table (single source of truth) | To do |
| F | Flecs meta reflection for auto-generated editors/serde (optional) | To do |
---
## What is already done (A + B)
### A — Type-erased renderer
`ui/ComponentRegistry.hpp` now stores a `ComponentRenderer` closure per
component in `ComponentInfo`:
```cpp
struct ComponentInfo {
const char *name;
const char *group;
int order = 0; // lower renders first; Transform = -1
std::unique_ptr<IComponentEditor> editor;
ComponentRenderer renderer; // type-erased render
ComponentAdder adder;
ComponentRemover remover;
ComponentChecker checker;
ComponentModified onModified; // optional post-render side effect
};
```
`registerComponent<T>()` builds `renderer` from the (templated) type, handling
empty/tag components via `std::is_empty_v<T>` so `get_mut<T>()` is never called
on a zero-sized type.
`EditorUISystem::renderComponentList()` is now a generic loop: it collects the
registered components present on the entity, sorts them by `(order, name)`, and
invokes each `renderer`; if the renderer returns `true` it also runs the
optional `onModified` hook.
### B — Unified registration
The six previously hardcoded components were extracted into module files that
use the same `REGISTER_COMPONENT_GROUP` mechanism as every other component:
- `components/TransformModule.cpp`
- `components/RenderableModule.cpp`
- `components/RigidBodyModule.cpp`
- `components/PhysicsColliderModule.cpp`
- `components/PrefabInstanceModule.cpp`
- `components/CharacterIdentityModule.cpp`
`EditorUISystem::registerComponentEditors()` was removed; the constructor now
calls `registerModularComponents()` directly, which invokes
`ComponentRegistration::registerAll()`.
Cross-component post-render side effects were moved into `onModified` hooks
co-located with each component's registration:
- Transform → marks `StaticGeometryMemberComponent` dirty.
- Sun / Skybox / WaterPlane / Room / ClearArea → `markDirty()`.
- NavMesh → syncs `debugDraw` with `NavMeshSystem`.
---
## Suggestion C — Serialization hooks
**Problem.** `systems/SceneSerializer.cpp` (~4366 lines) hardcodes a
`serializeXxx`/`deserializeXxx` method pair per component plus two giant
`if (entity.has<T>())` / `if (json.contains("key"))` dispatch chains in
`serializeEntity()` and `deserializeEntityComponents()`. Adding a component
today still requires edits to the serializer.
**Design.** Extend `ComponentInfo` (or a parallel serde table) with two closures
and drive the serializer from the registry:
```cpp
using ComponentSerialize = std::function<nlohmann::json(flecs::entity)>;
using ComponentDeserialize = std::function<void(flecs::entity, const nlohmann::json&)>;
struct ComponentInfo {
...
ComponentSerialize serialize;
ComponentDeserialize deserialize;
ComponentModified postLoad; // second-pass reference resolution
};
```
The serializer then:
1. In `serializeEntity`, iterate registered components, call `serialize(e)` for
those that have it, and write the result under a per-component JSON key.
2. In `deserializeEntityComponents`, for each key in the entity JSON that
matches a registered component, call `deserialize(e, value)`.
3. Keep the two-pass reference resolution (material references,
procedural-texture references, character spawner) as `postLoad` hooks that
run after all entities are loaded.
**Steps.**
1. Add the serde type aliases + fields to `ComponentRegistry.hpp` (keep the
`nlohmann::json` dependency out of the header if possible; prefer a
`void*`/`std::function` boundary or move the serde table into
`SceneSerializer` so the registry stays UI-only).
2. Migrate simple components first (Transform, Renderable, RigidBody,
Collider, Light, Camera, Lod, etc.) — move their `serialize/deserialize`
bodies from `SceneSerializer.cpp` into their `*Module.cpp` registration.
3. Leave complex components (Town/District/Lot, ProceduralMaterial,
CharacterSpawner, NavMesh) as explicit overrides initially; the registry is
a fallback. Then migrate them one at a time, adding `postLoad` where the
current two-pass logic lives.
4. Remove the now-dead `serializeXxx`/`deserializeXxx` declarations from
`SceneSerializer.hpp` and their dispatch blocks from `.cpp`.
**Acceptance criteria.**
- `serializeEntity` / `deserializeEntityComponents` contain no
`if (entity.has<T>())` component dispatch (or only a tiny, documented set of
explicit overrides for two-pass components).
- Round-trip save/load tests still pass (see `tests/` and
`docs/SaveLoadSystem.md`).
- Adding a new serializable component touches only its `*Module.cpp`.
---
## Suggestion D — Lifecycle hooks
**Problem.** Three UI hot-spots still enumerate components by hand:
1. `EditorUISystem::renderEntityNode()` — ~26 hardcoded hierarchy badge lines
(`indicators += " [T]"`).
2. `EditorUISystem::deleteEntity()` — hardcoded cleanup for RigidBody
(physics body), Transform (scene node), Renderable (Ogre entity).
3. `EditorUISystem::duplicateEntity()` — hardcoded copy logic for name,
transform, renderable.
**Design.** Add nullable hooks to `ComponentInfo` (following the `onModified`
pattern already established):
```cpp
struct ComponentInfo {
...
std::function<std::string(flecs::entity)> badge; // hierarchy indicator
std::function<void(flecs::entity)> onDelete; // cleanup on delete
std::function<void(flecs::entity, flecs::entity)> onDuplicate; // copy
};
```
**Steps.**
1. Add the fields + optional parameters to `registerComponent<T>()` (or a
post-registration setter to avoid growing the signature further).
2. Move the badge strings into each `*Module.cpp` (Transform `[T]`, Renderable
`[R]`, Light `[L]`, …). `renderEntityNode` then appends
`info.badge(entity)` for each component the entity has.
3. Move `deleteEntity` cleanup into `onDelete` hooks (RigidBody removes its
physics body, Transform destroys its scene node, Renderable destroys its
Ogre entity). `deleteEntity` becomes a generic "invoke onDelete for every
registered component present, then destruct".
4. Move `duplicateEntity` copy logic into `onDuplicate` hooks, or — for most
components — a generic `entity.set<T>(other.get<T>())` copy that falls back
to a hook for components with non-trivial ownership.
**Acceptance criteria.**
- `renderEntityNode`, `deleteEntity`, `duplicateEntity` contain no
`if (entity.has<T>())` component dispatch.
- Deleting/duplicating an entity with a renderable + rigid body + transform
behaves identically to before.
---
## Suggestion E — Unified Lua component table
**Problem.** `lua/LuaComponentApi.cpp` maintains its own `s_components` map and
its own `REGISTER_COMPONENT` macro with hand-written push/read field lambdas —
a third independent list of components.
**Design.** Make one shared descriptor the single source of truth. The editor
registry, the serializer (C) and the Lua binder (E) all read name/group/fields
from it.
**Steps.**
1. Introduce a `ComponentTraits<T>` (or `ComponentDescriptor`) struct in a
neutral header (not UI-, not Lua-specific) holding at least `name`, `group`,
and optional `luaPush` / `luaRead` closures (or field-table reflection).
2. Have the `REGISTER_COMPONENT_GROUP` macro (or a new unified macro) populate
both the `ComponentRegistry` entry and the Lua `s_components` entry from the
same traits.
3. Migrate the existing hand-written `REGISTER_COMPONENT(...)` entries in
`LuaComponentApi.cpp` to the shared traits one component at a time, keeping
`lua-examples/` and `tests/*_lua_test.cpp` green.
**Acceptance criteria.**
- There is exactly one place that lists a component's name/group/serde/Lua
behaviour.
- `component_lua_test` and the other `*_lua_test` executables still pass.
---
## Suggestion F — Flecs meta reflection (optional, long term)
**Problem.** Even after C/D/E, "dumb data" components still need a hand-written
editor + serializer + Lua binding.
**Design.** Register plain-data components with Flecs meta (`flecs::meta`) and
generate the ImGui editor, JSON serde and Lua accessor from the reflected
fields. Keep custom editors only for behaviour-heavy components (Camera
preview, NavMesh, CharacterSlots, etc.).
**Steps.**
1. Add `flecs::meta` to the project and register a pilot component with it.
2. Write a generic `MetaComponentEditor<T>` that walks `flecs::meta` cursor
fields and renders an ImGui widget per field.
3. Write a generic meta-based serializer (or use the built-in `flecs::json`
exporter) and a meta-based Lua binding.
4. Migrate the simplest components; validate against existing round-trip tests.
**Acceptance criteria.**
- A new plain-data component can be added without writing an editor, a
serializer, or a Lua binding.
- OGRE math types (`Ogre::Vector3`, `Quaternion`, `ColourValue`) have registered
meta type handlers so they render and serialize correctly.
---
## Conventions and caveats
- Keep `order` on `ComponentInfo` for deterministic render order; only Transform
needs a non-zero value today. Do not rely on `unordered_map` iteration order.
- Empty/tag components must render via a stack-local dummy (handled by
`std::is_empty_v` in the renderer). Do not call `get_mut<T>()` on them.
- Two-pass scene load (material/character/procedural-texture references) must
stay explicit; fold it into a `postLoad` hook, never into a single pass.
- The source code is the source of truth. Update `AGENTS.md` ("Adding a New
Component"), `docs/`, and `lua-examples/` whenever the registration API
changes.
@@ -120,6 +120,10 @@ REGISTER_COMPONENT_GROUP("Room", "Room Layout", RoomComponent, RoomEditor)
if (e.has<RoomComponent>()) {
e.remove<RoomComponent>();
}
},
// On-modified: mark dirty so RoomLayoutSystem rebuilds the room
[](flecs::entity e) {
e.get_mut<RoomComponent>().markDirty();
}
);
}
@@ -158,6 +162,10 @@ REGISTER_COMPONENT_GROUP("Clear Area", "Room Layout", ClearAreaComponent, ClearA
if (e.has<ClearAreaComponent>()) {
e.remove<ClearAreaComponent>();
}
},
// On-modified: mark dirty so RoomLayoutSystem rebuilds the clear area
[](flecs::entity e) {
e.get_mut<ClearAreaComponent>().markDirty();
}
);
}
@@ -0,0 +1,23 @@
#include "CharacterIdentity.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/CharacterIdentityEditor.hpp"
// Register CharacterIdentity component
REGISTER_COMPONENT_GROUP("Character Identity", "Character",
CharacterIdentityComponent, CharacterIdentityEditor)
{
registry.registerComponent<CharacterIdentityComponent>(
"Character Identity", "Character",
std::make_unique<CharacterIdentityEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<CharacterIdentityComponent>())
e.set<CharacterIdentityComponent>(
CharacterIdentityComponent{});
},
// Remover
[](flecs::entity e) {
if (e.has<CharacterIdentityComponent>())
e.remove<CharacterIdentityComponent>();
});
}
@@ -2,6 +2,7 @@
#include "../ui/ComponentRegistration.hpp"
#include "../ui/NavMeshEditor.hpp"
#include "../ui/NavMeshGeometrySourceEditor.hpp"
#include "../systems/NavMeshSystem.hpp"
REGISTER_COMPONENT_GROUP("NavMesh", "Navigation", NavMeshComponent,
NavMeshEditor)
@@ -18,6 +19,13 @@ REGISTER_COMPONENT_GROUP("NavMesh", "Navigation", NavMeshComponent,
[](flecs::entity e) {
if (e.has<NavMeshComponent>())
e.remove<NavMeshComponent>();
},
// On-modified: sync the debug-draw toggle with the NavMesh system
[](flecs::entity e) {
auto &nav = e.get_mut<NavMeshComponent>();
if (nav.debugDraw && NavMeshSystem::getInstance())
NavMeshSystem::getInstance()->setDebugDraw(e,
nav.debugDraw);
});
}
@@ -0,0 +1,22 @@
#include "PhysicsCollider.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/PhysicsColliderEditor.hpp"
// Register PhysicsCollider component
REGISTER_COMPONENT_GROUP("Physics Collider", "Physics",
PhysicsColliderComponent, PhysicsColliderEditor)
{
registry.registerComponent<PhysicsColliderComponent>(
"Physics Collider", "Physics",
std::make_unique<PhysicsColliderEditor>(sceneMgr),
// Adder
[](flecs::entity e) {
if (!e.has<PhysicsColliderComponent>())
e.set<PhysicsColliderComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<PhysicsColliderComponent>())
e.remove<PhysicsColliderComponent>();
});
}
@@ -0,0 +1,21 @@
#include "PrefabInstance.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/PrefabInstanceEditor.hpp"
// Register PrefabInstance component
REGISTER_COMPONENT_GROUP("Prefab Instance", "Scene", PrefabInstanceComponent,
PrefabInstanceEditor)
{
registry.registerComponent<PrefabInstanceComponent>(
"Prefab Instance", "Scene", std::make_unique<PrefabInstanceEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<PrefabInstanceComponent>())
e.set<PrefabInstanceComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<PrefabInstanceComponent>())
e.remove<PrefabInstanceComponent>();
});
}
@@ -0,0 +1,28 @@
#include "Renderable.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/RenderableEditor.hpp"
// Register Renderable component
REGISTER_COMPONENT_GROUP("Renderable", "Rendering", RenderableComponent,
RenderableEditor)
{
registry.registerComponent<RenderableComponent>(
"Renderable", "Rendering",
std::make_unique<RenderableEditor>(sceneMgr),
// Adder
[](flecs::entity e) {
if (!e.has<RenderableComponent>())
e.set<RenderableComponent>({});
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<RenderableComponent>()) {
auto &renderable = e.get_mut<RenderableComponent>();
if (renderable.entity) {
sceneMgr->destroyEntity(renderable.entity);
renderable.entity = nullptr;
}
e.remove<RenderableComponent>();
}
});
}
@@ -0,0 +1,21 @@
#include "RigidBody.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/RigidBodyEditor.hpp"
// Register RigidBody component
REGISTER_COMPONENT_GROUP("Rigid Body", "Physics", RigidBodyComponent,
RigidBodyEditor)
{
registry.registerComponent<RigidBodyComponent>(
"Rigid Body", "Physics", std::make_unique<RigidBodyEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<RigidBodyComponent>())
e.set<RigidBodyComponent>({});
},
// Remover
[](flecs::entity e) {
if (e.has<RigidBodyComponent>())
e.remove<RigidBodyComponent>();
});
}
@@ -20,5 +20,9 @@ REGISTER_COMPONENT_GROUP("Skybox", "Environment", SkyboxComponent, SkyboxEditor)
if (e.has<SkyboxComponent>()) {
e.remove<SkyboxComponent>();
}
},
// On-modified: mark dirty so EditorSkyboxSystem rebuilds the skybox
[](flecs::entity e) {
e.get_mut<SkyboxComponent>().markDirty();
});
}
@@ -20,5 +20,9 @@ REGISTER_COMPONENT_GROUP("Sun", "Environment", SunComponent, SunEditor)
if (e.has<SunComponent>()) {
e.remove<SunComponent>();
}
},
// On-modified: mark dirty so EditorSunSystem rebuilds light/material
[](flecs::entity e) {
e.get_mut<SunComponent>().markDirty();
});
}
@@ -0,0 +1,42 @@
#include "Transform.hpp"
#include "StaticGeometryMember.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/TransformEditor.hpp"
// Register Transform component
REGISTER_COMPONENT_GROUP("Transform", "Transform", TransformComponent,
TransformEditor)
{
registry.registerComponent<TransformComponent>(
"Transform", "Transform", std::make_unique<TransformEditor>(),
// Adder
[sceneMgr](flecs::entity e) {
if (!e.has<TransformComponent>()) {
TransformComponent transform;
transform.node = sceneMgr->getRootSceneNode()
->createChildSceneNode();
transform.position = Ogre::Vector3::ZERO;
transform.rotation = Ogre::Quaternion::IDENTITY;
transform.scale = Ogre::Vector3::UNIT_SCALE;
e.set<TransformComponent>(transform);
}
},
// Remover
[sceneMgr](flecs::entity e) {
if (e.has<TransformComponent>()) {
auto &transform = e.get_mut<TransformComponent>();
if (transform.node) {
sceneMgr->destroySceneNode(transform.node);
transform.node = nullptr;
}
e.remove<TransformComponent>();
}
},
// On-modified: keep a StaticGeometryMember in sync with its transform
[](flecs::entity e) {
if (e.has<StaticGeometryMemberComponent>())
e.get_mut<StaticGeometryMemberComponent>().markDirty();
},
// Render order: Transform always renders first
-1);
}
@@ -21,5 +21,9 @@ REGISTER_COMPONENT_GROUP("Water Plane", "Water", WaterPlane, WaterPlaneEditor)
if (e.has<WaterPlane>()) {
e.remove<WaterPlane>();
}
},
// On-modified: mark dirty so EditorWaterPlaneSystem rebuilds the plane
[](flecs::entity e) {
e.get_mut<WaterPlane>().markDirty();
});
}
+23 -512
View File
@@ -52,13 +52,7 @@
#include "../components/Inventory.hpp"
#include "CharacterClassSystem.hpp"
#include "../ui/TransformEditor.hpp"
#include "../ui/RenderableEditor.hpp"
#include "../ui/PhysicsColliderEditor.hpp"
#include "../ui/RigidBodyEditor.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/CharacterIdentityEditor.hpp"
#include "../ui/PrefabInstanceEditor.hpp"
#include "PhysicsSystem.hpp"
#include "BuoyancySystem.hpp"
#include "NavMeshSystem.hpp"
@@ -79,7 +73,7 @@ EditorUISystem::EditorUISystem(flecs::world &world,
, m_selectedEntity(flecs::entity::null())
, m_nameQuery(world.query<EntityNameComponent>())
{
registerComponentEditors();
registerModularComponents();
m_gizmo = std::make_unique<Gizmo>(m_sceneMgr);
m_roadGizmo = std::make_unique<RoadGizmo>(m_sceneMgr);
m_cursor3D = std::make_unique<Cursor3D>(m_sceneMgr);
@@ -249,135 +243,6 @@ bool EditorUISystem::onMouseReleased()
return false;
}
void EditorUISystem::registerComponentEditors()
{
// Register Transform component
auto transformEditor = std::make_unique<TransformEditor>();
m_componentRegistry.registerComponent<TransformComponent>(
"Transform", "Transform", std::move(transformEditor),
// Adder
[this](flecs::entity e) {
if (!e.has<TransformComponent>()) {
TransformComponent transform;
transform.node =
m_sceneMgr->getRootSceneNode()
->createChildSceneNode();
transform.position = Ogre::Vector3::ZERO;
transform.rotation = Ogre::Quaternion::IDENTITY;
transform.scale = Ogre::Vector3::UNIT_SCALE;
e.set<TransformComponent>(transform);
}
},
// Remover
[this](flecs::entity e) {
if (e.has<TransformComponent>()) {
auto &transform =
e.get_mut<TransformComponent>();
if (transform.node) {
m_sceneMgr->destroySceneNode(
transform.node);
transform.node = nullptr;
}
e.remove<TransformComponent>();
}
});
// Register Renderable component
auto renderableEditor = std::make_unique<RenderableEditor>(m_sceneMgr);
m_componentRegistry.registerComponent<RenderableComponent>(
"Renderable", "Rendering", std::move(renderableEditor),
// Adder
[this](flecs::entity e) {
if (!e.has<RenderableComponent>()) {
e.set<RenderableComponent>({});
}
},
// Remover
[this](flecs::entity e) {
if (e.has<RenderableComponent>()) {
auto &renderable =
e.get_mut<RenderableComponent>();
if (renderable.entity) {
m_sceneMgr->destroyEntity(
renderable.entity);
renderable.entity = nullptr;
}
e.remove<RenderableComponent>();
}
});
// Register RigidBody component
auto rigidBodyEditor = std::make_unique<RigidBodyEditor>();
m_componentRegistry.registerComponent<RigidBodyComponent>(
"Rigid Body", "Physics", std::move(rigidBodyEditor),
// Adder
[this](flecs::entity e) {
if (!e.has<RigidBodyComponent>()) {
e.set<RigidBodyComponent>({});
}
},
// Remover
[this](flecs::entity e) {
if (e.has<RigidBodyComponent>()) {
e.remove<RigidBodyComponent>();
}
});
// Register PhysicsCollider component
auto colliderEditor =
std::make_unique<PhysicsColliderEditor>(m_sceneMgr);
m_componentRegistry.registerComponent<PhysicsColliderComponent>(
"Physics Collider", "Physics", std::move(colliderEditor),
// Adder
[this](flecs::entity e) {
if (!e.has<PhysicsColliderComponent>()) {
e.set<PhysicsColliderComponent>({});
}
},
// Remover
[this](flecs::entity e) {
if (e.has<PhysicsColliderComponent>()) {
e.remove<PhysicsColliderComponent>();
}
});
// Register PrefabInstance component
auto prefabEditor = std::make_unique<PrefabInstanceEditor>();
m_componentRegistry.registerComponent<PrefabInstanceComponent>(
"Prefab Instance", "Scene", std::move(prefabEditor),
// Adder
[this](flecs::entity e) {
if (!e.has<PrefabInstanceComponent>()) {
e.set<PrefabInstanceComponent>({});
}
},
// Remover
[this](flecs::entity e) {
if (e.has<PrefabInstanceComponent>()) {
e.remove<PrefabInstanceComponent>();
}
});
// Register CharacterIdentity component
auto characterIdentityEditor =
std::make_unique<CharacterIdentityEditor>();
m_componentRegistry.registerComponent<CharacterIdentityComponent>(
"Character Identity", "Character",
std::move(characterIdentityEditor),
[](flecs::entity e) {
if (!e.has<CharacterIdentityComponent>())
e.set<CharacterIdentityComponent>(
CharacterIdentityComponent{});
},
[](flecs::entity e) {
if (e.has<CharacterIdentityComponent>())
e.remove<CharacterIdentityComponent>();
});
// Register modular components (Light, Camera, etc.)
registerModularComponents();
}
void EditorUISystem::registerModularComponents()
{
// This calls all component modules registered via REGISTER_COMPONENT macro
@@ -1056,387 +921,33 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
// Render component editors
ImGui::BeginChild("Components", ImVec2(0, 0), true);
// Dynamically render all components the entity has
// Collect the components present on the entity and render them through
// their registered type-erased renderers. Sort by (order, name) so
// Transform (order -1) always renders first.
std::vector<const ComponentRegistry::ComponentInfo *> infos;
m_componentRegistry.forEach(
[&](const std::type_index &,
const ComponentRegistry::ComponentInfo &info) {
if (info.renderer && info.checker(entity))
infos.push_back(&info);
});
std::sort(infos.begin(), infos.end(),
[](const ComponentRegistry::ComponentInfo *a,
const ComponentRegistry::ComponentInfo *b) {
if (a->order != b->order)
return a->order < b->order;
return std::string(a->name) < std::string(b->name);
});
int componentCount = 0;
// Render Transform first if present (it's the base component)
if (entity.has<TransformComponent>()) {
auto &transform = entity.get_mut<TransformComponent>();
if (m_componentRegistry.render<TransformComponent>(entity,
transform)) {
// Transform changed - mark StaticGeometryMember dirty if present
if (entity.has<StaticGeometryMemberComponent>()) {
entity.get_mut<StaticGeometryMemberComponent>()
.markDirty();
}
}
componentCount++;
}
// Render Renderable
if (entity.has<RenderableComponent>()) {
auto &renderable = entity.get_mut<RenderableComponent>();
m_componentRegistry.render<RenderableComponent>(entity,
renderable);
componentCount++;
}
// Render Light if present
if (entity.has<LightComponent>()) {
auto &light = entity.get_mut<LightComponent>();
m_componentRegistry.render<LightComponent>(entity, light);
componentCount++;
}
// Render Camera if present
if (entity.has<CameraComponent>()) {
auto &camera = entity.get_mut<CameraComponent>();
m_componentRegistry.render<CameraComponent>(entity, camera);
componentCount++;
}
// Render RigidBody
if (entity.has<RigidBodyComponent>()) {
auto &rigidBody = entity.get_mut<RigidBodyComponent>();
m_componentRegistry.render<RigidBodyComponent>(entity,
rigidBody);
componentCount++;
}
// Render PhysicsCollider
if (entity.has<PhysicsColliderComponent>()) {
auto &collider = entity.get_mut<PhysicsColliderComponent>();
m_componentRegistry.render<PhysicsColliderComponent>(entity,
collider);
componentCount++;
}
// Render BuoyancyInfo if present
if (entity.has<BuoyancyInfo>()) {
auto &buoyancy = entity.get_mut<BuoyancyInfo>();
m_componentRegistry.render<BuoyancyInfo>(entity, buoyancy);
componentCount++;
}
// Render WaterPhysics if present
if (entity.has<WaterPhysics>()) {
auto &wp = entity.get_mut<WaterPhysics>();
m_componentRegistry.render<WaterPhysics>(entity, wp);
componentCount++;
}
// Render Sun if present
if (entity.has<SunComponent>()) {
auto &sun = entity.get_mut<SunComponent>();
if (m_componentRegistry.render<SunComponent>(entity, sun)) {
sun.markDirty();
}
componentCount++;
}
// Render Skybox if present
if (entity.has<SkyboxComponent>()) {
auto &sky = entity.get_mut<SkyboxComponent>();
if (m_componentRegistry.render<SkyboxComponent>(entity, sky)) {
sky.markDirty();
}
componentCount++;
}
// Render WaterPlane if present
if (entity.has<WaterPlane>()) {
auto &wp = entity.get_mut<WaterPlane>();
if (m_componentRegistry.render<WaterPlane>(entity, wp)) {
wp.markDirty();
}
componentCount++;
}
// Render Terrain if present
if (entity.has<TerrainComponent>()) {
auto &tc = entity.get_mut<TerrainComponent>();
m_componentRegistry.render<TerrainComponent>(entity, tc);
componentCount++;
}
// Render LOD Settings if present
if (entity.has<LodSettingsComponent>()) {
auto &lodSettings = entity.get_mut<LodSettingsComponent>();
m_componentRegistry.render<LodSettingsComponent>(entity,
lodSettings);
componentCount++;
}
// Render LOD if present
if (entity.has<LodComponent>()) {
auto &lod = entity.get_mut<LodComponent>();
m_componentRegistry.render<LodComponent>(entity, lod);
componentCount++;
}
// Render StaticGeometry Region if present
if (entity.has<StaticGeometryComponent>()) {
auto &region = entity.get_mut<StaticGeometryComponent>();
m_componentRegistry.render<StaticGeometryComponent>(entity,
region);
componentCount++;
}
// Render StaticGeometry Member if present
if (entity.has<StaticGeometryMemberComponent>()) {
auto &member = entity.get_mut<StaticGeometryMemberComponent>();
m_componentRegistry.render<StaticGeometryMemberComponent>(
entity, member);
componentCount++;
}
// Render ProceduralTexture if present
if (entity.has<ProceduralTextureComponent>()) {
auto &texture = entity.get_mut<ProceduralTextureComponent>();
m_componentRegistry.render<ProceduralTextureComponent>(entity,
texture);
componentCount++;
}
// Render ProceduralMaterial if present
if (entity.has<ProceduralMaterialComponent>()) {
auto &material = entity.get_mut<ProceduralMaterialComponent>();
m_componentRegistry.render<ProceduralMaterialComponent>(
entity, material);
componentCount++;
}
// Render Primitive if present
if (entity.has<PrimitiveComponent>()) {
auto &primitive = entity.get_mut<PrimitiveComponent>();
m_componentRegistry.render<PrimitiveComponent>(entity,
primitive);
componentCount++;
}
// Render TriangleBuffer if present
if (entity.has<TriangleBufferComponent>()) {
auto &tb = entity.get_mut<TriangleBufferComponent>();
m_componentRegistry.render<TriangleBufferComponent>(entity, tb);
componentCount++;
}
// Render Character if present
if (entity.has<CharacterComponent>()) {
auto &cc = entity.get_mut<CharacterComponent>();
m_componentRegistry.render<CharacterComponent>(entity, cc);
componentCount++;
}
// Render CharacterSlots if present
if (entity.has<CharacterSlotsComponent>()) {
/* CharacterSlotsComponent is now an empty tag; get_mut is illegal
* for zero-sized types in flecs. */
CharacterSlotsComponent cs;
m_componentRegistry.render<CharacterSlotsComponent>(entity, cs);
componentCount++;
}
// Render CharacterIdentity if present
if (entity.has<CharacterIdentityComponent>()) {
auto &ci = entity.get_mut<CharacterIdentityComponent>();
m_componentRegistry.render<CharacterIdentityComponent>(entity,
ci);
componentCount++;
}
// Render CharacterSpawner if present
if (entity.has<CharacterSpawnerComponent>()) {
auto &spawner = entity.get_mut<CharacterSpawnerComponent>();
m_componentRegistry.render<CharacterSpawnerComponent>(entity,
spawner);
componentCount++;
}
// Render AnimationTree if present
if (entity.has<AnimationTreeComponent>()) {
auto &at = entity.get_mut<AnimationTreeComponent>();
m_componentRegistry.render<AnimationTreeComponent>(entity, at);
componentCount++;
}
// Render StartupMenu if present
if (entity.has<StartupMenuComponent>()) {
auto &sm = entity.get_mut<StartupMenuComponent>();
m_componentRegistry.render<StartupMenuComponent>(entity, sm);
componentCount++;
}
// Render PlayerController if present
if (entity.has<PlayerControllerComponent>()) {
auto &pc = entity.get_mut<PlayerControllerComponent>();
m_componentRegistry.render<PlayerControllerComponent>(entity,
pc);
componentCount++;
}
// Render CellGrid if present
if (entity.has<CellGridComponent>()) {
auto &grid = entity.get_mut<CellGridComponent>();
m_componentRegistry.render<CellGridComponent>(entity, grid);
componentCount++;
}
// Render Lot if present
if (entity.has<LotComponent>()) {
auto &lot = entity.get_mut<LotComponent>();
m_componentRegistry.render<LotComponent>(entity, lot);
componentCount++;
}
// Render District if present
if (entity.has<DistrictComponent>()) {
auto &district = entity.get_mut<DistrictComponent>();
m_componentRegistry.render<DistrictComponent>(entity, district);
componentCount++;
}
// Render Town if present
if (entity.has<TownComponent>()) {
auto &town = entity.get_mut<TownComponent>();
m_componentRegistry.render<TownComponent>(entity, town);
componentCount++;
}
// Render Roof if present
if (entity.has<RoofComponent>()) {
auto &roof = entity.get_mut<RoofComponent>();
m_componentRegistry.render<RoofComponent>(entity, roof);
componentCount++;
}
// Render Room if present
if (entity.has<RoomComponent>()) {
auto &room = entity.get_mut<RoomComponent>();
if (m_componentRegistry.render<RoomComponent>(entity, room)) {
room.markDirty();
}
componentCount++;
}
// Render ClearArea if present
if (entity.has<ClearAreaComponent>()) {
auto &clearArea = entity.get_mut<ClearAreaComponent>();
if (m_componentRegistry.render<ClearAreaComponent>(entity,
clearArea)) {
clearArea.markDirty();
}
componentCount++;
}
// Render FurnitureTemplate if present
if (entity.has<FurnitureTemplateComponent>()) {
auto &furniture = entity.get_mut<FurnitureTemplateComponent>();
m_componentRegistry.render<FurnitureTemplateComponent>(
entity, furniture);
componentCount++;
}
// Render ActionDatabaseComponent if present
if (entity.has<ActionDatabaseComponent>()) {
auto &db = entity.get_mut<ActionDatabaseComponent>();
m_componentRegistry.render<ActionDatabaseComponent>(entity, db);
componentCount++;
}
// Render ActionDebug if present
if (entity.has<ActionDebug>()) {
auto &debug = entity.get_mut<ActionDebug>();
m_componentRegistry.render<ActionDebug>(entity, debug);
componentCount++;
}
// Render BehaviorTree if present
if (entity.has<BehaviorTreeComponent>()) {
auto &bt = entity.get_mut<BehaviorTreeComponent>();
m_componentRegistry.render<BehaviorTreeComponent>(entity, bt);
componentCount++;
}
// Render NavMesh if present
if (entity.has<NavMeshComponent>()) {
auto &nav = entity.get_mut<NavMeshComponent>();
if (m_componentRegistry.render<NavMeshComponent>(entity, nav)) {
if (nav.debugDraw && NavMeshSystem::getInstance())
NavMeshSystem::getInstance()->setDebugDraw(
entity, nav.debugDraw);
}
componentCount++;
}
// Render NavMeshGeometrySource if present
if (entity.has<NavMeshGeometrySource>()) {
auto &src = entity.get_mut<NavMeshGeometrySource>();
m_componentRegistry.render<NavMeshGeometrySource>(entity, src);
componentCount++;
}
// Render SmartObject if present
if (entity.has<SmartObjectComponent>()) {
auto &so = entity.get_mut<SmartObjectComponent>();
m_componentRegistry.render<SmartObjectComponent>(entity, so);
componentCount++;
}
// Render GoapPlanner if present
if (entity.has<GoapPlannerComponent>()) {
auto &planner = entity.get_mut<GoapPlannerComponent>();
m_componentRegistry.render<GoapPlannerComponent>(entity,
planner);
componentCount++;
}
// Render GoapRunner if present
if (entity.has<GoapRunnerComponent>()) {
auto &runner = entity.get_mut<GoapRunnerComponent>();
m_componentRegistry.render<GoapRunnerComponent>(entity, runner);
componentCount++;
}
// Render PathFollowing if present
if (entity.has<PathFollowingComponent>()) {
auto &pf = entity.get_mut<PathFollowingComponent>();
m_componentRegistry.render<PathFollowingComponent>(entity, pf);
componentCount++;
}
// Render Actuator if present
if (entity.has<ActuatorComponent>()) {
auto &actuator = entity.get_mut<ActuatorComponent>();
m_componentRegistry.render<ActuatorComponent>(entity, actuator);
componentCount++;
}
// Render EventHandler if present
if (entity.has<EventHandlerComponent>()) {
auto &handler = entity.get_mut<EventHandlerComponent>();
m_componentRegistry.render<EventHandlerComponent>(entity,
handler);
componentCount++;
}
// Render Item if present
if (entity.has<ItemComponent>()) {
auto &item = entity.get_mut<ItemComponent>();
m_componentRegistry.render<ItemComponent>(entity, item);
componentCount++;
}
// Render Inventory if present
if (entity.has<InventoryComponent>()) {
auto &inv = entity.get_mut<InventoryComponent>();
m_componentRegistry.render<InventoryComponent>(entity, inv);
for (const auto *info : infos) {
bool modified = info->renderer(entity);
if (modified && info->onModified)
info->onModified(entity);
componentCount++;
}
// Show message if no components
if (componentCount == 0) {
ImGui::TextDisabled("No components");
ImGui::Text("Click 'Add Component' to add components");
@@ -243,7 +243,6 @@ private:
void renderRemoveComponentMenu(flecs::entity entity);
// Helper functions
void registerComponentEditors();
void registerModularComponents();
flecs::entity findEntityParent(flecs::entity entity);
std::vector<flecs::entity> getEntityChildren(flecs::entity entity);
@@ -2,6 +2,7 @@
#define EDITSCENE_COMPONENTREGISTRY_HPP
#pragma once
#include <typeindex>
#include <type_traits>
#include <unordered_map>
#include <memory>
#include <functional>
@@ -26,6 +27,24 @@ using ComponentRemover = std::function<void(flecs::entity)>;
*/
using ComponentChecker = std::function<bool(flecs::entity)>;
/**
* Function type for rendering a component editor from an entity.
*
* The closure performs the type-erased fetch of the component pointer
* (via e.get_mut<T>() for data components, or a stack-local instance for
* empty/tag components) and forwards it to the registered editor.
*/
using ComponentRenderer = std::function<bool(flecs::entity)>;
/**
* Function type invoked after a component editor reports a modification.
*
* Used for cross-component/system side effects that cannot live inside a
* single editor (e.g. marking a StaticGeometryMember dirty when its
* Transform changed, or syncing NavMesh debug-draw state).
*/
using ComponentModified = std::function<void(flecs::entity)>;
/**
* Registry for component editors and their add/remove functions
*/
@@ -34,10 +53,13 @@ public:
struct ComponentInfo {
const char *name;
const char *group; // Group name for menu organization (nullptr = "Miscellaneous")
int order = 0; // Render order (lower renders first; Transform is -1)
std::unique_ptr<IComponentEditor> editor;
ComponentRenderer renderer; // Type-erased render closure
ComponentAdder adder;
ComponentRemover remover;
ComponentChecker checker; // Checks if entity has this component
ComponentModified onModified; // Optional post-render side effect
};
ComponentRegistry() = default;
@@ -58,19 +80,38 @@ public:
* @param editor The editor for this component
* @param adder Function to add the component to an entity
* @param remover Function to remove the component from an entity
* @param onModified Optional callback invoked after the editor reports a
* modification (for cross-component side effects)
* @param order Render order; lower values render first (Transform uses -1)
*/
template<typename T>
void registerComponent(const char *name, const char *group,
std::unique_ptr<ComponentEditor<T>> editor,
ComponentAdder adder, ComponentRemover remover)
ComponentAdder adder, ComponentRemover remover,
ComponentModified onModified = nullptr,
int order = 0)
{
ComponentInfo info;
info.name = name;
info.group = group ? group : "Miscellaneous";
info.order = order;
IComponentEditor *rawEditor = editor.get();
info.editor = std::move(editor);
info.renderer = [rawEditor](flecs::entity e) -> bool {
if (!e.has<T>())
return false;
if constexpr (std::is_empty_v<T>) {
/* Empty/tag components are zero-sized in flecs, so
* get_mut<T>() is illegal; render against a dummy. */
T dummy{};
return rawEditor->render(e, &dummy);
}
return rawEditor->render(e, &e.get_mut<T>());
};
info.adder = adder;
info.remover = remover;
info.checker = [](flecs::entity e) { return e.has<T>(); };
info.onModified = onModified;
m_components[std::type_index(typeid(T))] = std::move(info);
}