EntitySystem/Matcher效率优化与功能改进
目前问题
不适合使用QuerySystem实现EntitySystem实体列表更新,主要原因在于:
- 缓存有效的前提是查询结果在一段时间内保持稳定
- ECS 的特性是组件频繁变化来表达状态
- 任何变化都会清空
QuerySystem的全部缓存
具体而言,目前EntitySystem主要有三个方面的问题:
一、多系统更新导致的开销占用过大:
例如我的一个基于2.1.27版本的3D RPG项目,包含16个EntitySystem用于处理这些逻辑:
玩家预输入、技能的分阶段释放&打断&伤害计算、动画状态与Root Motion位置重定位...
系统与系统之间、系统内部之中通过添加与消耗Component实现的状态转移,这就意味着几乎每时每刻组件都在发生变化。在新版本中实现这一点需要每一帧执行16次缓存失效的Query查询,并且每一次Query查询中会构建各种临时的结构体、数组对象;查询结束后还需要逐一遍历来寻找最近新增、最近删除的实体,这些微小的影响在每帧中累计起来足够造成可观的性能消耗。
二、lateUpdate中直接复用了update中的查询结果,可能会得到错误的实体列表
在process中修改的实体不会立刻影响lastProcess的参数,这点在特殊场景中影响很严重,例如某些系统需要在lastProcess中考虑process中增删组件操作的结果
https://github.com/esengine/ecs-framework/blob/a572c8096701074250012694d54440b76397db08/packages/core/src/ECS/Systems/EntitySystem.ts#L582-L584
三、EntitySystem中查询方法导致文件庞大:
目前为了实现查询功能,在EntitySystem中添加了queryEntities、_entityCache、updateEntityTracking、isSingleCondition、executeSingleConditionQuery、executeComplexQuery、getEntityIdMap等方法与变量,占用了相当多的篇幅。
主要改进方法
1.通过组件的变更通知实现精确的增量更新,消除查询开销:
可以基于目前的QuerySystem更新策略进行升级,将源代码中的:
https://github.com/esengine/ecs-framework/blob/a572c8096701074250012694d54440b76397db08/packages/core/src/ECS/Entity.ts#L75-L86
重构为:
/**
* 通知Scene实体组件发生变动
* @private
*/
private static notifyScene(entity: Entity) {
entity.scene?.updateEntity(entity);
}
在Scene.updateEntity中,再更新QuerySystem与所有的EntitySystem:
// Scene.ts
updateEntity(entity: Entity) {
if(this.querySystem) {
this.querySystem.updateEntity(entity);
}
for (const system of this.systems) {
// 通知System实体发生改变...
system.updateEntity(entity);
}
}
2.提升Matcher职能,实现对单一实体的过滤逻辑:
// Matcher.ts
/**
* 检查是否匹配给定实体
* @param entity
*/
isMatch(entity: Entity) {
return this.condition.all.every(t => entity.hasComponent(t)) &&
this.condition.any.some(t => entity.hasComponent(t)) &&
this.condition.none.every(t => !entity.hasComponent(t)) &&
(this.condition.tag === undefined || entity.tag === this.condition.tag) &&
(this.condition.name === undefined || entity.name === this.condition.name) &&
(this.condition.component === undefined || entity.hasComponent(this.condition.component));
}
3.在EntitySystem中使用SparseSet维护实体数组,最大化减少实体更新开销:
// EntitySystem.ts
// 实体数组
private _entities: SparseSet<Entity>;
/**
* 检查组件变更是否依然匹配Matcher
* @param entity
*/
public updateEntity(entity: Entity) {
const oldState = this._entities.has(entity);
const newState = this._matcher.isMatch(entity);
if(oldState == newState) return;
if(newState) {
// 新增组件相关方法
} else {
// 移除组件相关方法
}
}
// 在process中直接传递_entity.getDenseArrayUnsafe()保证最大化的性能
this.process(_entity.getDenseArrayUnsafe())
预期结果
预期会大幅缩减EntitySystem文件的代码量(无需query、_entityCache等辅助),并最佳化系统更新的运行时效率。