Query

查询是在世界中搜索实体及其组件的机制

  • 所有查询无需缓存,在栈上分配,可以即时使用
  • 支持按组件、标签、实体状态和集群过滤
  • 两种迭代模式:Strict(默认,更快)和 Flexible(允许修改其他实体上的过滤类型)

过滤器

用于描述过滤的类型。每个占 1 字节且无需初始化。

// 假设世界中有 5 个实体:
//             Components                 Tags           EntityType
// Entity 1:  Position, Velocity         Unit           Npc
// Entity 2:  Position, Name             Player         Npc
// Entity 3:  Position, Velocity, Name   Unit, Player   Npc
// Entity 4:  Velocity                   —              Bullet
// Entity 5:  Position■, Velocity        Unit           Bullet
//            (■ = disabled)
//
// 以下示例展示每个过滤器匹配哪些实体

组件:

// All — 所有已启用的组件都存在(1 到 8 个类型)
All<Position, Velocity, Direction> all = default;

// AllOnlyDisabled — 所有已禁用的组件都存在
AllOnlyDisabled<Position> disabled = default;

// AllWithDisabled — 所有组件都存在(任何状态)
AllWithDisabled<Position, Velocity> any = default;

// None — 已启用的组件不存在(1 到 8 个类型)
None<Position, Name> none = default;

// NoneWithDisabled — 组件不存在(任何状态)
NoneWithDisabled<Position> noneAll = default;

// Any — 至少一个已启用的组件存在(2 到 8 个类型)
Any<Position, Velocity> any = default;

// AnyOnlyDisabled — 至少一个已禁用的
AnyOnlyDisabled<Position, Velocity> anyDis = default;

// AnyWithDisabled — 至少一个(任何状态)
AnyWithDisabled<Position, Velocity> anyAll = default;

// 以上实体的结果:
// All<Position, Velocity>              → 1, 3
// AllOnlyDisabled<Position>            → 5
// AllWithDisabled<Position, Velocity>  → 1, 3, 5
// None<Name>                           → 1, 4, 5
// NoneWithDisabled<Position>           → 4
// Any<Position, Name>                  → 1, 2, 3
// AnyOnlyDisabled<Position, Velocity>  → 5
// AnyWithDisabled<Position, Name>      → 1, 2, 3, 5

标签:

标签使用与组件相同的过滤器 — All<>None<>Any<> 及其变体。没有单独的标签过滤器类型。

// All — 所有指定的标签都存在(1 到 8 个类型)
All<Unit, Player> tagAll = default;

// None — 指定的标签不存在(1 到 8 个类型)
None<Unit, Player> tagNone = default;

// Any — 至少一个指定的标签存在(2 到 8 个类型)
Any<Unit, Player> tagAny = default;

// 以上实体的结果:
// All<Unit, Player>  → 3
// None<Unit>         → 2, 4
// Any<Unit, Player>  → 1, 2, 3, 5

变更追踪:

// AllAdded — 自上次 ClearTracking 以来所有指定组件被添加(1 到 5 个类型)
AllAdded<Position> added = default;
AllAdded<Position, Velocity> addedMulti = default;

// AnyAdded — 至少一个指定组件被添加(2 到 5 个类型)
AnyAdded<Position, Velocity> anyAdded = default;

// NoneAdded — 没有任何指定组件被添加(1 到 5 个类型)
NoneAdded<Position> noneAdded = default;

// AllDeleted — 自上次 ClearTracking 以来所有指定组件被删除(1 到 5 个类型)
AllDeleted<Position> deleted = default;

// AnyDeleted — 至少一个被删除(2 到 5 个类型)
AnyDeleted<Position, Velocity> anyDeleted = default;

// NoneDeleted — 没有任何被删除(1 到 5 个类型)
NoneDeleted<Position> noneDeleted = default;

// AllChanged — 自上次 ClearChangedTracking 以来所有指定组件被更改(1 到 5 个类型)
// 需要 ComponentTypeConfig.TrackChanged = true
AllChanged<Position> changed = default;

