常见陷阱

使用 StaticEcs 时的常见错误列表。对开发者和 AI 编程助手都有用。


生命周期错误

忘记注册类型

所有组件、标签、事件、链接和多组件类型都必须在 W.Create()W.Initialize() 之间注册。使用未注册的类型会导致运行时错误。

// 错误:组件未注册
W.Create(WorldConfig.Default());
W.Initialize();
var e = W.NewEntity<Position>(0); // RuntimeError!

// 正确 — 手动注册
W.Create(WorldConfig.Default());
W.Types().Component<Position>();
W.Initialize();
var e = W.NewEntity<Position>(0); // OK

// 正确 — 从程序集自动注册所有类型
W.Create(WorldConfig.Default());
W.Types().RegisterAll();
W.Initialize();

在 Initialize 之前操作实体

NewEntity、查询和所有实体操作只有在 W.Initialize() 之后才能使用。在 Created 阶段(Create 和 Initialize 之间)调用将会失败。

重复调用 Create

在没有先调用 W.Destroy() 的情况下调用 W.Create() 是错误的。世界必须在重新创建之前销毁。


Entity 句柄错误

在 Destroy 之后使用 Entity

Entity 是一个 4 字节的 uint 槽位句柄,没有代数计数器。调用 Destroy() 后,槽位立即可供重用。旧句柄现在悄悄指向一个完全不同的实体。

var entity = W.NewEntity<Position>(0);
entity.Destroy();
// entity 现在无效——任何使用都是未定义行为
entity.Ref<Position>(); // 危险:可能访问另一个实体的数据

跨帧存储 Entity

由于 Entity 没有代数计数器,它无法检测过期。永远不要将 Entity 存储在字段、列表或其他持久结构中。请使用 EntityGID

// 错误
class MySystem { Entity targetEntity; } // 目标销毁后会过期

// 正确
class MySystem { EntityGID targetGid; } // 安全——版本检查能检测过期
// 用法:
if (targetGid.TryUnpack<WT>(out var entity)) {
    // entity 有效且存活
}

用 Entity 比较身份

Entity 的相等性仅基于 IdWithOffset (uint)。在不同时间创建的两个实体如果使用同一个槽位,它们的 Entity 值相同。请使用 EntityGID 进行身份比较。


组件错误

Add 与 Set 语义

Add<T>() 不带值是幂等的——如果组件已存在,返回现有数据的 ref,不调用任何钩子。这不是覆写。

Set(value) 总是覆写——对旧值调用 OnDelete,覆写数据,对新值调用 OnAdd。

entity.Set(new Position { Value = Vector3.Zero }); // 设置位置
entity.Add<Position>(); // 什么都不做——返回现有 {0,0,0} 的 ref
entity.Set(new Position { Value = Vector3.One }); // 覆写:OnDelete(old) → set → OnAdd(new)

实现空钩子方法

ComponentTypeInfo<T> 在启动时使用反射检测已实现的钩子。如果任何钩子有非空方法体,该组件类型的所有实例都会启用钩子分发。不要实现不需要的钩子。

// 错误:空钩子体仍然导致分发开销
public struct Foo : IComponent {
    public void OnAdd<TW>(World<TW>.Entity self) where TW : struct, IWorldType { }
}

// 正确:不需要的钩子就不要实现
public struct Foo : IComponent { }

HasOnDelete vs DataLifecycle

OnDelete 钩子和 DataLifecycle(重置为 DefaultValue)是互斥的清理路径。如果组件有 OnDelete 钩子,钩子负责清理——数据不会被重置。DataLifecycle 重置仅适用于没有 OnDelete 的组件。当配置中设置 noDataLifecycle: true 时,框架不执行任何初始化或清理。


查询错误

违反 Strict 模式

在默认的 Strict 查询模式下,禁止在迭代期间修改其他实体上的被过滤组件/标签类型。

// 在 Strict 模式下错误:
foreach (var e in W.Query<All<Position>>().Entities()) {
    otherEntity.Delete<Position>(); // 修改了另一个实体的被过滤类型!
}

// 正确:使用 Flexible 模式
foreach (var e in W.Query<All<Position>>().EntitiesFlexible()) {
    otherEntity.Delete<Position>(); // 在 Flexible 模式下 OK
}

并行迭代限制

ForParallel 期间,只能修改当前实体的数据。不要创建/销毁实体,不要修改其他实体。

不必要的 Flexible 模式

Flexible 模式在每个实体上重新检查位掩码,比 Strict 慢很多。只有在确实需要修改其他实体的被过滤类型时才使用 Flexible。


注册错误

MultiComponent 没有 Multi 包装

IMultiComponent 类型必须通过 W.Types().Multi<Item>() 注册,而不是作为普通组件。

缺少序列化设置

序列化需要:

  1. FFS.StaticPack 依赖
  2. 所有可序列化类型需要 Guid 注册:W.Types().Component<T>(new ComponentTypeConfig<T> { Guid = ... })
  3. 非 unmanaged 组件需要 Write/Read 钩子实现
  4. Unmanaged 组件可以使用 UnmanagedPackArrayStrategy<T>

资源错误

NamedResource 缓存问题

NamedResource<T> 在首次访问时缓存内部 box 引用。如果存储为 readonly 或在首次使用后按值传递,缓存副本将变得过期。

// 错误
readonly NamedResource<Config> config = new("main"); // readonly 破坏缓存

// 正确
NamedResource<Config> config = new("main"); // 可变——缓存正常工作

This site uses Just the Docs, a documentation theme for Jekyll.