实体销毁时需通过带版本号的关系组件和双索引实现可追踪、批量清理的依赖管理,禁止运行时检查与动态修改,确保级联清理集中化、阶段化、无迭代器失效。

实体销毁时若存在跨实体依赖(比如 ParentOf、TargetOf、Contains),不加控制会导致悬挂引用、迭代器失效或系统读取脏数据。核心解法不是“禁止依赖”,而是让依赖关系本身可追踪、可批量清理、与实体生命周期解耦。
用关系组件(Relation Component)替代裸指针或索引
传统做法是组件里存一个 Entity ID 字段,比如 struct Parent { Entity parent_id; };。这在 ECS 中极危险:删除父实体后,子实体的 parent_id 变成无效值,但系统仍可能读它;且遍历时无法快速定位所有子实体。
实操建议:
- 定义二元组关系组件,如
struct Relation { Predicate pred; Entity object; };,其中Predicate是枚举(ParentOf、MemberOf等) - 把所有
Relation存在一个独立的std::vector<relation></relation>中,按pred和object建立双索引(例如两个std::vector<uint32_t></uint32_t>分别存“谁以我为父”和“我指向谁”的偏移) - 删除实体
e时,只扫描该实体参与的关系项(利用索引定位),批量清除对应条目,不遍历全部关系数组
销毁前触发级联清理而非运行时检查
不要在每个系统里写 if (world.valid(parent_id)) { ... } —— 这是性能毒药,也掩盖设计缺陷。ECS 的优势在于“可知性”,应把依赖清理变成显式、集中、一次性的动作。
实操建议:
- 在实体销毁入口(如
World::destroy(Entity))中,先调用cleanup_relations(e),清空所有以e为主语或宾语的关系项 - 对每类关系预设级联策略:例如删
Player实体时,自动删其Inventory实体;但删Projectile时不删目标TargetOf,只清关系项 - 关系组件本身不带析构逻辑,避免在
std::vector::erase时触发额外开销;清理由世界层统一调度
避免在系统执行中修改关系结构
System 在遍历 Relation 组件时,如果另一个系统正在往同一 std::vector<relation></relation> 里插入或删除,会直接导致迭代器失效或未定义行为。这不是锁能优雅解决的问题。
实操建议:
- 关系变更(添加/删除)仅允许发生在帧开始前(
PreUpdate阶段)或帧结束后(PostUpdate),不在Update系统中直接调用add_relation() - 使用延迟队列:系统内发现需建立/断开关系时,只推入
std::vector<:tuple class="brush:php;toolbar:false;" dicate entity>></:tuple>,由专用RelationSyncSystem在固定阶段批量提交 - 若必须动态变更(如 AI 实时切换锁定目标),确保该系统独占访问
Relation存储,且其他系统对该存储只读(通过const std::vector<relation>&</relation>传入)
真正难的不是“怎么删”,而是“删完之后谁还拿着旧引用”。关系组件必须和实体 ID 一样带版本号(EntityID = uint32_t + uint16_t version),否则即使清了关系数组,外部缓存的 Entity 值仍可能误判为有效。这点极易被忽略,却直接决定线上 crash 率。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