// AnyChanged — 至少一个被更改(2 到 5 个类型)
AnyChanged<Position, Velocity> anyChanged = default;

// NoneChanged — 没有任何被更改(1 到 5 个类型)
NoneChanged<Position> noneChanged = default;

// AllAdded / AnyAdded / NoneAdded / AllDeleted / AnyDeleted / NoneDeleted
// 同样适用于标签 — 对组件和标签使用相同的过滤器

// Created — 自上次 ClearCreatedTracking 以来实体被创建
// (需要 WorldConfig.TrackCreated = true,无类型参数)
Created created = default;

// 与其他过滤器组合
foreach (var entity in W.Query<AllAdded<Position>, All<Velocity, Unit>>().Entities()) {
    ref var pos = ref entity.Ref<Position>();
    // 处理新添加 Position 的实体
}

在基于委托的迭代(For)中,ref 参数将组件标记为 Changed,而 in 参数不标记。使用 in 进行只读访问以避免不必要的 Changed 标记。详见 Changed Tracking

实体类型:

// EntityIs — 精确此实体类型(1 个类型参数)
EntityIs<Bullet> entityIs = default;

// EntityIsNot — 排除实体类型(从 1 到 5 个类型)
EntityIsNot<Effect> entityIsNot = default;

// EntityIsAny — 匹配指定实体类型中的任何一个(从 2 到 5 个类型)
EntityIsAny<Bullet, Rocket> entityIsAny = default;

// 以上实体的结果:
// EntityIs<Npc>              → 1, 2, 3
// EntityIsNot<Bullet>        → 1, 2, 3
// EntityIsAny<Npc, Bullet>   → 1, 2, 3, 4, 5

And / Or — 复合过滤器:

AndOr 允许将多个过滤器组合为一个类型。适用场景:

  • 将复合过滤器作为一个泛型参数传递 — 存储在字段中、传递给方法、用作类型参数
  • 构建基本类型无法表达的过滤器 — 例如,”拥有组件集 A 组件集 B 的实体”

And — 所有条件必须匹配(2 到 6 个过滤器):

And<All<Position, Velocity>, None<Name>, Any<Unit, Player>> filter = default;

// 通过工厂方法(类型推断)
var filter = And.By(
    default(All<Position, Velocity>),
    default(None<Name>),
    default(Any<Unit, Player>)
);

// 用例:将复合过滤器传递给辅助方法
void ProcessMovable(And<All<Position, Velocity>, None<Frozen>> filter) {
    foreach (var entity in W.Query(filter).Entities()) {
        entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
    }
}

Or — 至少一个条件必须匹配(2 到 6 个过滤器):

Or 可以构建基本过滤器类型无法表达的组合式复杂过滤。

// 近战战士或远程战士 — 完全不同的组件集,
// 无法用单个 All/Any/None 组合表达
Or<All<MeleeWeapon, Damage>, All<RangedWeapon, Ammo>> fighters = default;

// 当 Position 被添加、删除或修改时重建空间索引
Or<AllAdded<Position>, AllDeleted<Position>, AllChanged<Position>> spatialChanged = default;

// 处理 UI 按钮 (ClickArea + Label) 和世界交互对象 (Collider + Interaction)
Or<All<ClickArea, Label>, All<Collider, Interaction>> clickable = default;

// 通过工厂方法
var filter = Or.By(
    default(All<MeleeWeapon, Damage>),
    default(All<RangedWeapon, Ammo>)
);

// 以上实体的结果:
// Or<All<Position, Velocity>, All<Position, Name>>
// Entity 1: Pos✓ Vel✓         → ✓ (通过第一个)
// Entity 2: Pos✓ Name✓        → ✓ (通过第二个)
// Entity 3: Pos✓ Vel✓ Name✓   → ✓ (通过两个)
// Entity 4: Pos✗              → ✗
// → 结果:1, 2, 3, 5

嵌套:

