mirror of
https://gitee.com/dgflash/oops-plugin-framework.git
synced 2026-06-23 19:22:47 +08:00
ecs 命名空间:统一导出所有 API,业务只需 import { ecs } from './ECS'
核心类型别名:ecs.Entity / ecs.Comp / ecs.RootSystem / ecs.ComblockSystem
接口再导出:ecs.IComp / ecs.IMatcher / ecs.IEntityEnterSystem / ecs.IEntityRemoveSystem / ecs.ISystemFirstUpdate / ecs.ISystemUpdate
实体工厂:ecs.getEntity(ctor, world?) — 创建或从对象池获取实体
动态查询:ecs.query(matcher, world?) — 按匹配器查询实体
过滤器工厂:ecs.allOf() / ecs.anyOf() / ecs.onlyOf() / ecs.excludeOf() — 组合匹配规则
单例组件:ecs.getSingleton() / ecs.addSingleton() — 全局唯一组件访问
子模块门面:ecs.world / ecs.pool / ecs.system / ecs.storage / ecs.network / ecs.serialize / ecs.entityRef
2. 驱动器 — ECSDriver.ts
封装默认世界的 init / execute / destroy 生命周期
add(system) — 向默认世界添加业务系统
init() — 初始化 ECS(并入 @ecs.register 注册的系统、拓扑排序、init)
execute(dt) — 驱动默认世界一帧
destroy() — 清理所有子系统
3. 实体 — ECSEntity.ts
组件容器:位掩码 mask + 按 tid 索引的密集数组,get/has 为 O(1)
双重重载 add():
add(组件类) — 从对象池取实例或恢复软移除缓存
add(组件实例) — 挂载外部实例(如 cc.Component),设 canRecycle=false
remove(ctor, isRecycle?):
isRecycle=true(默认):reset + 回池/释放 SoA 槽位
isRecycle=false:软移除,数据暂存 compTid2Obj,下次 add 同 tid 恢复
父子层级:addChild / removeChild,带循环引用检测
destroy():断开父子 → 移除全部组件 → 清理软移除缓存 → 回收实体 + 释放 eid
forEachComponent(cb):遍历当前挂载的所有组件
4. 组件 — ECSComp.ts
抽象基类,子类须实现 reset() 方法
canRecycle — 是否可回收(外部创建的组件设 false)
ent — 拥有该组件的实体引用
变更检测:markDirty() / isChangedSince(sinceEpoch) / lastWriteEpoch — 基于 epoch 的帧级脏标记
5. 位掩码 — ECSMask.ts
基于 Uint32Array 的位运算,支持 set / has / delete / and / or / bitCount
对象池复用,clearPool() 清理
6. 注册系统 — ECSRegister.ts + ECSTypeRegistry.ts
@ecs.register('Name') 类装饰器:自动识别注册类型
组件:分配 tid,写入 compCtors 表
实体:记录 ctor → name 映射
系统:注册到全局表或指定世界
ECSTypeRegistry:全局类型注册表(跨世界共享),记录组件/实体/系统元数据
7. 查询 — ECSMatcher.ts + ECSGroup.ts
四种规则:AllOf / AnyOf / OnlyOf / ExcludeOf,规则间为"与"关系
flyweight 缓存:相同组合共享同一 Matcher 实例
小规则优化:tid 数 ≤ 4 时直接 mask.has,否则分配 ECSMask
ECSGroup:SparseSet 实现,O(1) 增删(swap-pop),稳定快照遍历
进入/离开追踪:watchEntityEnterAndRemove 供系统帧内查询变化
8. 系统 — ECSComblockSystem.ts + ECSRootSystem.ts
ComblockSystem:业务系统基类
生命周期:init → entityEnter → firstUpdate → update → entityRemove → onDestroy
filter() — 声明实体匹配规则
interval — 执行间隔(0=每帧,>0=固定间隔)
被动系统:isPassiveSystem() 返回 true 时不参与每帧 update
构造时探测子类钩子,选定 execute 变体(零开销分支)
RootSystem:根系统,一世界一个
init() — 并入 @ecs.register 系统 + 拓扑排序 + 绑定世界 + init
execute(dt) — 切换当前世界 → 递增 epoch → tick 各系统 → flush 命令缓冲
9. 系统调度 — SystemScheduler.ts
声明式执行顺序:@ecs.system.executeBefore / @ecs.system.executeAfter / @ecs.system.inSet
拓扑排序(Kahn 算法):无约束系统保持原始顺序,循环依赖抛 CycleDependencyError
集合机制:inSet('名') 把系统加入虚拟分组,其他系统用 executeAfter('set:名') 批量依赖
10. 世界 — ECSWorld.ts + ECSWorldManager.ts
ECSWorld:运行期数据容器
entities — 活动实体表(eid → 实体)
groups — 响应式查询分组
singletons — 单例组件表
refs — @entityRef 引用追踪
commands — 延迟结构变更命令队列
epoch — 世代号(每帧递增,变更检测用)
getEntity(ctor) — 创建/从池获取实体
assignEid(entity, eid) — 反序列化时保持 eid 一致
ECSWorldManager(ecs.world):多世界管理
get(name?) / default() / current — 获取/切换世界
use(world) — 切换当前世界,返回切换前的世界
inWorld(world, fn) — 在指定世界中执行,自动还原
createSystems(world, ...ctors) — 批量装配系统并 init
defer(fn) / flushCommands() — 延迟结构变更
11. 命令缓冲 — ECSCommandBuffer.ts
延迟结构变更队列,帧末由 RootSystem.execute 统一 flush
push(fn) 入队,flush() 先快照再执行(避免本帧新入队命令在本帧执行)
12. 对象池 — ECSPoolManager.ts + ECSDynamicPool.ts
ecs.pool:统一管理实体/组件动态对象池
getPool(name, factory) — 获取或创建池
clearAll() / clearPools() — 清空池缓存(不触碰存活数据)
getAllMetrics() — 获取各池统计信息(命中/未命中/缓存/创建数)
13. 实体引用 — ECSEntityRef.ts + ECSReferenceTracker.ts
@ecs.entityRef() 属性装饰器:标记组件属性为实体引用
目标实体销毁时自动置 null,避免悬空引用
组件回收时 clearComponentEntityRefs 清理所有引用
14. SoA 列存储 — StorageSoA.ts + StorageDecorators.ts
@ecs.storage.enableSoA 类装饰器:为组件启用 SoA 列存储(默认 AoS,opt-in)
字段装饰器:float64 / float32 / int32 / uint32 / int16 / uint16 / int8 / uint8
数值字段按列存入 TypedArray,按 eid 分配槽位
acquire 返回 Proxy 视图:SoA 字段读写直接落到 TypedArray,非 SoA 字段走后备实例
槽位超出容量时 2 倍扩容,释放时入空闲栈
自注册机制:模块加载时注册到核心,删除 storage/ 目录后基础 ECS 仍可运行
15. 网络同步 — NetworkSync.ts + SyncDecorators.ts + SyncCodec.ts + ByteBuffer.ts
@ecs.network.sync(类型) 字段装饰器:标记组件字段参与网络同步
ecs.network.net 门面:
encodeWorld(op?) — 编码当前世界为紧凑二进制(Full 全量 / Delta 增量)
applyToWorld(bytes, onMissing?) — 将二进制同步数据应用到世界
track(entity, markAll?) — 为实体初始化变更追踪器
clearDirty() — 清除所有脏标记
SyncCodec:实体/组件级编解码器
ByteWriter / ByteReader:二进制读写缓冲(变长整数等)
ChangeTracker:字段级脏标记追踪
16. 序列化 — Serialization.ts + Incremental.ts
@ecs.serialize() 字段装饰器:标记组件需要持久化的字段
全量序列化:
ecs.serialize.serializeWorld(pretty?) — 序列化当前世界为 JSON
ecs.serialize.deserializeWorld(json) — 从 JSON 反序列化到当前世界
增量序列化:
ecs.serialize.snapshot() — 对当前世界拍快照(基线)
ecs.serialize.computeDelta(base) — 计算相对基线的增量(added / removed / changed)
ecs.serialize.applyDelta(delta) — 将增量应用到当前世界
保留实体层级(parentEid),两阶段重建(先建实体+组件,再重建父子)
17. 监控日志 — ECSMonitorLogger.ts
零入侵监控模块,通过控制台命令触发
注册全局命令:
ecsLog() — 打印所有监控表格
ecsWorldSummary() — 各世界实体/系统/组件缓存统计
ecsSummary() — 总体统计(所有世界合计)
ecsEntityPools() — 实体池明细(命中/未命中/缓存)
ecsCompPools() — 组件池明细(按 M/B/V/VC 分类排序)
ecsEntityCaches() — 实体软移除组件缓存明细
ecsHelp() — 显示帮助信息
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
import type { ecs } from '../ECS';
|
||
import type { CompCtor, IComp } from '../registry/ECSTypes';
|
||
import type { ECSWorld } from '../world/ECSWorld';
|
||
import { bindSoAStorageAccessor } from './StorageAccessor';
|
||
import { getSoAFields, isSoAEnabled } from './StorageMeta';
|
||
import type { SoAArrayKind } from './StorageTypes';
|
||
import { setStorageProvider, type IComponentStorageProvider } from '../entity/ComponentStorage';
|
||
|
||
/** SoA 列存储支持的 TypedArray 联合类型 */
|
||
type AnyTypedArray =
|
||
| Float64Array
|
||
| Float32Array
|
||
| Int32Array
|
||
| Uint32Array
|
||
| Int16Array
|
||
| Uint16Array
|
||
| Int8Array
|
||
| Uint8Array;
|
||
|
||
/** 按类型与容量创建 TypedArray */
|
||
function createTypedArray(kind: SoAArrayKind, capacity: number): AnyTypedArray {
|
||
switch (kind) {
|
||
case 'float64': return new Float64Array(capacity);
|
||
case 'float32': return new Float32Array(capacity);
|
||
case 'int32': return new Int32Array(capacity);
|
||
case 'uint32': return new Uint32Array(capacity);
|
||
case 'int16': return new Int16Array(capacity);
|
||
case 'uint16': return new Uint16Array(capacity);
|
||
case 'int8': return new Int8Array(capacity);
|
||
case 'uint8':
|
||
case 'bool': return new Uint8Array(capacity);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SoA 列存储(每种 SoA 组件一个)
|
||
*
|
||
* 数值/布尔字段按列存入 TypedArray,按 eid 分配槽位。`acquire` 返回一个 Proxy 视图:
|
||
* - 读写 SoA 字段 → 直接落到 TypedArray 槽位(零拷贝)
|
||
* - 读写非 SoA 字段、调用方法、访问基类字段 → 落到后备实例(保持对象语义)
|
||
*/
|
||
export class StorageSoA<T extends ecs.IComp> {
|
||
/** 关联的组件构造函数 */
|
||
readonly type: CompCtor<T>;
|
||
|
||
/** 字段名 -> TypedArray 列 */
|
||
private readonly fields = new Map<string, AnyTypedArray>();
|
||
/** 字段名 -> TypedArray 类型 */
|
||
private readonly fieldKinds = new Map<string, SoAArrayKind>();
|
||
|
||
/** 实体 eid -> 槽位索引 */
|
||
private eidToSlot = new Map<number, number>();
|
||
/** 槽位索引 -> 实体 eid */
|
||
private slotToEid: number[] = [];
|
||
/** 可回收的空闲槽位栈 */
|
||
private freeSlots: number[] = [];
|
||
/** 槽位对应的后备组件实例(非 SoA 字段与方法) */
|
||
private backings: Array<T | undefined> = [];
|
||
/** 实体 eid -> Proxy 视图缓存 */
|
||
private proxies = new Map<number, T>();
|
||
|
||
/** 当前 TypedArray 容量(槽位上限) */
|
||
private _capacity: number;
|
||
/** 已分配过的最大槽位计数 */
|
||
private _size = 0;
|
||
|
||
/**
|
||
* 构造函数
|
||
* @param componentType 组件构造函数
|
||
* @param initialCapacity 初始槽位容量
|
||
*/
|
||
constructor(componentType: CompCtor<T>, initialCapacity = 256) {
|
||
this.type = componentType;
|
||
this._capacity = initialCapacity;
|
||
this.initFields(componentType);
|
||
}
|
||
|
||
/** 根据组件原型与装饰器声明初始化 SoA 列 */
|
||
private initFields(ctor: CompCtor<T>): void {
|
||
const declared = getSoAFields(ctor);
|
||
const probe = new ctor() as unknown as Record<string, unknown>;
|
||
|
||
const skip = new Set(['tid', 'ent', 'canRecycle']);
|
||
for (const key of Object.keys(probe)) {
|
||
if (skip.has(key) || key.startsWith('_')) continue;
|
||
let kind: SoAArrayKind | undefined = declared?.get(key);
|
||
if (!kind) {
|
||
const v = probe[key];
|
||
if (typeof v === 'number') kind = 'float64';
|
||
else if (typeof v === 'boolean') kind = 'bool';
|
||
}
|
||
if (kind) {
|
||
this.fieldKinds.set(key, kind);
|
||
this.fields.set(key, createTypedArray(kind, this._capacity));
|
||
}
|
||
}
|
||
if (declared) {
|
||
declared.forEach((kind, key) => {
|
||
if (!this.fields.has(key)) {
|
||
this.fieldKinds.set(key, kind);
|
||
this.fields.set(key, createTypedArray(kind, this._capacity));
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 所有 SoA 字段名 */
|
||
get fieldNames(): string[] {
|
||
return Array.from(this.fields.keys());
|
||
}
|
||
|
||
/** 槽位超出容量时按 2 倍扩容所有列 */
|
||
private ensureCapacity(slot: number): void {
|
||
if (slot < this._capacity) return;
|
||
let cap = this._capacity;
|
||
while (cap <= slot) cap *= 2;
|
||
this.fieldKinds.forEach((kind, name) => {
|
||
const next = createTypedArray(kind, cap);
|
||
next.set(this.fields.get(name)!);
|
||
this.fields.set(name, next);
|
||
});
|
||
this._capacity = cap;
|
||
}
|
||
|
||
/**
|
||
* 为实体分配组件(返回 Proxy 视图)。
|
||
*
|
||
* 注意:SoA 字段会被清零(列存储语义),组件构造函数里给 SoA 字段写的默认值
|
||
* (如 `x = 100`)不会生效——SoA 字段一律以 0 起始,需在 add 后显式赋值。
|
||
* 非 SoA 字段与方法仍走后备实例,构造默认值正常保留。
|
||
*/
|
||
acquire(eid: number): T {
|
||
let slot = this.freeSlots.pop();
|
||
if (slot === undefined) {
|
||
slot = this._size++;
|
||
this.ensureCapacity(slot);
|
||
}
|
||
this.fields.forEach((arr) => { arr[slot!] = 0; });
|
||
|
||
const backing = new this.type();
|
||
this.backings[slot] = backing;
|
||
this.eidToSlot.set(eid, slot);
|
||
this.slotToEid[slot] = eid;
|
||
|
||
const proxy = this.createProxy(eid, backing);
|
||
this.proxies.set(eid, proxy);
|
||
return proxy;
|
||
}
|
||
|
||
/** 创建将 SoA 字段读写路由到 TypedArray 的 Proxy 视图 */
|
||
private createProxy(eid: number, backing: T): T {
|
||
const fields = this.fields;
|
||
const self = this;
|
||
const handler: ProxyHandler<T & object> = {
|
||
get(target, prop, receiver) {
|
||
if (typeof prop === 'string' && fields.has(prop)) {
|
||
const slot = self.eidToSlot.get(eid);
|
||
if (slot === undefined) return undefined;
|
||
const v = fields.get(prop)![slot];
|
||
return self.fieldKinds.get(prop) === 'bool' ? v !== 0 : v;
|
||
}
|
||
return Reflect.get(target, prop, receiver);
|
||
},
|
||
set(target, prop, value, receiver) {
|
||
if (typeof prop === 'string' && fields.has(prop)) {
|
||
const slot = self.eidToSlot.get(eid);
|
||
if (slot === undefined) return true;
|
||
fields.get(prop)![slot] = typeof value === 'boolean' ? (value ? 1 : 0) : (value as number);
|
||
return true;
|
||
}
|
||
return Reflect.set(target, prop, value, receiver);
|
||
},
|
||
has(target, prop) {
|
||
if (typeof prop === 'string' && fields.has(prop)) return true;
|
||
return Reflect.has(target, prop);
|
||
}
|
||
};
|
||
return new Proxy(backing as T & object, handler) as T;
|
||
}
|
||
|
||
/** 释放实体的组件槽位 */
|
||
release(eid: number): void {
|
||
const slot = this.eidToSlot.get(eid);
|
||
if (slot === undefined) return;
|
||
this.eidToSlot.delete(eid);
|
||
this.proxies.delete(eid);
|
||
this.backings[slot] = undefined;
|
||
this.freeSlots.push(slot);
|
||
}
|
||
|
||
/** 获取实体当前的 Proxy 视图 */
|
||
getProxy(eid: number): T | undefined {
|
||
return this.proxies.get(eid);
|
||
}
|
||
|
||
/** 直接获取某字段的 TypedArray(供批处理;按槽位索引) */
|
||
getFieldArray(name: string): AnyTypedArray | undefined {
|
||
return this.fields.get(name);
|
||
}
|
||
|
||
/** 实体 eid -> 槽位 */
|
||
getSlot(eid: number): number | undefined {
|
||
return this.eidToSlot.get(eid);
|
||
}
|
||
|
||
/** 遍历所有活跃槽位 */
|
||
forEachActive(cb: (eid: number, slot: number) => void): void {
|
||
this.eidToSlot.forEach((slot, eid) => cb(eid, slot));
|
||
}
|
||
|
||
/** 当前活跃实体数 */
|
||
get size(): number {
|
||
return this.eidToSlot.size;
|
||
}
|
||
|
||
/** 当前槽位容量 */
|
||
get capacity(): number {
|
||
return this._capacity;
|
||
}
|
||
|
||
/** 清空所有槽位与映射,重置存储 */
|
||
clear(): void {
|
||
this.eidToSlot.clear();
|
||
this.proxies.clear();
|
||
this.slotToEid.length = 0;
|
||
this.backings.length = 0;
|
||
this.freeSlots.length = 0;
|
||
this._size = 0;
|
||
this.fields.forEach((arr) => arr.fill(0));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SoA 管理器(全局单世界):按组件 tid 路由列存储。
|
||
*/
|
||
export class SoAManager {
|
||
/** 组件 tid -> SoA 列存储 */
|
||
private readonly storages = new Map<number, StorageSoA<ecs.IComp>>();
|
||
|
||
/** 按组件类型获取或创建 SoA 列存储 */
|
||
getStorage<T extends ecs.IComp>(ctor: CompCtor<T>): StorageSoA<T> {
|
||
let storage = this.storages.get(ctor.tid) as StorageSoA<T> | undefined;
|
||
if (!storage) {
|
||
storage = new StorageSoA<T>(ctor);
|
||
this.storages.set(ctor.tid, storage as unknown as StorageSoA<ecs.IComp>);
|
||
}
|
||
return storage;
|
||
}
|
||
|
||
/** 指定 tid 是否已有 SoA 存储 */
|
||
has(tid: number): boolean {
|
||
return this.storages.has(tid);
|
||
}
|
||
|
||
/** 遍历所有 SoA 列存储 */
|
||
forEach(cb: (storage: StorageSoA<ecs.IComp>, tid: number) => void): void {
|
||
this.storages.forEach(cb);
|
||
}
|
||
|
||
/** 清空并移除所有 SoA 列存储 */
|
||
clear(): void {
|
||
this.storages.forEach((s) => s.clear());
|
||
this.storages.clear();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SoA 存储提供者:实现核心的 {@link IComponentStorageProvider} 端口,把 @ecs.storage.enableSoA 组件
|
||
* 的 acquire/release 接管为列存储。每个世界一个 {@link SoAManager}(按世界隔离列数据)。
|
||
*
|
||
* 该提供者在本模块加载时自注册到核心;核心因此无需 import storage,
|
||
* 删除整个 storage/ 目录后基础 ECS(纯 AoS)仍可正常运行。
|
||
*/
|
||
export class SoAStorageProvider implements IComponentStorageProvider {
|
||
/** 世界 -> 该世界的 SoA 管理器(弱引用,世界回收后自动释放) */
|
||
private readonly managers = new WeakMap<ECSWorld, SoAManager>();
|
||
|
||
/** 获取或创建某世界的 SoA 管理器 */
|
||
managerFor(world: ECSWorld): SoAManager {
|
||
let manager = this.managers.get(world);
|
||
if (!manager) {
|
||
manager = new SoAManager();
|
||
this.managers.set(world, manager);
|
||
}
|
||
return manager;
|
||
}
|
||
|
||
/** 组件是否标记 @ecs.storage.enableSoA */
|
||
handles(ctor: CompCtor<IComp>): boolean {
|
||
return isSoAEnabled(ctor);
|
||
}
|
||
|
||
/** 分配 SoA 列槽位并返回 Proxy 视图 */
|
||
acquire(world: ECSWorld, eid: number, ctor: CompCtor<IComp>): IComp {
|
||
return this.managerFor(world).getStorage(ctor).acquire(eid) as IComp;
|
||
}
|
||
|
||
/** 释放该实体在此组件类型上的 SoA 列槽位 */
|
||
release(world: ECSWorld, eid: number, ctor: CompCtor<IComp>): void {
|
||
this.managerFor(world).getStorage(ctor).release(eid);
|
||
}
|
||
|
||
/** 清空某世界的全部 SoA 列数据 */
|
||
clearWorld(world: ECSWorld): void {
|
||
this.managers.get(world)?.clear();
|
||
}
|
||
}
|
||
|
||
/** 全局 SoA 存储提供者实例 */
|
||
export const soaStorageProvider = new SoAStorageProvider();
|
||
|
||
// 注入 SoA 访问器(避免循环依赖)+ 注册核心存储提供者
|
||
bindSoAStorageAccessor(soaStorageProvider);
|
||
setStorageProvider(soaStorageProvider);
|