mirror of
https://gitee.com/dgflash/oops-plugin-framework.git
synced 2026-09-03 04:03:46 +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() — 显示帮助信息
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
import { instantiate, Node, Prefab, SafeArea } from 'cc';
|
||
import { Collection } from 'db://oops-framework/libs/collection/Collection';
|
||
import { resAutoTracker } from '../../common/loader/ResAutoTracker';
|
||
import { resLoader } from '../../common/loader/ResLoader';
|
||
import { GameComponent } from '../../../module/common/GameComponent';
|
||
import { oops } from '../../Oops';
|
||
import type { Uiid } from './LayerEnum';
|
||
import { LayerHelper } from './LayerHelper';
|
||
import type { UIParam } from './LayerUIElement';
|
||
import { LayerUIElement, UIState } from './LayerUIElement';
|
||
import type { UIConfig } from './UIConfig';
|
||
|
||
/** 界面层对象 */
|
||
export class LayerUI extends Node {
|
||
/** 全局窗口打开失败事件 */
|
||
onOpenFailure: Function = null!;
|
||
/** 显示界面节点集合 */
|
||
protected ui_nodes = new Collection<string, UIState>();
|
||
/** 被移除的界面缓存数据 */
|
||
protected ui_cache = new Map<string, UIState>();
|
||
/** 正在加载中的界面 Promise(用于并发去重,避免狂点按钮触发重复加载) */
|
||
protected ui_loading = new Map<string, Promise<Node>>();
|
||
/** 缓存界面的最大数量限制 */
|
||
protected readonly MAX_CACHE_SIZE = 10;
|
||
|
||
/**
|
||
* UI基础层,允许添加多个预制件节点
|
||
* @param name 该层名
|
||
*/
|
||
constructor(name: string) {
|
||
super(name);
|
||
LayerHelper.setFullScreen(this);
|
||
|
||
this.on(Node.EventType.CHILD_ADDED, this.onChildAdded, this);
|
||
this.on(Node.EventType.CHILD_REMOVED, this.onChildRemoved, this);
|
||
}
|
||
|
||
protected onChildAdded(child: Node) {
|
||
|
||
}
|
||
|
||
protected onChildRemoved(child: Node) {
|
||
const comp = child.getComponent(LayerUIElement);
|
||
if (comp) {
|
||
this.closeUi(comp.state);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加一个预制件节点到层容器中,该方法将返回一个唯一`uuid`来标识该操作节点
|
||
* @param uiid 窗口唯一标识
|
||
* @param config 界面配置数据
|
||
* @param params 自定义参数
|
||
* @returns ture为成功,false为失败
|
||
*/
|
||
add(uiid: Uiid, config: UIConfig, params?: UIParam): Promise<Node> {
|
||
// 并发去重:同一预制正在加载中时复用同一个 Promise,避免重复加载
|
||
const pending = this.ui_loading.get(config.prefab);
|
||
if (pending) return pending;
|
||
|
||
// 已显示的界面,保持原有「重复加载」拒绝行为
|
||
if (this.ui_nodes.has(config.prefab)) {
|
||
const error = `路径为【${config.prefab}】的预制重复加载`;
|
||
console.warn(error);
|
||
return Promise.reject(new Error(error));
|
||
}
|
||
|
||
const promise = this.doAdd(uiid, config, params);
|
||
this.ui_loading.set(config.prefab, promise);
|
||
const cleanup = () => this.ui_loading.delete(config.prefab);
|
||
promise.then(cleanup, cleanup);
|
||
return promise;
|
||
}
|
||
|
||
/** 实际执行界面加载流程 */
|
||
private async doAdd(uiid: Uiid, config: UIConfig, params?: UIParam): Promise<Node> {
|
||
try {
|
||
// 检查缓存中是否存界面
|
||
const state = this.initUIConfig(uiid, config, params);
|
||
await this.load(state);
|
||
if (state.node) {
|
||
return state.node;
|
||
}
|
||
throw new Error(`路径为【${config.prefab}】的预制加载失败,节点为空`);
|
||
}
|
||
catch (error) {
|
||
console.error(`添加界面【${config.prefab}】时发生错误:`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/** 初始化界面配置初始值 */
|
||
protected initUIConfig(uiid: Uiid, config: UIConfig, params?: UIParam) {
|
||
let state = this.ui_cache.get(config.prefab);
|
||
if (state == null) {
|
||
if (config.bundle == null) config.bundle = resLoader.defaultBundleName;
|
||
if (config.destroy == null) config.destroy = true;
|
||
if (config.vacancy == null) config.vacancy = false;
|
||
if (config.mask == null) config.mask = false;
|
||
if (config.safeArea == null) config.safeArea = false;
|
||
|
||
state = new UIState();
|
||
state.uiid = uiid.toString();
|
||
state.config = config;
|
||
}
|
||
state.params = params ?? {};
|
||
state.valid = true;
|
||
this.ui_nodes.set(config.prefab, state);
|
||
return state;
|
||
}
|
||
|
||
/**
|
||
* 加载界面资源
|
||
* @param state 显示参数
|
||
* @param bundle 远程资源包名,如果为空就是默认本地资源包
|
||
*/
|
||
protected async load(state: UIState): Promise<Node> {
|
||
// 首次加载:拉取预制并实例化节点
|
||
if (state.node == null) {
|
||
let timerId: any = null;
|
||
try {
|
||
timerId = setTimeout(this.onLoadingTimeoutGui, oops.config.game.loadingTimeoutGui);
|
||
|
||
const res = await resLoader.load(state.config.bundle!, state.config.prefab, Prefab);
|
||
|
||
// 加载期间已被移除,取消实例化
|
||
if (!state.valid) {
|
||
console.log(`界面【${state.config.prefab}】在加载过程中已被移除,取消实例化`);
|
||
if (res) res.decRef();
|
||
return null!;
|
||
}
|
||
|
||
if (res) {
|
||
state.node = instantiate(res);
|
||
|
||
if (state.config.safeArea) state.node.addComponent(SafeArea);
|
||
|
||
const comp = state.node.addComponent(LayerUIElement);
|
||
comp.state = state;
|
||
|
||
const viewRoot = state.node.getComponent(GameComponent);
|
||
if (viewRoot) {
|
||
resAutoTracker.acquire(viewRoot, res);
|
||
state.prefabTrackedByView = true;
|
||
}
|
||
else {
|
||
state.prefabTrackedByView = false;
|
||
}
|
||
}
|
||
else {
|
||
console.warn(`路径为【${state.config.prefab}】的预制加载失败`);
|
||
this.failure(state);
|
||
}
|
||
}
|
||
finally {
|
||
if (timerId !== null) clearTimeout(timerId);
|
||
oops.gui.waitClose();
|
||
}
|
||
}
|
||
|
||
await this.uiInit(state);
|
||
return state.node;
|
||
}
|
||
|
||
/**
|
||
* 创建界面节点
|
||
* @param state 视图参数
|
||
*/
|
||
protected async uiInit(state: UIState): Promise<boolean> {
|
||
if (!state.node || !state.valid) {
|
||
return false;
|
||
}
|
||
|
||
const comp = state.node.getComponent(LayerUIElement)!;
|
||
const r = await comp.add();
|
||
if (r) {
|
||
state.valid = true;
|
||
if (!state.params.preload) {
|
||
state.params.preload = false;
|
||
state.node.parent = this;
|
||
}
|
||
}
|
||
else {
|
||
console.warn(`路径为【${state.config.prefab}】的自定义预处理逻辑异常.检查预制上绑定的组件中 onAdded 方法,返回true才能正确完成窗口显示流程`);
|
||
this.failure(state);
|
||
}
|
||
return r;
|
||
}
|
||
|
||
/** 加载超时事件*/
|
||
private onLoadingTimeoutGui() {
|
||
oops.gui.waitOpen();
|
||
}
|
||
|
||
/** 窗口关闭事件 */
|
||
protected closeUi(state: UIState) {
|
||
this.ui_nodes.delete(state.config.prefab);
|
||
}
|
||
|
||
/** 打开窗口失败逻辑 */
|
||
protected failure(state: UIState) {
|
||
this.closeUi(state);
|
||
this.onOpenFailure && this.onOpenFailure();
|
||
}
|
||
|
||
/**
|
||
* 根据预制件路径删除,预制件如在队列中也会被删除,如果该预制件存在多个也会一起删除
|
||
* @param prefabPath 预制路径
|
||
*/
|
||
remove(prefabPath: string): void {
|
||
const state = this.ui_nodes.get(prefabPath);
|
||
if (state) {
|
||
const release: boolean = state.config.destroy!;
|
||
|
||
// 不释放界面,缓存起来待下次使用
|
||
if (release === false) {
|
||
this.addToCache(state.config.prefab, state);
|
||
}
|
||
|
||
// 界面移出舞台(增加 node 判空保护,避免异步加载未完成时崩溃)
|
||
if (state.node) {
|
||
const comp = state.node.getComponent(LayerUIElement);
|
||
comp && comp.remove(release);
|
||
}
|
||
|
||
// 标记为无效,防止异步加载完成后创建僵尸节点
|
||
state.valid = false;
|
||
}
|
||
}
|
||
|
||
/** 添加界面到缓存,实现 LRU 策略 */
|
||
private addToCache(prefabPath: string, state: UIState) {
|
||
// 如果缓存已满,移除最早的缓存项
|
||
if (this.ui_cache.size >= this.MAX_CACHE_SIZE) {
|
||
const firstKey = this.ui_cache.keys().next().value;
|
||
if (firstKey) {
|
||
const oldState = this.ui_cache.get(firstKey);
|
||
if (oldState) {
|
||
this.ui_cache.delete(firstKey);
|
||
const comp = oldState.node.getComponent(LayerUIElement);
|
||
comp && comp.remove(true);
|
||
}
|
||
}
|
||
}
|
||
this.ui_cache.set(prefabPath, state);
|
||
}
|
||
|
||
/** 删除缓存的界面,当调用 remove 移除舞台时,可通过此方法删除缓存界面 */
|
||
removeCache(prefabPath: string) {
|
||
const state = this.ui_cache.get(prefabPath);
|
||
if (state) {
|
||
this.ui_cache.delete(prefabPath);
|
||
const comp = state.node.getComponent(LayerUIElement);
|
||
comp && comp.remove(true);
|
||
}
|
||
}
|
||
|
||
/** 显示界面 */
|
||
show(prefabPath: string) {
|
||
const state = this.ui_nodes.get(prefabPath);
|
||
if (state) state.node.parent = this;
|
||
}
|
||
|
||
/**
|
||
* 根据预制路径获取已打开界面的节点对象
|
||
* @param prefabPath 预制路径
|
||
*/
|
||
get(prefabPath: string): Node {
|
||
const state = this.ui_nodes.get(prefabPath);
|
||
if (state) return state.node;
|
||
return null!;
|
||
}
|
||
|
||
/**
|
||
* 判断当前层是否包含 uuid或预制件路径对应的Node节点
|
||
* @param prefabPath 预制件路径或者UUID
|
||
*/
|
||
has(prefabPath: string): boolean {
|
||
return this.ui_nodes.has(prefabPath);
|
||
}
|
||
|
||
/**
|
||
* 判断当前层缓存中是否包含指定预制件路径的界面
|
||
* @param prefabPath 预制件路径
|
||
*/
|
||
hasCache(prefabPath: string): boolean {
|
||
return this.ui_cache.has(prefabPath);
|
||
}
|
||
|
||
/**
|
||
* 清除所有节点,队列当中的也删除
|
||
* @param isDestroy 移除后是否释放
|
||
*/
|
||
clear(isDestroy: boolean): void {
|
||
// 清除所有显示的界面
|
||
const length = this.ui_nodes.array.length - 1;
|
||
for (let i = length; i >= 0; i--) {
|
||
const uip = this.ui_nodes.array[i];
|
||
this.remove(uip.config.prefab);
|
||
}
|
||
this.ui_nodes.clear();
|
||
|
||
// 清除缓存中的界面
|
||
if (isDestroy) {
|
||
this.ui_cache.forEach((value: UIState, prefabPath: string) => {
|
||
this.removeCache(prefabPath);
|
||
});
|
||
}
|
||
}
|
||
} |