// And 和 Or 可以嵌套以实现任意复杂的逻辑
// (A 和 B 和 C) 或 (A 和 B 和 D):
Or<All<A, B, C>, All<A, B, D>> complex = default;

// 所有可见实体中,活着的单位或活跃的效果:
And<All<Visible>, Or<All<Unit, Alive>, All<Effect, Active>>> visibleAlive = default;

实体迭代

// 遍历所有实体(无过滤)
foreach (var entity in W.Query().Entities()) {
    Console.WriteLine(entity.PrettyString);
}

// 通过 generic 过滤(1 到 8 个过滤器)
foreach (var entity in W.Query<All<Position, Velocity>>().Entities()) {
    entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}

// 多个过滤器
foreach (var entity in W.Query<All<Position, Velocity>, None<Name>>().Entities()) {
    entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}

// 通过过滤器值
var all = default(All<Position, Velocity>);
foreach (var entity in W.Query(all).Entities()) {
    entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}

// 通过 And/Or — 将过滤器分组为一个类型,用于传递给方法或存储在字段中
var filter = default(And<All<Position, Velocity>, None<Name>>);
foreach (var entity in W.Query(filter).Entities()) {
    entity.Ref<Position>().Value += entity.Read<Velocity>().Value;
}

// Flexible 模式 — 允许修改其他实体上的过滤类型
foreach (var entity in W.Query<All<Position>>().EntitiesFlexible()) {
    // ...
}

// 查找第一个匹配的实体
if (W.Query<All<Position>>().Any(out var found)) {
    // found — 第一个具有 Position 的实体
}

// 获取唯一实体(debug 模式下如果找到多个则报错)
if (W.Query<All<Position>>().One(out var single)) {
    // single — 唯一具有 Position 的实体
}

// 统计匹配的实体数量(完整遍历)
int count = W.Query<All<Position>>().EntitiesCount();

基于委托的迭代 (For)

通过委托优化迭代 — 底层展开循环。

// 遍历所有实体
W.Query().For(entity => {
    Console.WriteLine(entity.PrettyString);
});

// 按组件迭代(1 到 6 个类型)
// 委托中的组件自动作为 All 过滤器
W.Query().For(static (ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value;
});

// 在委托中包含实体
W.Query().For(static (W.Entity entity, ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value;
});

// 使用用户数据(避免委托分配)
W.Query().For(deltaTime, static (ref float dt, ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value * dt;
});

// 使用 ref 数据(用于累积结果)
int count = 0;
W.Query().For(ref count, static (ref int counter, W.Entity entity, ref Position pos) => {
    counter++;
});

// 使用多参数元组
W.Query().For((deltaTime, gravity), static (ref (float dt, float g) data, ref Position pos, ref Velocity vel) => {
    vel.Value += data.g * data.dt;
    pos.Value += vel.Value * data.dt;
});

只读组件 (Read):

当组件只被读取而不被修改时,在委托中使用 in 代替 ref。这告诉变更追踪系统不要将组件标记为已更改。

// 最后 N 个组件通过 `in` 设为只读
W.Query().For(static (ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value;  // Position — 可写 (ref),Velocity — 只读 (in)
});

// 所有组件只读
W.Query().For(static (in Position pos, in Velocity vel) => {
    Console.WriteLine(pos.Value + vel.Value);
});

// 带实体
W.Query().For(static (W.Entity entity, ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value;
});

// 带用户数据
W.Query().For(ref result, static (ref float res, in Position pos, in Velocity vel) => {
    res += pos.Value.Length;
});

Read 变体在启用变更追踪时可用(默认启用)。可通过 FFS_ECS_DISABLE_CHANGED_TRACKING 定义禁用。

附加过滤:

// 委托中的组件作为 All 过滤器,
// 附加过滤器直接在 Query 上指定,无需指定委托中的组件
W.Query<Any<Unit, Player>>().For(static (ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value;
});

// 多个过滤器
W.Query<None<Name>, Any<Unit, Player>>().For(static (ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value;
});

