ecs-framework icon indicating copy to clipboard operation
ecs-framework copied to clipboard

EntitySystem/Matcher效率优化与功能改进

Open 0MirageTank0 opened this issue 2 months ago • 7 comments

目前问题

不适合使用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_entityCacheupdateEntityTrackingisSingleConditionexecuteSingleConditionQueryexecuteComplexQuerygetEntityIdMap等方法与变量,占用了相当多的篇幅。

主要改进方法

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等辅助),并最佳化系统更新的运行时效率。

0MirageTank0 avatar Oct 15 '25 05:10 0MirageTank0