From d80df444aab4cd8033ed03c356177fdd613b08e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 18 Aug 2026 10:46:36 +0000 Subject: [PATCH] refactor(scene): register link kinds instead of hard-coding them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A scene link keeps an external object in step with the node it hangs off. The kinds were a closed enum with a switch in TransformSystem, so scene had to include graphics, physics and audio to do the casts, and no other module could ever attach to the tree without editing SceneHost.h -- particles, voxel, spritestack, avatar and weather all had to stay outside it. Kinds are registered now (scene/SceneLink.h). The module owning the target type supplies pushWorld / pullWorld / alive, keeps the casts on its own side where the type is complete, and scene never learns what a Renderable3D is. Dispatch is a function-pointer table rather than virtual calls, since link sync runs for every dirty node every frame. The six typed helpers (linkRenderable2D and friends) stay as conveniences and now resolve their kind by name, so they return false rather than crash when the module that registers it was trimmed out. The script API already named kinds with strings, which is exactly the registry key, so it is unchanged. Direction is now the right way round: graphics, physics and audio depend on scene rather than scene on them, and scene drops from L5 to L1. What remains is scene -> graphics from pickScreenAt / collectFrustumIdsAt, which take a Camera3D and read its projection; that is recorded in the back-edge whitelist as the next thing to move. Each provider lives in a SceneLinks.cpp, sharing the basename so the three come out of the build together when scene is disabled. Co-authored-by: 西风逍遥游 --- cmake/module_manifest.cmake | 10 +- scripts/module_depgraph.py | 5 + src/engine/devtools/SceneInspect.cpp | 2 +- src/modules/CMakeLists.txt | 10 +- src/modules/audio/SceneLinks.cpp | 26 +++++ src/modules/graphics/SceneLinks.cpp | 93 ++++++++++++++++ src/modules/physics/SceneLinks.cpp | 69 ++++++++++++ src/modules/scene/Scene.cpp | 19 ++-- src/modules/scene/SceneHost.cpp | 34 +++--- src/modules/scene/SceneHost.h | 42 +++---- src/modules/scene/SceneLink.cpp | 58 ++++++++++ src/modules/scene/SceneLink.h | 74 +++++++++++++ src/modules/scene/TransformSystem.cpp | 140 ++---------------------- src/modules/sceneloader/SceneLoader.cpp | 10 +- test/scene.cpp | 6 +- test/sceneloader.cpp | 4 +- 16 files changed, 404 insertions(+), 198 deletions(-) create mode 100644 src/modules/audio/SceneLinks.cpp create mode 100644 src/modules/graphics/SceneLinks.cpp create mode 100644 src/modules/physics/SceneLinks.cpp create mode 100644 src/modules/scene/SceneLink.cpp create mode 100644 src/modules/scene/SceneLink.h diff --git a/cmake/module_manifest.cmake b/cmake/module_manifest.cmake index a870bed4..1c235b50 100644 --- a/cmake/module_manifest.cmake +++ b/cmake/module_manifest.cmake @@ -142,6 +142,7 @@ eve_declare_module(NAME touch LAYER 2 SCRIPT Touch SLOT touch GROUP minimal 2d 3d web) eve_declare_module(NAME audio LAYER 2 SCRIPT Audio SLOT audio DEPS event sound + OPTIONAL_DEPS scene THIRDPARTY openal GROUP 2d 3d) eve_declare_module(NAME font LAYER 2 SCRIPT Font SLOT font @@ -176,6 +177,7 @@ eve_declare_module(NAME ui LIB EVUI LAYER 4 SCRIPT UI SLOT ui GROUP minimal 2d 3d web) eve_declare_module(NAME physics LAYER 4 SCRIPT Physics SLOT physics DEPS event graphics + OPTIONAL_DEPS scene THIRDPARTY box2d box3d GROUP 2d 3d web) eve_declare_module(NAME map LAYER 4 SCRIPT Map SLOT map @@ -214,9 +216,11 @@ eve_declare_module(NAME demo LAYER 4 SCRIPT Demo # L5 / L6 -- aggregates and orchestration # --------------------------------------------------------------------------- -eve_declare_module(NAME scene LAYER 5 SCRIPT Scene SLOT scene - DEPS graphics physics spatial - OPTIONAL_DEPS audio +# Renderables, bodies and audio sources attach through registered link kinds +# (scene/SceneLink.h), so scene no longer depends on those modules; graphics +# remains only because pickScreenAt / collectFrustumIdsAt take a Camera3D. +eve_declare_module(NAME scene LAYER 1 SCRIPT Scene SLOT scene + DEPS graphics spatial THIRDPARTY poco GROUP 3d web) eve_declare_module(NAME particles LAYER 5 SCRIPT Particles SLOT particles diff --git a/scripts/module_depgraph.py b/scripts/module_depgraph.py index 61df0d7d..fb06e1f8 100755 --- a/scripts/module_depgraph.py +++ b/scripts/module_depgraph.py @@ -33,6 +33,11 @@ KNOWN_BACK_EDGES = { # pointer and decodes its own icon image. ("window", "graphics"), ("window", "image"), + # scene/Scene.cpp: pickScreenAt and collectFrustumIdsAt take a + # graphics::Camera3D and read its projection. Everything else about scene is + # now renderer-agnostic (graphics registers its own link kinds), so these two + # entry points are what is left to move out to camera/graphics. + ("scene", "graphics"), } diff --git a/src/engine/devtools/SceneInspect.cpp b/src/engine/devtools/SceneInspect.cpp index 325d419a..17e7655b 100644 --- a/src/engine/devtools/SceneInspect.cpp +++ b/src/engine/devtools/SceneInspect.cpp @@ -122,7 +122,7 @@ image::ImageData* buildIdMask(eve::graphics::Graphics* gfx, std::string* outJson host->walkDepthFirst([&](eve::scene::SceneHost*, int, eve::scene::SceneNode& node) { if (!node.visible || node.space != "3d") return; - const auto* l = host->findLink(&node, eve::scene::LinkKind::Renderable3D); + const auto* l = host->findLink(&node, eve::scene::findLinkKind("renderable3d")); if (!l || !l->target) return; auto* r = static_cast(l->target); auto mr = r->meshRenderer(); diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 8a25278b..daecae00 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -13,6 +13,15 @@ if(NOT _has_font) CACHE INTERNAL "Exact basenames excluded from module source scan") endif() +# Each of graphics / physics / audio has a SceneLinks.cpp registering the link +# kinds it owns (scene/SceneLink.h). They all share the basename, so dropping +# scene drops all three in one rule. +eve_module_enabled(scene _has_scene) +if(NOT _has_scene) + set(EVENGINE_EXCLUDED_MODULE_FILES "${EVENGINE_EXCLUDED_MODULE_FILES};SceneLinks.cpp" + CACHE INTERNAL "Exact basenames excluded from module source scan") +endif() + # --- create every enabled module ------------------------------------------- foreach(_mod IN LISTS EVE_ENABLED_MODULES) @@ -167,7 +176,6 @@ if(_has_procgen) ${CMAKE_SOURCE_DIR}/third-party/DungeonTemplateLibrary/include) endif() -eve_module_enabled(scene _has_scene) if(_has_scene AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # Scene serialization needs Poco JSON; the browser build has no Poco headers, # so EVENGINE_SCENE_JSON compiles that path out of Scene.cpp there. diff --git a/src/modules/audio/SceneLinks.cpp b/src/modules/audio/SceneLinks.cpp new file mode 100644 index 00000000..f3939445 --- /dev/null +++ b/src/modules/audio/SceneLinks.cpp @@ -0,0 +1,26 @@ +// Registers the scene link kind backed by audio: a positional 3D source that +// follows its node. +// +// audio::Source has no liveness API, so no `alive` callback is supplied and the +// link is assumed alive until explicitly unlinked. + +#include "audio/Source.h" +#include "scene/SceneHost.h" +#include "scene/SceneLink.h" + +namespace eve::audio { +namespace { + +using eve::scene::SceneNode; + +void pushSource3D(const SceneNode &n, void *target) { + auto *s = static_cast(target); + s->setPosition(n.world[3][0], n.world[3][1], n.world[3][2]); +} + +struct Register { + Register() { eve::scene::registerLinkKind({"audio3d", &pushSource3D, nullptr, nullptr}); } +} g_register; + +} // namespace +} // namespace eve::audio diff --git a/src/modules/graphics/SceneLinks.cpp b/src/modules/graphics/SceneLinks.cpp new file mode 100644 index 00000000..c2a03656 --- /dev/null +++ b/src/modules/graphics/SceneLinks.cpp @@ -0,0 +1,93 @@ +// Registers the scene link kinds backed by graphics: 2D and 3D renderables and +// the 3D camera. +// +// The world-matrix decomposition used to live in scene/TransformSystem.cpp, +// which forced scene to include graphics. Keeping it here puts the casts where +// the types are complete and lets scene stay unaware of what it is driving. + +#include "common/ECS.h" +#include "graphics/RenderSystem.h" +#include "graphics/RenderSystem3D.h" +#include "scene/SceneHost.h" +#include "scene/SceneLink.h" + +#include +#include +#include + +namespace eve::graphics { +namespace { + +using eve::scene::LinkOps; +using eve::scene::SceneNode; + +void pushRenderable3D(const SceneNode &n, void *target) { + auto *r = static_cast(target); + auto t = r->transform(); + const glm::mat4 &w = n.world; + t->x = w[3][0]; + t->y = w[3][1]; + t->z = w[3][2]; + + glm::vec3 c0(w[0]); + glm::vec3 c1(w[1]); + glm::vec3 c2(w[2]); + t->sx = glm::length(c0); + t->sy = glm::length(c1); + t->sz = glm::length(c2); + if (t->sx > 1e-8f) c0 /= t->sx; + if (t->sy > 1e-8f) c1 /= t->sy; + if (t->sz > 1e-8f) c2 /= t->sz; + + glm::mat3 rot(c0, c1, c2); + glm::quat q = glm::quat_cast(rot); + glm::vec3 euler = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) + t->pitch = euler.x; + t->yaw = euler.y; + t->roll = euler.z; + + r->meshRenderer()->visible = n.visible; +} + +void pushRenderable2D(const SceneNode &n, void *target) { + auto *r = static_cast(target); + auto t = r->transform(); + const glm::mat4 &w = n.world; + t->x = w[3][0]; + t->y = w[3][1]; + + glm::vec3 c0(w[0]); + glm::vec3 c1(w[1]); + t->sx = glm::length(c0); + t->sy = glm::length(c1); + if (t->sx > 1e-8f) c0 /= t->sx; + // 2D rotation from first column in XY + t->rot = std::atan2(c0.y, c0.x); + + r->sprite()->visible = n.visible; +} + +void pushCamera3D(const SceneNode &n, void *target) { + auto *c = static_cast(target); + c->setEye(n.world[3][0], n.world[3][1], n.world[3][2]); +} + +/** These are all ECS entities, destroyed through ecs::DestroyEntity. */ +template +bool entityAlive(const void *target) { + return ecs::is_entity_visible(static_cast(static_cast(target))); +} + +struct Register { + Register() { + eve::scene::registerLinkKind( + {"renderable2d", &pushRenderable2D, nullptr, &entityAlive}); + eve::scene::registerLinkKind( + {"renderable3d", &pushRenderable3D, nullptr, &entityAlive}); + eve::scene::registerLinkKind( + {"camera3d", &pushCamera3D, nullptr, &entityAlive}); + } +} g_register; + +} // namespace +} // namespace eve::graphics diff --git a/src/modules/physics/SceneLinks.cpp b/src/modules/physics/SceneLinks.cpp new file mode 100644 index 00000000..b215a435 --- /dev/null +++ b/src/modules/physics/SceneLinks.cpp @@ -0,0 +1,69 @@ +// Registers the scene link kinds backed by physics: 2D and 3D rigid bodies. +// +// These are the only kinds that can drive a node instead of following it +// (syncMode 1), so they are also the only ones supplying pullWorld. + +#include "physics/Body.h" +#include "physics/Body3D.h" +#include "scene/SceneHost.h" +#include "scene/SceneLink.h" + +#include + +namespace eve::physics { +namespace { + +using eve::scene::LinkOps; +using eve::scene::SceneNode; + +void pushBody2D(const SceneNode &n, void *target) { + auto *b = static_cast(target); + b->setPosition(n.x, n.y); + b->setAngle(n.roll); +} + +void pullBody2D(SceneNode &n, void *target) { + auto *b = static_cast(target); + n.x = b->getX(); + n.y = b->getY(); + n.roll = b->getAngle(); +} + +bool aliveBody2D(const void *target) { + return static_cast(target)->raw() != nullptr; +} + +void pushBody3D(const SceneNode &n, void *target) { + auto *b = static_cast(target); + b->setPosition(n.x, n.y, n.z); + glm::quat q = glm::angleAxis(n.yaw, glm::vec3(0.f, 1.f, 0.f)) * + glm::angleAxis(n.pitch, glm::vec3(1.f, 0.f, 0.f)) * + glm::angleAxis(n.roll, glm::vec3(0.f, 0.f, 1.f)); + b->setRotation(q.x, q.y, q.z, q.w); +} + +void pullBody3D(SceneNode &n, void *target) { + auto *b = static_cast(target); + n.x = b->getX(); + n.y = b->getY(); + n.z = b->getZ(); + glm::quat q(b->getRotW(), b->getRotX(), b->getRotY(), b->getRotZ()); + glm::vec3 e = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) + n.pitch = e.x; + n.yaw = e.y; + n.roll = e.z; +} + +bool aliveBody3D(const void *target) { + return static_cast(target)->isValid(); +} + +struct Register { + Register() { + eve::scene::registerLinkKind({"physics2d", &pushBody2D, &pullBody2D, &aliveBody2D}); + eve::scene::registerLinkKind({"physics3d", &pushBody3D, &pullBody3D, &aliveBody3D}); + } +} g_register; + +} // namespace +} // namespace eve::physics diff --git a/src/modules/scene/Scene.cpp b/src/modules/scene/Scene.cpp index 56cdf0ae..02f9ec5a 100644 --- a/src/modules/scene/Scene.cpp +++ b/src/modules/scene/Scene.cpp @@ -77,15 +77,12 @@ bool sameScriptObject(const HSQOBJECT &a, const HSQOBJECT &b) { std::memcmp(&a._unVal, &b._unVal, sizeof(a._unVal)) == 0; } -LinkKind linkKindFromString(const std::string &kind) { - if (kind == "renderable2d") return LinkKind::Renderable2D; - if (kind == "renderable3d") return LinkKind::Renderable3D; - if (kind == "physics2d") return LinkKind::Physics2D; - if (kind == "physics3d") return LinkKind::Physics3D; - if (kind == "camera3d") return LinkKind::Camera3D; - if (kind == "audio3d") return LinkKind::Audio3D; - return LinkKind::None; -} +/** + * The script API already names link kinds with strings, which is exactly the + * registry's key, so an unknown name and a kind whose module was trimmed out + * are the same case: -1. + */ +int linkKindFromString(const std::string &kind) { return findLinkKind(kind.c_str()); } int syncModeFromString(const std::string &mode) { return mode == "body" ? 1 : 0; @@ -898,8 +895,8 @@ bool Scene::unlinkNodeKindAt(const std::string &hostName, const std::string &nod const std::string &kind) { SceneHost *h = resolveHost(hostName); if (!h) return false; - const LinkKind k = linkKindFromString(kind); - if (k == LinkKind::None) return false; + const int k = linkKindFromString(kind); + if (k < 0) return false; const bool removed = h->unlink(nodeId, k); if (removed) TransformSystem::updateHost(h); return removed; diff --git a/src/modules/scene/SceneHost.cpp b/src/modules/scene/SceneHost.cpp index 5f674a25..00355458 100644 --- a/src/modules/scene/SceneHost.cpp +++ b/src/modules/scene/SceneHost.cpp @@ -53,7 +53,7 @@ void linkAsLastChild(SceneHost::Tree &tree, int parentIndex, int childIndex) { } /** Add a link of a kind, replacing an existing same-kind link (keep others). */ -bool setLink(SceneNode &n, LinkKind kind, void *target, int syncMode) { +bool setLink(SceneNode &n, int kind, void *target, int syncMode) { for (auto &l : n.links) { if (l.kind == kind) { l.target = target; @@ -522,37 +522,39 @@ std::vector SceneHost::collectIdsVisible(bool visible) { return collectIdsWhere([&](SceneHost *, int, const SceneNode &n) { return n.visible == visible; }); } -bool SceneHost::linkRenderable2D(const std::string &nodeId, graphics::Renderable2D *r) { +bool SceneHost::link(const std::string &nodeId, int kind, void *target, int syncMode) { + if (!linkOps(kind)) return false; // kind's module is not in this build SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Renderable2D, r, 0) : false; + return n ? setLink(*n, kind, target, syncMode) : false; +} + +// The typed helpers resolve their kind by name, so they return false rather +// than crashing when the module that registers it was trimmed out. +bool SceneHost::linkRenderable2D(const std::string &nodeId, graphics::Renderable2D *r) { + return link(nodeId, findLinkKind("renderable2d"), r, 0); } bool SceneHost::linkRenderable3D(const std::string &nodeId, graphics::Renderable3D *r) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Renderable3D, r, 0) : false; + return link(nodeId, findLinkKind("renderable3d"), r, 0); } bool SceneHost::linkPhysics2D(const std::string &nodeId, physics::Body *b, int syncMode) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Physics2D, b, syncMode) : false; + return link(nodeId, findLinkKind("physics2d"), b, syncMode); } bool SceneHost::linkPhysics3D(const std::string &nodeId, physics::Body3D *b, int syncMode) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Physics3D, b, syncMode) : false; + return link(nodeId, findLinkKind("physics3d"), b, syncMode); } bool SceneHost::linkCamera3D(const std::string &nodeId, graphics::Camera3D *c) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Camera3D, c, 0) : false; + return link(nodeId, findLinkKind("camera3d"), c, 0); } bool SceneHost::linkAudio3D(const std::string &nodeId, audio::Source *s) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Audio3D, s, 0) : false; + return link(nodeId, findLinkKind("audio3d"), s, 0); } -bool SceneHost::unlink(const std::string &nodeId, LinkKind kind) { +bool SceneHost::unlink(const std::string &nodeId, int kind) { SceneNode *n = findById(nodeId); if (!n) return false; for (auto it = n->links.begin(); it != n->links.end(); ++it) { @@ -571,7 +573,7 @@ bool SceneHost::unlink(const std::string &nodeId) { return true; } -SceneLink *SceneHost::findLink(SceneNode *node, LinkKind kind) { +SceneLink *SceneHost::findLink(SceneNode *node, int kind) { if (!node) return nullptr; for (auto &l : node->links) { if (l.kind == kind) return &l; @@ -579,7 +581,7 @@ SceneLink *SceneHost::findLink(SceneNode *node, LinkKind kind) { return nullptr; } -const SceneLink *SceneHost::findLink(const SceneNode *node, LinkKind kind) const { +const SceneLink *SceneHost::findLink(const SceneNode *node, int kind) const { if (!node) return nullptr; for (const auto &l : node->links) { if (l.kind == kind) return &l; diff --git a/src/modules/scene/SceneHost.h b/src/modules/scene/SceneHost.h index 1f308abb..1be44361 100644 --- a/src/modules/scene/SceneHost.h +++ b/src/modules/scene/SceneHost.h @@ -12,7 +12,10 @@ #include #include "scene/NodeDesc.h" +#include "scene/SceneLink.h" +// Only pointers to these cross the interface, so a forward declaration is +// enough: scene links carry them as void* and the owning module does the casts. namespace eve::graphics { class Renderable2D; class Renderable3D; @@ -30,29 +33,6 @@ class Source; namespace eve::scene { -/** Link target kind for the generic per-node link system. */ -enum class LinkKind { - None = 0, - Renderable2D, - Renderable3D, - Physics2D, - Physics3D, - Camera3D, - Audio3D, -}; - -/** - * One generic link between a scene node and an external object (renderable, - * physics body, camera, audio source). A node can hold several links of - * different kinds; re-linking the same kind replaces the target. - * syncMode: 0 = node → target (node authoritative), 1 = target → node. - */ -struct SceneLink { - LinkKind kind = LinkKind::None; - void *target = nullptr; - int syncMode = 0; -}; - /** * Retained scene node (arena). Conceptual GameObject; isomorphic to eve::ui::UINode. */ @@ -241,9 +221,15 @@ public: std::vector collectIdsVisible(bool visible = true); /** - * Link an external object to a node id. Survives reconcile/rebuild by id. - * After TransformSystem, the link is synced per kind (and syncMode). + * Attach an external object to a node id. Survives reconcile/rebuild by id. + * After TransformSystem the link is synced per kind (and syncMode). + * + * `kind` is an id from scene::registerLinkKind, so any module can attach to + * the tree; the typed helpers below are conveniences for the kinds the + * engine ships. Returns false when the node or the kind is unknown. */ + bool link(const std::string &nodeId, int kind, void *target, int syncMode = 0); + bool linkRenderable2D(const std::string &nodeId, graphics::Renderable2D *r); bool linkRenderable3D(const std::string &nodeId, graphics::Renderable3D *r); bool linkPhysics2D(const std::string &nodeId, physics::Body *b, int syncMode = 0); @@ -252,12 +238,12 @@ public: bool linkAudio3D(const std::string &nodeId, audio::Source *s); /** Remove all links of a kind; returns true if a link was removed. */ - bool unlink(const std::string &nodeId, LinkKind kind); + bool unlink(const std::string &nodeId, int kind); /** Remove every link on the node. */ bool unlink(const std::string &nodeId); - SceneLink *findLink(SceneNode *node, LinkKind kind); - const SceneLink *findLink(const SceneNode *node, LinkKind kind) const; + SceneLink *findLink(SceneNode *node, int kind); + const SceneLink *findLink(const SceneNode *node, int kind) const; int linkCount(const std::string &nodeId); /** Reparent by id; empty parentId detaches (parent = -1). Cycle-safe. */ diff --git a/src/modules/scene/SceneLink.cpp b/src/modules/scene/SceneLink.cpp new file mode 100644 index 00000000..0e8ef45d --- /dev/null +++ b/src/modules/scene/SceneLink.cpp @@ -0,0 +1,58 @@ +#include "scene/SceneLink.h" + +#include +#include +#include + +namespace eve::scene { +namespace { + +struct Entry { + std::string name; + LinkOps ops; +}; + +/** + * Function-local so registration from another module's static initialiser + * cannot run before the table exists. Ids are indices and never reused, so a + * SceneLink's kind stays valid for the life of the process. + */ +std::vector& table() { + static std::vector t; + return t; +} + +} // namespace + +int registerLinkKind(const LinkOps& ops) { + if (!ops.kind || !ops.pushWorld) return -1; + const int existing = findLinkKind(ops.kind); + if (existing >= 0) return existing; + table().push_back({ops.kind, ops}); + // Point the stored ops at our own copy of the name so a caller may pass a + // temporary. + Entry& e = table().back(); + e.ops.kind = e.name.c_str(); + return int(table().size()) - 1; +} + +int findLinkKind(const char* kind) { + if (!kind) return -1; + const auto& t = table(); + for (size_t i = 0; i < t.size(); ++i) + if (t[i].name == kind) return int(i); + return -1; +} + +const LinkOps* linkOps(int kindId) { + const auto& t = table(); + if (kindId < 0 || size_t(kindId) >= t.size()) return nullptr; + return &t[size_t(kindId)].ops; +} + +const char* linkKindName(int kindId) { + const LinkOps* ops = linkOps(kindId); + return ops ? ops->kind : ""; +} + +} // namespace eve::scene diff --git a/src/modules/scene/SceneLink.h b/src/modules/scene/SceneLink.h new file mode 100644 index 00000000..cdae3b31 --- /dev/null +++ b/src/modules/scene/SceneLink.h @@ -0,0 +1,74 @@ +#pragma once + +// Attaching things to scene nodes. +// +// A link keeps an external object -- a renderable, a physics body, a camera, an +// audio source -- in step with the node it hangs off. The set of kinds used to +// be a closed enum with a switch in TransformSystem, so scene had to include +// graphics, physics and audio, and no other module could ever attach to the +// tree without editing this file. +// +// Kinds are registered instead. The module that owns the target type supplies +// the three operations, keeps the casts on its own side where the type is +// complete, and scene never learns what a Renderable3D is. +// +// Dispatch is through a function-pointer table, not virtual calls: link sync +// runs for every dirty node every frame. + +#include "common/Export.h" + +#include + +namespace eve::scene { + +struct SceneNode; + +/** Behaviour for one kind of link target, supplied by the owning module. */ +struct LinkOps { + /** Stable name, e.g. "renderable3d". Registration is idempotent by name. */ + const char* kind = nullptr; + + /** Node -> target, after the node's world matrix is recomputed. */ + void (*pushWorld)(const SceneNode& node, void* target) = nullptr; + + /** + * Target -> node, before world propagation, for links whose target is + * authoritative (syncMode 1). Null when the kind cannot drive a node. + */ + void (*pullWorld)(SceneNode& node, void* target) = nullptr; + + /** + * Whether the target is still alive. Dead links are dropped rather than + * followed into freed memory. Null means always alive. + */ + bool (*alive)(const void* target) = nullptr; +}; + +/** + * Register a link kind and return its id. Registering the same name twice + * returns the first id and leaves the original operations in place, so a + * module reloaded as a plugin cannot silently swap them out. + */ +EVENGINE_API int registerLinkKind(const LinkOps& ops); + +/** Id for a previously registered kind, or -1. */ +EVENGINE_API int findLinkKind(const char* kind); + +/** Operations for an id, or nullptr when the id is unknown. */ +EVENGINE_API const LinkOps* linkOps(int kindId); + +/** Name for an id, or "" when the id is unknown. */ +EVENGINE_API const char* linkKindName(int kindId); + +/** + * One link between a scene node and an external object. The node may hold + * several links of different kinds; re-linking the same kind replaces the + * target. syncMode: 0 = node drives target, 1 = target drives node. + */ +struct SceneLink { + int kind = -1; + void* target = nullptr; + int syncMode = 0; +}; + +} // namespace eve::scene diff --git a/src/modules/scene/TransformSystem.cpp b/src/modules/scene/TransformSystem.cpp index dd88170d..69cdb6d4 100644 --- a/src/modules/scene/TransformSystem.cpp +++ b/src/modules/scene/TransformSystem.cpp @@ -1,14 +1,7 @@ #include "scene/TransformSystem.h" #include "scene/SceneHost.h" - -#ifdef EVENGINE_SCENE_AUDIO -#include "audio/Source.h" -#endif -#include "graphics/RenderSystem.h" -#include "graphics/RenderSystem3D.h" -#include "physics/Body.h" -#include "physics/Body3D.h" +#include "scene/SceneLink.h" #include #include @@ -40,50 +33,6 @@ void updateNode(SceneHost::Tree &tree, int nodeIndex, const glm::mat4 &parentWor } } -void writeWorldToRenderable3D(const SceneNode &n, graphics::Renderable3D *r) { - auto t = r->transform(); - const glm::mat4 &w = n.world; - t->x = w[3][0]; - t->y = w[3][1]; - t->z = w[3][2]; - - glm::vec3 c0(w[0]); - glm::vec3 c1(w[1]); - glm::vec3 c2(w[2]); - t->sx = glm::length(c0); - t->sy = glm::length(c1); - t->sz = glm::length(c2); - if (t->sx > 1e-8f) c0 /= t->sx; - if (t->sy > 1e-8f) c1 /= t->sy; - if (t->sz > 1e-8f) c2 /= t->sz; - - glm::mat3 rot(c0, c1, c2); - glm::quat q = glm::quat_cast(rot); - glm::vec3 euler = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) - t->pitch = euler.x; - t->yaw = euler.y; - t->roll = euler.z; - - r->meshRenderer()->visible = n.visible; -} - -void writeWorldToRenderable2D(const SceneNode &n, graphics::Renderable2D *r) { - auto t = r->transform(); - const glm::mat4 &w = n.world; - t->x = w[3][0]; - t->y = w[3][1]; - - glm::vec3 c0(w[0]); - glm::vec3 c1(w[1]); - t->sx = glm::length(c0); - t->sy = glm::length(c1); - if (t->sx > 1e-8f) c0 /= t->sx; - // 2D rotation from first column in XY - t->rot = std::atan2(c0.y, c0.x); - - r->sprite()->visible = n.visible; -} - /** syncMode == 1: pull target state into the node before world propagation. */ void pullTargets(SceneHost *host) { if (!host) return; @@ -92,29 +41,9 @@ void pullTargets(SceneHost *host) { SceneNode &n = t->nodes[i]; for (auto &l : n.links) { if (l.syncMode != 1 || !l.target) continue; - switch (l.kind) { - case LinkKind::Physics2D: { - auto *b = static_cast(l.target); - n.x = b->getX(); - n.y = b->getY(); - n.roll = b->getAngle(); - break; - } - case LinkKind::Physics3D: { - auto *b = static_cast(l.target); - n.x = b->getX(); - n.y = b->getY(); - n.z = b->getZ(); - glm::quat q(b->getRotW(), b->getRotX(), b->getRotY(), b->getRotZ()); - glm::vec3 e = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) - n.pitch = e.x; - n.yaw = e.y; - n.roll = e.z; - break; - } - default: - break; - } + const LinkOps *ops = linkOps(l.kind); + if (!ops || !ops->pullWorld) continue; + ops->pullWorld(n, l.target); host->markSubtreeDirty(int(i)); } } @@ -142,43 +71,8 @@ void updateNodeIncremental(SceneHost::Tree &tree, int nodeIndex, /** syncMode == 0: push node state into the linked target. */ void pushTarget(const SceneNode &n, const SceneLink &l) { if (!l.target) return; - switch (l.kind) { - case LinkKind::Renderable2D: - writeWorldToRenderable2D(n, static_cast(l.target)); - break; - case LinkKind::Renderable3D: - writeWorldToRenderable3D(n, static_cast(l.target)); - break; - case LinkKind::Physics2D: { - auto *b = static_cast(l.target); - b->setPosition(n.x, n.y); - b->setAngle(n.roll); - break; - } - case LinkKind::Physics3D: { - auto *b = static_cast(l.target); - b->setPosition(n.x, n.y, n.z); - glm::quat q = glm::angleAxis(n.yaw, glm::vec3(0.f, 1.f, 0.f)) * - glm::angleAxis(n.pitch, glm::vec3(1.f, 0.f, 0.f)) * - glm::angleAxis(n.roll, glm::vec3(0.f, 0.f, 1.f)); - b->setRotation(q.x, q.y, q.z, q.w); - break; - } - case LinkKind::Camera3D: { - auto *c = static_cast(l.target); - c->setEye(n.world[3][0], n.world[3][1], n.world[3][2]); - break; - } - case LinkKind::Audio3D: { -#ifdef EVENGINE_SCENE_AUDIO - auto *s = static_cast(l.target); - s->setPosition(n.world[3][0], n.world[3][1], n.world[3][2]); -#endif - break; - } - default: - break; - } + const LinkOps *ops = linkOps(l.kind); + if (ops) ops->pushWorld(n, l.target); } void syncLinks(SceneHost *host) { @@ -192,25 +86,15 @@ void syncLinks(SceneHost *host) { } /** - * Best-effort liveness check for a linked target. ECS entities (renderables, - * cameras) die via ecs::DestroyEntity; physics bodies expose raw()/isValid(). - * audio::Source has no liveness API, so it is always assumed alive. + * Best-effort liveness check for a linked target. Each kind knows how to answer + * for its own type; a kind that cannot tell (an audio source has no liveness + * API) leaves `alive` null and is assumed alive. */ bool linkTargetAlive(const SceneLink &l) { if (!l.target) return false; - switch (l.kind) { - case LinkKind::Renderable2D: - case LinkKind::Renderable3D: - case LinkKind::Camera3D: - return ecs::is_entity_visible(static_cast(l.target)); - case LinkKind::Physics2D: - return static_cast(l.target)->raw() != nullptr; - case LinkKind::Physics3D: - return static_cast(l.target)->isValid(); - case LinkKind::Audio3D: - default: - return true; - } + const LinkOps *ops = linkOps(l.kind); + if (!ops || !ops->alive) return true; + return ops->alive(l.target); } /** Drop links whose target was destroyed (prevents dangling-pointer sync). */ diff --git a/src/modules/sceneloader/SceneLoader.cpp b/src/modules/sceneloader/SceneLoader.cpp index 6b81224d..29db8302 100644 --- a/src/modules/sceneloader/SceneLoader.cpp +++ b/src/modules/sceneloader/SceneLoader.cpp @@ -652,7 +652,7 @@ bool SceneLoader::applyTreeDiff(scene::SceneHost *host, const scene::NodeDesc &n // Destroy Renderable3D of removed GameObjects. for (const auto &kv : oldNodes) { if (!newIds.count(kv.first)) { - if (const auto *l = host->findLink(kv.second, scene::LinkKind::Renderable3D)) { + if (const auto *l = host->findLink(kv.second, scene::findLinkKind("renderable3d"))) { destroyRenderable(static_cast(l->target)); } } @@ -728,7 +728,7 @@ bool SceneLoader::applyTreeDiff(scene::SceneHost *host, const scene::NodeDesc &n r = nullptr; } if (r) { - n.links.push_back(scene::SceneLink{scene::LinkKind::Renderable3D, r, 0}); + n.links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0}); } } } @@ -763,7 +763,7 @@ void SceneLoader::linkMeshNodes(scene::SceneHost *host, const MeshSlotMap &slots for (const auto &kv : slots) { scene::SceneNode *n = host->findById(kv.first); if (!n || kv.second.empty()) continue; - if (host->findLink(n, scene::LinkKind::Renderable3D)) continue; + if (host->findLink(n, scene::findLinkKind("renderable3d"))) continue; const MeshSlot &slot = kv.second[0]; graphics::Mesh *mesh = nullptr; auto it = shared.find(slot.mesh); @@ -810,7 +810,7 @@ void SceneLoader::linkMeshNodes(scene::SceneHost *host, const MeshSlotMap &slots if (albedo) r->setTexture(albedo); if (normal) r->setNormalTexture(normal); if (height) r->setHeightTexture(height); - n->links.push_back(scene::SceneLink{scene::LinkKind::Renderable3D, r, 0}); + n->links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0}); } } @@ -963,7 +963,7 @@ void SceneLoader::unload(const std::string &path) { if (it->second.host) { auto t = it->second.host->tree(); for (auto &n : t->nodes) { - if (const auto *l = it->second.host->findLink(&n, scene::LinkKind::Renderable3D)) { + if (const auto *l = it->second.host->findLink(&n, scene::findLinkKind("renderable3d"))) { destroyRenderable(static_cast(l->target)); } n.links.clear(); diff --git a/test/scene.cpp b/test/scene.cpp index 2a1a1972..dea89626 100644 --- a/test/scene.cpp +++ b/test/scene.cpp @@ -475,9 +475,9 @@ TEST_CASE("Scene.link.genericMultiLinkAndSync") { CHECK(approxEq(n->z, 30.f)); // unlink by kind keeps the other links - CHECK(h->unlink("soldier", LinkKind::Physics3D)); + CHECK(h->unlink("soldier", findLinkKind("physics3d"))); CHECK_EQ(h->linkCount("soldier"), 2); - CHECK(!h->unlink("soldier", LinkKind::Physics3D)); + CHECK(!h->unlink("soldier", findLinkKind("physics3d"))); // full rebuild preserves remaining links by id h->setTree(node("root", {node("soldier")})); @@ -519,7 +519,7 @@ TEST_CASE("Scene.link.physics2D") { CHECK(approxEq(n->y, 40.f)); CHECK(approxEq(n->roll, 0.5f)); - CHECK(h->unlink("p", LinkKind::Physics2D)); + CHECK(h->unlink("p", findLinkKind("physics2d"))); CHECK_EQ(h->linkCount("p"), 0); w.destroy(); } diff --git a/test/sceneloader.cpp b/test/sceneloader.cpp index a98cdf1b..d846a9c7 100644 --- a/test/sceneloader.cpp +++ b/test/sceneloader.cpp @@ -204,7 +204,7 @@ TEST_CASE("SceneLoader.apply.preservesUnchangedRenderableIdentity") { // The unchanged GameObject keeps its linked Renderable3D — no rebuild / re-upload. REQUIRE_EQ(after->links.size(), 1u); CHECK(after->links[0].target == r); - CHECK(int(after->links[0].kind) == int(eve::scene::LinkKind::Renderable3D)); + CHECK(after->links[0].kind == eve::scene::findLinkKind("renderable3d")); CHECK(h->findById("keep2") != nullptr); CHECK(h->findById("gone") == nullptr); @@ -258,7 +258,7 @@ TEST_CASE("SceneLoader.load.buildsGameObjectTreeWithRenderables") { std::vector linked = h->findAllLinked(); REQUIRE_EQ(linked.size(), 1u); REQUIRE_EQ(linked[0]->links.size(), 1u); - CHECK(int(linked[0]->links[0].kind) == int(eve::scene::LinkKind::Renderable3D)); + CHECK(linked[0]->links[0].kind == eve::scene::findLinkKind("renderable3d")); auto *r = static_cast(linked[0]->links[0].target); REQUIRE(r != nullptr); CHECK(r->meshRenderer()->mesh != nullptr);