// 通过值
var filter = default(Any<Unit, Player>);
W.Query(filter).For(static (ref Position pos, in Velocity vel) => {
    pos.Value += vel.Value;
});

实体和组件状态:

W.Query().For(
    static (ref Position pos, ref Velocity vel) => {
        // ...
    },
    entities: EntityStatusType.Disabled,    // Enabled(默认)、Disabled、Any
    components: ComponentStatus.Disabled    // Enabled(默认)、Disabled、Any
);

首次匹配时提前退出的迭代。搜索委托中的所有组件均为只读(in)。

if (W.Query().Search(out W.Entity found,
    (W.Entity entity, in Position pos, in Health health) => {
        return pos.Value.x > 100 && health.Current < 50;
    })) {
    // found — 第一个满足条件的实体
}

函数结构体 (IQuery / IQueryBlock)

用函数结构体代替委托 — 用于优化、传递状态或提取逻辑。 函数结构体使用 WorldQuery 上的 fluent builder API — 与委托不同,组件类型不通过 For 的泛型参数指定,而通过构建器链指定。

IQuery — 逐实体回调:

接口层次结构使用嵌套类型控制写入/读取访问(总共 1 到 6 个组件):

  • IQuery.Write<T0, T1> — 所有组件可写(ref
  • IQuery.Read<T0, T1> — 所有组件只读(in
  • IQuery.Write<T0>.Read<T1> — 前面可写,后面只读
// 全部可写 — IQuery.Write
readonly struct MoveFunction : W.IQuery.Write<Position, Velocity> {
    public void Invoke(W.Entity entity, ref Position pos, ref Velocity vel) {
        pos.Value += vel.Value;
    }
}

// Fluent API:Write<...>() 指定可写组件,然后 For<TFunction>() 执行
W.Query().Write<Position, Velocity>().For<MoveFunction>();

// 通过值
W.Query().Write<Position, Velocity>().For(new MoveFunction());

// 通过 ref(迭代后保留状态)
var func = new MoveFunction();
W.Query().Write<Position, Velocity>().For(ref func);

// 混合写入/读取 — IQuery.Write<>.Read<>
readonly struct ApplyVelocity : W.IQuery.Write<Position>.Read<Velocity> {
    public void Invoke(W.Entity entity, ref Position pos, in Velocity vel) {
        pos.Value += vel.Value;
    }
}

// 链:Write<可写>().Read<只读>().For<TFunction>()
W.Query().Write<Position>().Read<Velocity>().For<ApplyVelocity>();

// 全部只读 — IQuery.Read
readonly struct PrintPositions : W.IQuery.Read<Position, Velocity> {
    public void Invoke(W.Entity entity, in Position pos, in Velocity vel) {
        Console.WriteLine(pos.Value + vel.Value);
    }
}

W.Query().Read<Position, Velocity>().For<PrintPositions>();

// 附加过滤
W.Query<None<Name>, Any<Unit, Player>>()
    .Write<Position, Velocity>().For<MoveFunction>();

// 组合系统和 IQuery
public struct MoveSystem : ISystem, W.IQuery.Write<Position>.Read<Velocity> {
    private float _speed;

    public void Update() {
        _speed = W.GetResource<GameConfig>().Speed;
        W.Query<All<Unit>>()
            .Write<Position>().Read<Velocity>().For(ref this);
    }

    public void Invoke(W.Entity entity, ref Position pos, in Velocity vel) {
        pos.Value += vel.Value * _speed;
    }
}

WorldQuery 方法

委托 — 组件类型从 lambda 推导:

方法 组件
For(delegate) 1–6,每个组件 refin
ForParallel(delegate) 1–6,每个组件 refin
Search(out entity, delegate) 1–6,全部 in

函数结构体 — 通过构建器指定组件访问:

方法 组件 访问
Write<1‑6>() 1–6 全部 ref
Write<1‑5>().Read<1‑5>() 2–6 合计 前面 ref,后面 in
Read<1‑6>() 1–6 全部 in

块函数结构体 — 相同模式,仅限 unmanaged:

方法 组件 访问
WriteBlock<1‑6>() 1–6 全部 Block<T>
WriteBlock<1‑5>().Read<1‑5>() 2–6 合计 Block<T> + BlockR<T>
ReadBlock<1‑6>() 1–6 全部 BlockR<T>

每个构建器提供 For<F>()ForParallel<F>()Read / ReadBlock 需要变更追踪(默认启用,通过 FFS_ECS_DISABLE_CHANGED_TRACKING 禁用)。


并行处理

并行处理需要在创建世界时启用:在 WorldConfig 中设置 ParallelQueryType.MaxThreadsCountParallelQueryType.CustomThreadsCount 并指定 CustomThreadCount。 在并行迭代中只允许修改和销毁当前迭代的实体。禁止:创建实体、修改其他实体、读取事件。发送事件(SendEvent)是线程安全的(在没有同时读取同一类型时,详见事件)。始终使用 QueryMode.Strict

// 委托是第一个参数,minEntitiesPerThread 是命名参数(默认 256)
W.Query().ForParallel(
    static (W.Entity entity, ref Position pos, in Velocity vel) => {
        pos.Value += vel.Value;
    },
    minEntitiesPerThread: 50000
);

// 不带实体 — 仅组件
W.Query().ForParallel(
    static (ref Position pos, in Velocity vel) => {
        pos.Value += vel.Value;
    },
    minEntitiesPerThread: 50000
);

// 使用用户数据
W.Query().ForParallel(deltaTime,
    static (ref float dt, ref Position pos, in Velocity vel) => {
        pos.Value += vel.Value * dt;
    },
    minEntitiesPerThread: 50000
);

// 带过滤
W.Query<None<Name>, Any<Unit, Player>>().ForParallel(
    static (W.Entity entity) => {
        entity.Add<Name>();
    },
    minEntitiesPerThread: 50000
);

// 通过函数结构体
W.Query().Write<Position>().Read<Velocity>().ForParallel<ApplyVelocity>(
    minEntitiesPerThread: 50000
);

// workersLimit — 限制线程数量(0 = 使用所有可用线程)
W.Query().ForParallel(
    static (ref Position pos) => { /* ... */ },
    minEntitiesPerThread: 10000,
    workersLimit: 4
);

块迭代 (ForBlock)

通过函数结构体进行低级别迭代 — 对于 unmanaged 组件,提供带有数据数组直接指针的 Block<T>(可写)和 BlockR<T>(只读)包装器。

接口层次结构与 IQuery 相同(总共 1 到 6 个 unmanaged 组件):

  • IQueryBlock.Write<T0, T1> — 所有组件可写(Block<T>
  • IQueryBlock.Read<T0, T1> — 所有组件只读(BlockR<T>
  • IQueryBlock.Write<T0>.Read<T1> — 前面可写,后面只读
// 全部可写 — IQueryBlock.Write
readonly struct MoveBlock : W.IQueryBlock.Write<Position, Velocity> {
    public void Invoke(uint count, EntityBlock entitiesBlock,
                       Block<Position> positions, Block<Velocity> velocities) {
        for (uint i = 0; i < count; i++) {
            positions[i].Value += velocities[i].Value;
        }
    }
}

// Fluent API: WriteBlock<...>().For<TFunction>()
W.Query().WriteBlock<Position, Velocity>().For<MoveBlock>();

// 混合写入/读取 — WriteBlock<>.Read<>
readonly struct ApplyVelocityBlock : W.IQueryBlock.Write<Position>.Read<Velocity> {
    public void Invoke(uint count, EntityBlock entitiesBlock,
                       Block<Position> positions, BlockR<Velocity> velocities) {
        for (uint i = 0; i < count; i++) {
            positions[i].Value += velocities[i].Value;
        }
    }
}

W.Query().WriteBlock<Position>().Read<Velocity>().For<ApplyVelocityBlock>();

// 全部只读 — ReadBlock<>
readonly struct SumPositionsBlock : W.IQueryBlock.Read<Position> {
    public void Invoke(uint count, EntityBlock entitiesBlock, BlockR<Position> positions) {
        for (uint i = 0; i < count; i++) {
            // 只读访问
        }
    }
}

W.Query().ReadBlock<Position>().For<SumPositionsBlock>();

// 通过 ref(保留状态)
var func = new MoveBlock();
W.Query().WriteBlock<Position, Velocity>().For(ref func);

// 并行版本
W.Query().WriteBlock<Position, Velocity>().ForParallel<MoveBlock>(minEntitiesPerThread: 50000);

批量操作

对所有匹配过滤器的实体执行批量操作 — 无需编写循环。 可以比通过 For 手动迭代快数十倍:批量操作使用位掩码而非逐个处理实体 — 在最佳情况下,为 64 个实体添加或删除组件/标签只需一次位运算。 支持链式调用 — 多个操作可以在一次遍历中执行。

// 为所有实体添加组件(1 到 5 个类型)
W.Query<All<Position>>().BatchSet(new Velocity { Value = 1f });

// 删除所有实体的组件
W.Query<All<Position, Velocity>>().BatchDelete<Velocity>();

// 禁用/启用所有实体的组件
W.Query<All<Position>>().BatchDisable<Position>();
W.Query<AllOnlyDisabled<Position>>().BatchEnable<Position>();

// 标签:设置、删除、切换、按条件应用(1 到 5 个类型)
W.Query<All<Position>>().BatchSet<Unit>();
W.Query<All<Unit>>().BatchDelete<Unit>();
W.Query<All<Position>>().BatchToggle<Unit>();
W.Query<All<Position>>().BatchApply<Unit>(true);

// 链式调用
W.Query<All<Position>>()
    .BatchSet(new Velocity { Value = 1f })
    .BatchSet<Unit>()
    .BatchDisable<Position>();

销毁和卸载实体

// 销毁所有匹配过滤器的实体
W.Query<All<Position>>().BatchDestroy();

// 带参数
W.Query<All<Unit>>().BatchDestroy(
    entities: EntityStatusType.Any,
    mode: QueryMode.Flexible
);

// 卸载所有匹配过滤器的实体
// (标记为已卸载,移除组件/标签,但保留实体 ID 和版本)
W.Query<All<Position>>().BatchUnload();

// 带参数
W.Query<All<Unit>>().BatchUnload(
    entities: EntityStatusType.Any,
    mode: QueryMode.Flexible
);

集群

所有查询方法(EntitiesForForParallelSearchBatch*BatchDestroyBatchUnload)都接受 clusters 参数:

ReadOnlySpan<ushort> clusters = stackalloc ushort[] { 2, 5, 12 };

foreach (var entity in W.Query<All<Position>>().Entities(clusters: clusters)) {
    // 仅遍历集群 2、5、12 中的实体
}

W.Query().For(static (W.Entity entity, ref Position pos) => {
    // ...
}, clusters: clusters);

QueryMode

用于 ForSearchEntities 方法:

  • QueryMode.Strict(默认)— 禁止在迭代期间修改其他实体上的过滤组件/标签类型。更快。
  • QueryMode.Flexible — 允许修改其他实体上的过滤类型,正确控制当前迭代。
var anotherEntity = W.NewEntity<Default>();
anotherEntity.Add<Position>();

// Strict:修改另一个实体的 Position — DEBUG 中报错
foreach (var entity in W.Query<All<Position>>().Entities()) {
    anotherEntity.Delete<Position>(); // DEBUG 中报错
}

// Flexible:允许修改,迭代保持稳定
foreach (var entity in W.Query<All<Position>>().EntitiesFlexible()) {
    anotherEntity.Delete<Position>(); // OK — anotherEntity 被正确排除
}

// 对于 For/Search 通过参数设置
W.Query().For(static (ref Position pos) => {
    // ...
}, queryMode: QueryMode.Flexible);

Flexible 适用于层次结构或缓存,当从一个实体的组件修改其他实体时。其他情况下推荐使用 Strict 以获得更好的性能。


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