简介 ECS 架构是 实体(Entity)-组件(Component)-系统(System) 组成的架构,主要目的是对数据和逻辑进行解耦以便更好的维护系统。其运行的原理也能提高 CPU 的缓存命中,即可以提高游戏运行的性能。
原理 概述 ECS 架构简单来说就是用实体来组合组件,用组件来保存数据,用系统来进行运算。单一的实体没有任何意义,只有在组合组件之后这个实体才有意义。组件只能用于保存数据,不能自己进行任何的运算。系统只负责运算,不会持久化保存任何数据。这样就把数据和逻辑分离开来,通过组合的方式实现特定的功能,实现数据和逻辑的解耦,以及逻辑与逻辑之间的解耦。
大致示意图如下
组件 组件是 ECS 架构的基础,实体需要组件,系统也根据组件处理逻辑。组件的设计很简单,我们只需要以下基础属性和方法:
组件 id,每个组件都拥有一个属于自己的 id,这个 id 是全局唯一的。
组件名,每个组件都拥有一个属于自己的名字,这个名字是全局唯一的。
实体,保存组件所属的具体实体。
是否可以被回收,有些组件在实体移除后需要回收,有些则不从实体回收。
重置方法,用于回收组件之后进行组件的重置。
因此我们可以定义一个组件接口,接口不关心组件的具体信息,只关心组件是否可以回收、组件的所属以及重置方法。
IComp 1 2 3 4 5 6 7 import Entity from "../Entity" ;export default interface IComp { canRecycle : boolean ; entity : Entity ; reset (): void ; }
然后我们定义一个抽象组件基类。
Comp 1 2 3 4 5 6 7 8 9 10 import Entity from "./Entity" ;import IComp from "./Interface/IComp" ;export abstract class Comp implements IComp { static tid = -1 ; static compName : string ; public canRecycle : boolean = true ; public entity : Entity | null ; abstract reset (): void ; }
然后我们就可以定义实际的 Comp 类来保存我们需要的数据。
组件还有一种形式是标签,标签不继承抽象组件基类,但是在注册的时候也是和组件共享一套 id 自增规则。注意,标签类中的属性要任意赋一个值,这样才能在注册的时候获取到这个属性。
实体 实体是组件的合集,虽然实体的概念很简单,但是实体的实现却比较复杂。实体需要实现组件的添加和移除,也要在添加移除组件的时候通知对应系统进行处理,同时实体也要提供组件的查询功能。当然,实体自身也要提供移除的方法。
掩码工具 由于实体需要查询组件,系统也需要查询组件,因此我们需要先设计对应的功能。此处我们选择用二进制数来制作掩码系统保存组件信息。
这里我们使用一个二进制数组来保存,用数组的目的是如果组件数超过二进制数大小,就在数组增加一个二进制数来保存。在 32 位二进制数中,由于与(&)操作符最大只能操作 30 位数(一位符号位,一位进位),因此一个数只保存 30 个组件。
由于组件的数量在游戏的最开始就初始化完成,因此 Mask 实例的组件总数是固定的。
当我们进行掩码运算时,传入的组件 id 转为二进制数和当前的掩码进行比较,例如我们设置组件时,假设当前掩码为 0000 0000 0000 0000
,传入的组件 id 为 3,则我们把组件 id 化为 1 << (3 % 31)
,即 1 左移 3 位,得到 1000
,0000 0000 0000 0000 & 1000 = 0000 0000 0000 1000
,最终的组件就保存下来了。
也就是说 32 位掩码就是插槽,组件的 id 就是往哪个插槽插入组件,这样就能表示保存的组件了。
Mask 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import ECSManager from "./ECSManager" ;export default class Mask { private mask : Uint32Array ; private size : number = 0 ; constructor ( ) { let length = Math .ceil (ECSManager .getInst ().getCompTid () / 31 ); this .mask = new Uint32Array (length); this .size = length; } set (num: number ) { this .mask [(num / 31 ) >>> 0 ] |= 1 << num % 31 ; } delete (num: number ) { this .mask [(num / 31 ) >>> 0 ] &= ~(1 << num % 31 ); } has (num: number ) { return !!(this .mask [(num / 31 ) >>> 0 ] & (1 << num % 31 )); } or (other: Mask ) { for (let i = 0 ; i < this .size ; i++) { if (this .mask [i] & other.mask [i]) { return true ; } } return false ; } and (other: Mask ) { for (let i = 0 ; i < this .size ; i++) { if ((this .mask [i] & other.mask [i]) != this .mask [i]) { return false ; } } return true ; } clear ( ) { for (let i = 0 ; i < this .size ; i++) { this .mask [i] = 0 ; } } }
筛选工具 因为系统以实体进行遍历的,所以有了掩码之后,我们就可以对实体进行筛选了,通过比较实体的组件掩码和筛选工具的组件掩码,我们就可以筛选出系统需要的实体。
首先我们定义一个匹配规则抽象基类,基类的构造函数根据传入的组件设置掩码,并且按顺序保存组件 id。
然后我们定义各个匹配规则的类,判断是否匹配只需要调用掩码定义的规则即可。
所有规则类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import { CompTypeUnion } from "./ECSManager" ;import Entity from "./Entity" ;import { CompType } from "./Interface/CompType" ;import IComp from "./Interface/IComp" ;import Mask from "./Mask" ;export abstract class BaseOf { protected mask = new Mask (); public indices : number [] = []; constructor (...args: CompTypeUnion<IComp>[] ) { let componentTypeId = -1 ; let len = args.length ; for (let i = 0 ; i < len; i++) { if (typeof args[i] === "number" ) { componentTypeId = args[i] as number ; } else { componentTypeId = (args[i] as CompType <IComp >).tid ; } if (componentTypeId == -1 ) { console .error ("存在没有注册的组件!" ); } this .mask .set (componentTypeId); if (this .indices .indexOf (componentTypeId) < 0 ) { this .indices .push (componentTypeId); } } if (len > 1 ) { this .indices .sort ((a, b ) => { return a - b; }); } } public toString (): string { return this .indices .join ("-" ); } public abstract getKey (): string ; public abstract isMatch (entity : Entity ): boolean ; } export class AnyOf extends BaseOf { public isMatch (entity : Entity ): boolean { return this .mask .or (entity.mask ); } getKey (): string { return "anyOf:" + this .toString (); } } export class AllOf extends BaseOf { public isMatch (entity : Entity ): boolean { return this .mask .and (entity.mask ); } getKey (): string { return "allOf:" + this .toString (); } } export class ExcludeOf extends BaseOf { public getKey (): string { return "excludeOf:" + this .toString (); } public isMatch (entity : Entity ): boolean { return !this .mask .or (entity.mask ); } }
然后我们定义匹配器,匹配器可以包含复数规则,即匹配器是规则的集合,是所有匹配的实际执行类。匹配器也需要一个全局唯一的 id,给后面的 Group 使用。
IMatcher 1 2 3 4 5 6 7 8 import Entity from "../Entity" ;export interface IMatcher { mid : number ; indices : number []; key : string ; isMatch (entity : Entity ): boolean ; }
Matcher 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 import ECSManager , { CompTypeUnion } from "./ECSManager" ;import Entity from "./Entity" ;import { AllOf , AnyOf , BaseOf , ExcludeOf } from "./FilterRule" ;import IComp from "./Interface/IComp" ;import { IMatcher } from "./Interface/IMatcher" ;export class Matcher implements IMatcher { protected rules : BaseOf [] = []; protected _indices : number [] | null = null ; public mid : number = -1 ; private _key : string | null = null ; public get key (): string { if (!this ._key ) { let s = "" ; for (let i = 0 ; i < this .rules .length ; i++) { s += this .rules [i].getKey (); if (i < this .rules .length - 1 ) { s += " && " ; } } this ._key = s; } return this ._key ; } constructor ( ) { this .mid = ECSManager .getInst ().getMatherId (); } public get indices () { if (this ._indices === null ) { this ._indices = []; this .rules .forEach ((rule ) => { Array .prototype .push .apply (this ._indices , rule.indices ); }); } return this ._indices ; } public anyOf (...args : CompTypeUnion <IComp >[]): Matcher { this .rules .push (new AnyOf (...args)); return this ; } public allOf (...args : CompTypeUnion <IComp >[]): Matcher { this .rules .push (new AllOf (...args)); return this ; } public onlyOf (...args : CompTypeUnion <IComp >[]): Matcher { this .rules .push (new AllOf (...args)); let otherTids : CompTypeUnion <IComp >[] = []; for (let comp of ECSManager .getInst ().getComps ()) { if (args.indexOf (comp) < 0 ) { otherTids.push (comp); } } this .rules .push (new ExcludeOf (...otherTids)); return this ; } public excludeOf (...args: CompTypeUnion<IComp>[] ) { this .rules .push (new ExcludeOf (...args)); return this ; } public isMatch (entity : Entity ): boolean { for (let rule of this .rules ) { if (!rule.isMatch (entity)) { return false ; } } return true ; } public clone (): Matcher { let newMatcher = new Matcher (); newMatcher.mid = ECSManager .getInst ().getMatherId (); this .rules .forEach ((rule ) => newMatcher.rules .push (rule)); return newMatcher; } }
大致的关系如下图所示
实体类 现在我们可以实现实体类了。
大概的思路如图所示
首先是组件的增加。
我们设置一个 Map(下文都叫字典)_compInEntity
来保存当前实体的组件,设置一个字典 _compRemoved
用来保存已经移除但没有回收的组件。
添加组件的时候先判断是组件还是标签。
标签的情况,判断标签是否已经注册,已注册的情况下把标签添加到实体掩码,并且添加到已保存的组件字典中即可。
组件的情况,判断组件是否已注册,已注册并且已存在的情况下判断是否需要重新添加,需要的话就移除当前的组件,然后把组件添加到实体掩码,并且添加到已保存的组件字典中。
添加完成之后,我们还需要广播增删事件给系统。
然后是组件的移除。
组件的移除和添加类似,也需要分为标签和组件两个部分。
标签的情况,判断标签是否存在,是的话标记存在,到最后统一处理。
组件的情况,判断组件是否存在,是的话标记存在,取出当前组件的实例,设置实例的实体为 null,判断是否需要回收,需要回收的话执行组件初始化操作并回收,不需要的话就把组件放到移除列表中。
最后我们执行掩码的移除和组件存在列表的移除,并且广播通知系统执行对应操作。
在组件的添加和移除中,我们都可以设置实体类的对应名称属性为组件实例,以便更方便地访问组件。
接着是组件的查找。
组件的查找很简单,可以用掩码也可以用字典,根据自己的需要即可。组件的获取同理,可以用字典也可以用属性,自己决定即可。
最后是实体的移除。
实体移除的时候要移除所有组件,包括存在的和移除的列表。然后实体可以放入对象池。
Entity 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 import ECSManager , { CompTypeUnion } from "./ECSManager" ;import { CompType } from "./Interface/CompType" ;import IComp from "./Interface/IComp" ;import Mask from "./Mask" ;export default class Entity { public eid : number = -1 ; private mask = new Mask (); private _compInEntity : Map <number , CompTypeUnion <IComp >> = new Map (); private _compRemoved : Map <number , CompTypeUnion <IComp >> = new Map (); constructor ( ) {} public add<T extends IComp >( comp : CompTypeUnion <T>, isReAdded = false ): T | null { let compId; if (typeof comp == "number" ) { compId = comp; if (ECSManager .getInst ().hasTag (comp)) { this .mask .set (comp); this ._compInEntity .set (comp, comp); let tagName = ECSManager .getInst ().getTag (comp)!; this [tagName] = comp; ECSManager .getInst ().broadcastCompAddOrRemove (this , comp); } else { console .error ("不存在的tag!" ); } return null ; } else { compId = comp.tid ; if (compId == -1 ) { console .error ("组件未注册" ); return null ; } if (this ._compInEntity .has (compId)) { if (isReAdded) { this .remove (comp); } else { console .log ("组件" + comp.compName + "已存在" ); return this [comp.compName ]; } } this .mask .set (compId); let compInstance : T; if (this ._compRemoved .has (compId)) { compInstance = this ._compRemoved .get (compId) as unknown as T; this ._compRemoved .delete (compId); } else { compInstance = ECSManager .getInst ().createComp (comp); } this [comp.compName ] = compInstance; this ._compInEntity .set (compId, comp); compInstance.entity = this ; ECSManager .getInst ().broadcastCompAddOrRemove (this , compId); return compInstance as T; } } public addComps (reAdded = false , ...comps ) { for (let comp of comps) { this .add (comp, reAdded); } return this ; } public get<T>(comp : CompTypeUnion <T>) { let compName; if (typeof comp == "number" ) { compName = ECSManager .getInst ().getTag (comp); } else { compName = comp.compName ; } return this [compName] as T; } public has<T>(comp : CompTypeUnion <T>) { if (typeof comp == "number" ) { return this .mask .has (comp); } else { return this ._compInEntity .has (comp.tid ); } } private _remove (comp: CompTypeUnion<IComp> ) { this .remove (comp, true ); } public remove (comp: CompTypeUnion<IComp>, isRecycle: boolean = true ) { let compName : string ; let id = -1 ; let hasComp = false ; if (typeof comp == "number" ) { id = comp; if (this .mask .has (id)) { hasComp = true ; compName = ECSManager .getInst ().getTag (id); } else { console .warn ("试图移除不存在的tag" ); return ; } } else { id = comp.tid ; compName = comp.compName ; if (this .mask .has (id)) { hasComp = true ; let compInstance = this [compName] as CompType <IComp >; compInstance.entity = null ; if (isRecycle) { compInstance.reset (); if (compInstance.canRecycle ) { ECSManager .getInst ().recycle (id, compInstance); } } else { this ._compRemoved .set (id, compInstance); } } else { console .warn ("试图移除不存在的组件" , compName); } } if (hasComp) { this [compName] = null ; this .mask .delete (id); this ._compInEntity .delete (id); ECSManager .getInst ().broadcastCompAddOrRemove (this , id); } } public removeComps (isRecycle = true , ...args: CompTypeUnion<IComp>[] ) { for (let c of args) { this .remove (c, isRecycle); } } public removeSelf ( ) { this ._compInEntity .forEach (this ._remove , this ); this ._compRemoved .forEach (this ._remove , this ); ECSManager .getInst ().removeEntity (this .eid , this ); } }
系统 系统根据所需要的组件来筛选实体,有时候不同系统需要的组件相同,因此我们使用组 Group 来管理系统。
系统的结构如下图所示
群组 群组包含一个匹配器,如前文所说,匹配器的 id 也是群组的 id。群组只关心组件匹配的实体,操作的对象也都是实体。
我们保存一个匹配实体字典,以实体 id 为 key,以及一个缓存实体数组。缓存实体数组是系统运行时遍历的,每次组件广播后删除,下次重新缓存。
另外,我们保存一个进入实体列表和一个移除实体列表,这两个列表保存的是系统中对应列表的引用,在群组里专门负责监控组件变化时实体的进入和移除情况。
Group 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import Entity from "./Entity" ;import { IMatcher } from "./Interface/IMatcher" ;export default class Group <E extends Entity = Entity > { private matcher : IMatcher ; private _matchEntities : Map <number , E> = new Map (); private _entitiesCache : E[] | null = null ; public get matchEntities () { if (this ._entitiesCache === null ) { this ._entitiesCache = Array .from (this ._matchEntities .values ()); } return this ._entitiesCache ; } public count = 0 ; get entity (): E { return this .matchEntities [0 ]; } private _enteredEntities : Map <number , E> | null = null ; private _removedEntities : Map <number , E> | null = null ; constructor (matcher: IMatcher ) { this .matcher = matcher; } public onComponentAddOrRemove (entity: E ) { if (this .matcher .isMatch (entity)) { this ._matchEntities .set (entity.eid , entity); this ._entitiesCache = null ; this .count ++; if (this ._enteredEntities ) { this ._enteredEntities .set (entity.eid , entity); } if (this ._removedEntities ) { this ._removedEntities .delete (entity.eid ); } } else if (this ._matchEntities .has (entity.eid )) { this ._matchEntities .delete (entity.eid ); this ._entitiesCache = null ; this .count --; if (this ._enteredEntities ) { this ._enteredEntities .delete (entity.eid ); } if (this ._removedEntities ) { this ._removedEntities .set (entity.eid , entity); } } } public watchEntityEnterAndRemove ( enteredEntities: Map <number , E>, removedEntities: Map <number , E> ) { this ._enteredEntities = enteredEntities; this ._removedEntities = removedEntities; } clear ( ) { this ._matchEntities .clear (); this ._entitiesCache = null ; this .count = 0 ; this ._enteredEntities ?.clear (); this ._removedEntities ?.clear (); } }
系统实现 系统实现分为两个部分,一个部分是系统的具体实现,还有一个部分是根系统。
系统的具体实现核心目标是实现实体进入时的逻辑处理,每帧逻辑处理和实体移除时的逻辑处理。因此我们定义如下接口
ISystem 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import Entity from "../Entity" ;export interface IEntityEnterSystem <E extends Entity = Entity > { entityEnter (entities : E[]): void ; } export interface IEntityRemoveSystem <E extends Entity = Entity > { entityRemove (entities : E[]): void ; } export interface ISystemFirstUpdate <E extends Entity = Entity > { firstUpdate (entities : E[]): void ; }
我们在系统实现类的构造函数中判断是否有我们需要实现的方法,分为以下两种情况:
有首次进入/移除的逻辑函数execute1
。这时候我们要初始化实体进入/移除的数组,并且在组里监听这两个数组的实体变化。之后设置执行函数为execute1
。
没有上述逻辑,直接设置执行函数为每帧更新的逻辑函数execute0
。
如果有第一次执行 update 的函数updateOnce
的话,就保存当前设置的执行函数,并且设执行函数为updateOnce
。
在execute1
、execute0
、updateOnce
执行之后,我们都要置实体进入/移除的数组为空,防止下次执行时重复对已执行实体再次执行对应逻辑。
如果还有其他需求,也可以自行定义对应的接口和具体的处理逻辑。
ComblockSystem 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 import ECSManager from "./ECSManager" ;import Entity from "./Entity" ;import Group from "./Group" ;import { IMatcher } from "./Interface/IMatcher" ;import { IEntityEnterSystem , ISystemFirstUpdate , IEntityRemoveSystem , } from "./Interface/ISystem" ; export abstract class ComblockSystem <E extends Entity = Entity > { protected _group : Group <E>; protected _dt : number = 0 ; private _enteredEntities : Map <number , E> | null = null ; private _removedEntities : Map <number , E> | null = null ; private _hasEntityEnter : boolean = false ; private _hasEntityRemove : boolean = false ; private _tmpExecute : ((dt: number ) => void ) | null = null ; private execute!: (dt: number ) => void ; constructor ( ) { let hasOwnProperty = Object .hasOwnProperty ; let prototype = Object .getPrototypeOf (this ); let hasEntityEnter = hasOwnProperty.call (prototype, "entityEnter" ); let hasEntityRemove = hasOwnProperty.call (prototype, "entityRemove" ); let hasFirstUpdate = hasOwnProperty.call (prototype, "firstUpdate" ); this ._hasEntityEnter = hasEntityEnter; this ._hasEntityRemove = hasEntityRemove; if (hasEntityEnter || hasEntityRemove) { this ._enteredEntities = new Map <number , E>(); this ._removedEntities = new Map <number , E>(); this .execute = this .execute1 ; this ._group = ECSManager .getInst ().createGroup (this .filter ()); this ._group .watchEntityEnterAndRemove ( this ._enteredEntities , this ._removedEntities ); } else { this .execute = this .execute0 ; this ._group = ECSManager .getInst ().createGroup (this .filter ()); } if (hasFirstUpdate) { this ._tmpExecute = this .execute ; this .execute = this .updateOnce ; } } init (): void {} onDestroy (): void {} hasEntity (): boolean { return this ._group .count > 0 ; } private updateOnce (dt: number ) { if (this ._group .count === 0 ) { return ; } this ._dt = dt; if (this ._enteredEntities && this ._enteredEntities .size > 0 ) { (this as unknown as IEntityEnterSystem ).entityEnter ( Array .from (this ._enteredEntities .values ()) as E[] ); this ._enteredEntities .clear (); } (this as unknown as ISystemFirstUpdate ).firstUpdate ( this ._group .matchEntities ); this .execute = this ._tmpExecute !; this .execute (dt); this ._tmpExecute = null ; } private execute0 (dt : number ): void { if (this ._group .count === 0 ) { return ; } this ._dt = dt; this .update (this ._group .matchEntities ); } private execute1 (dt : number ): void { if (this ._removedEntities && this ._removedEntities .size > 0 ) { if (this ._hasEntityRemove ) { (this as unknown as IEntityRemoveSystem ).entityRemove ( Array .from (this ._removedEntities .values ()) as E[] ); } this ._removedEntities .clear (); } if (this ._group .count === 0 ) { return ; } this ._dt = dt; if (this ._enteredEntities && this ._enteredEntities .size > 0 ) { if (this ._hasEntityEnter ) { (this as unknown as IEntityEnterSystem ).entityEnter ( Array .from (this ._enteredEntities .values ()) as E[] ); } this ._enteredEntities .clear (); } this .update (this ._group .matchEntities as E[]); } abstract filter (): IMatcher ; abstract update (entities : E[]): void ; }
当然,同类型的系统我们也可以用系统组合器来组合使用。
System 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { ComblockSystem } from "./ComBlockSystem" ;export class System { private _comblockSystems : ComblockSystem [] = []; get comblockSystems () { return this ._comblockSystems ; } add (system: System | ComblockSystem ) { if (system instanceof System ) { Array .prototype .push .apply ( this ._comblockSystems , system._comblockSystems ); system._comblockSystems .length = 0 ; } else { this ._comblockSystems .push (system as ComblockSystem ); } return this ; } }
所有的系统实现,最后我们都要用根系统来使用。
根系统生命周期提供一个 init 方法,来遍历所有系统并且调用对应系统的初始化;提供一个 execute 方法来遍历所有系统,执行每帧更新的内容;提供一个 clear 方法来遍历所有系统,调用系统销毁时的 onDestroy 方法。
然后就是 add 方法,传入系统组合器时我们会平铺其的所有系统并加入数组;传入系统时直接加入数组。
所有的方法在我们自定义根系统的时候都不需要修改,只需要在构造函数中 add 新的系统即可。
RootSystem 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { ComblockSystem } from "./ComBlockSystem" ;import { System } from "./System" ;export class RootSystem { private executeSystemFlows : ComblockSystem [] = []; private systemCnt : number = 0 ; add (system: System | ComblockSystem ) { if (system instanceof System ) { Array .prototype .push .apply ( this .executeSystemFlows , system.comblockSystems ); } else { this .executeSystemFlows .push (system as ComblockSystem ); } this .systemCnt = this .executeSystemFlows .length ; return this ; } init ( ) { this .executeSystemFlows .forEach ((sys ) => sys.init ()); } execute (dt: number ) { for (let i = 0 ; i < this .systemCnt ; i++) { this .executeSystemFlows [i].execute (dt); } } clear ( ) { this .executeSystemFlows .forEach ((sys ) => sys.onDestroy ()); } }
管理器 ECS 的基本框架搭建好后,我们还需要一个管理器来管理 ECS 框架的一些操作。下面是本项目的分类,也可以根据需求自己设计。
ECS 管理器的核心功能简单概括如下图所示
组件注册 我们在使用组件之前,都要对组件进行注册。由上文我们知道组件有两种,一种是组件,一种是标签,因此注册也是分两种情况进行。
首先我们定义组件 id_compTid
来记录组件 id,定义注册池_registerPool
来判断是否存在同名组件或标签,定义组件池_comps
保存组件类,定义组件缓存池_compPools
来保存组件对象,定义 tag 池_tags
来保存标签名,定义组件变化池_compAddOrRemove
来保存变化的组件。
组件的注册用装饰器进行,我们定义两个类装饰器函数。
注册组件:传入一个组件名,还有一个可选变量 canNew 表示是否可以实例化,一般我们默认 true,然后我们判断注册的组件是否重复,没重复的话给当前组件命名,组件 id 赋值后自增,可以实例化组件的话就把组件存入组件池,并开辟一个当前组件的缓存池数组以及组件变化池数组。最后在注册池里标记为已注册。
注册组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public register<T>(compName : string , canNew : boolean = true ) { return function (comp: CompType<T> ) { if (comp.tid == -1 ) { let manager = ECSManager .getInst (); if (manager._registerPool [compName]) { console .warn ("组件与标签重名:" , compName); } else { comp.tid = manager._compTid ++; comp.compName = compName; if (canNew) { manager._comps .push (comp); manager._compPools .set (comp.tid , []); } else { manager._comps .push (null ); } manager._compAddOrRemove .set (comp.tid , []); console .log ("组件" + compName + "注册成功:" , comp.tid ) manager._registerPool [compName] = true ; } } else { console .log ("组件已注册" ); } } }
注册标签:注册标签我们不传入任何参数,我们只需要遍历标签类的所有属性,判断是否已注册,未注册的情况我们给标签 id 赋值并自增,然后和组件一样放入组件池并开辟组件缓存池。标签要额外放到标签池以方便标签的操作。最后也要在注册池里标注为已注册。
注册tag 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public registerTag ( ) { return function (_class: any ) { let manager = ECSManager .getInst (); let tid = manager._compTid ; for (let k in _class) { if (manager._registerPool [k]) { console .warn ("标签与组件重名:" , k); } else { tid = manager._compTid ++; _class[k] = tid; manager._comps .push (tid); manager._compPools .set (tid, []); manager._compAddOrRemove .set (tid, []); manager._tags .set (tid, k); console .log ("标签" + k + "注册成功:" , tid) manager._registerPool [k] = true ; } } } }
组件功能 组件的功能很简单,只是运用了对象池的概念。
组件功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public hasTag (id: number ) { return this ._tags .has (id); } public getTag (id : number ): string { return this ._tags .get (id) as string ; } public recycle (id: number , comp: IComp ) { this ._compPools .get (id)?.push (comp); } public createComp<T>(comp : CompType <T>) { if (!this ._comps [comp.tid ]) { console .error ("未找到组件" + comp.compName ) } let compInstance = this ._compPools .get (comp.tid )?.pop () || new this ._comps [comp.tid ]; return compInstance; } public getComps ( ) { return this ._comps ; }
实体功能 实体的功能也很简单,我们定义一个实体池_eid2Entity
用于保存当前所有的实体实例,定义一个实体缓存池_eneityPool
用来当实体的对象池。
实体功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public createEntity<E extends Entity = Entity >(): E { let entity = this ._eneityPool .pop (); if (!entity) { entity = new Entity (); entity.eid = this ._entityId ++; } this ._eid2Entity .set (entity.eid , entity); return entity as E; } public removeEntity (id: number , entity: Entity ) { if (this ._eid2Entity .has (id)) { this ._eneityPool .push (entity); this ._eid2Entity .delete (id); } else { console .warn ("试图销毁不存在的实体" ); } } public getEntityByEid (eid: number ) { return this ._eid2Entity .get (eid); } public activeEntityCount ( ) { return this ._eid2Entity .size ; }
系统功能 系统功能包括群组功能,过滤功能和组件变化的通知及清除功能。
组件变化的监听是在创建群组的时候就绑定好的。通过获取对应组件 id 的组件变化数组,存入群组的监听函数,就可以在每次组件变化的时候通知所有监听该组件的群组执行对应的函数。
其他功能就是对上文现有功能的再包装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 public createGroup<E extends Entity = Entity >(matcher : IMatcher ): Group <E> { let group = this ._groups .get (matcher.mid ); if (!group) { group = new Group (matcher); this ._groups .set (matcher.mid , group); let careCompIds = matcher.indices ; for (let i = 0 , len = careCompIds.length ; i < len; i++) { let child = this ._compAddOrRemove .get (careCompIds[i]); if (!child) { child = []; this ._compAddOrRemove .set (careCompIds[i], child); } child.push (group.onComponentAddOrRemove .bind (group)); } } return group as unknown as Group <E>; } public query (matcher: IMatcher ) { let group = this ._groups .get (matcher.mid ); if (!group) { group = this .createGroup (matcher); this ._eid2Entity .forEach (group.onComponentAddOrRemove , group); } return group.matchEntities ; } public broadcastCompAddOrRemove (entity: Entity, componentTypeId: number ) { let events = this ._compAddOrRemove .get (componentTypeId); if (events) { for (let i = events.length - 1 ; i >= 0 ; i--) { events![i](entity); } } } public clear ( ) { this ._eid2Entity .forEach ((entity ) => { entity.removeSelf (); }); this ._groups .forEach ((group ) => { group.clear (); }); this ._compAddOrRemove .forEach (callbackLst => { callbackLst.length = 0 ; }); this ._eid2Entity .clear (); this ._groups .clear (); } public getMatherId ( ) { return this ._matcherId ++; } public allOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().allOf (...args); } public anyOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().anyOf (...args); } public onlyOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().onlyOf (...args); } public excludeOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().excludeOf (); }
总的 ECS 管理类代码如下
ECSManager 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 import { Comp } from "./Comp" ;import Entity from "./Entity" ;import Group from "./Group" ;import { CompType } from "./Interface/CompType" ;import IComp from "./Interface/IComp" ;import { IMatcher } from "./Interface/IMatcher" ;import { Matcher } from "./Mathcer" ;export type CompAddOrRemove = (entity: Entity ) => void ;export type CompTypeUnion <T> = CompType <T> | number ;export default class ECSManager { private static _instance : ECSManager | null = null ; private _compTid = 0 ; private _entityId = 1 ; private _matcherId = 1 ; private _registerPool = {}; private _comps : any [] = []; private _compPools : Map <number , Comp []> = new Map (); private _tags : Map <number , string > = new Map (); private _compAddOrRemove : Map <number , CompAddOrRemove []> = new Map (); private _eid2Entity : Map <number , Entity > = new Map (); private _eneityPool : Entity [] = []; private _groups : Map <number , Group > = new Map (); public static getInst (): ECSManager { if (!this ._instance ) { this ._instance = new ECSManager (); } return this ._instance ; } public getCompTid ( ) { return this ._compTid ; } public register<T>(compName : string , canNew : boolean = true ) { return function (comp: CompType<T> ) { if (comp.tid == -1 ) { let manager = ECSManager .getInst (); if (manager._registerPool [compName]) { console .warn ("组件与标签重名:" , compName); } else { comp.tid = manager._compTid ++; comp.compName = compName; if (canNew) { manager._comps .push (comp); manager._compPools .set (comp.tid , []); } else { manager._comps .push (null ); } manager._compAddOrRemove .set (comp.tid , []); console .log ("组件" + compName + "注册成功:" , comp.tid ); manager._registerPool [compName] = true ; } } else { console .log ("组件已注册" ); } }; } public registerTag ( ) { return function (_class: any ) { let manager = ECSManager .getInst (); let tid = manager._compTid ; for (let k in _class) { if (manager._registerPool [k]) { console .warn ("标签与组件重名:" , k); } else { tid = manager._compTid ++; _class[k] = tid; manager._comps .push (tid); manager._compPools .set (tid, []); manager._compAddOrRemove .set (tid, []); manager._tags .set (tid, k); console .log ("标签" + k + "注册成功:" , tid); manager._registerPool [k] = true ; } } }; } public hasTag (id: number ) { return this ._tags .has (id); } public getTag (id : number ): string { return this ._tags .get (id) as string ; } public recycle (id: number , comp: IComp ) { this ._compPools .get (id)?.push (comp); } public createComp<T>(comp : CompType <T>) { if (!this ._comps [comp.tid ]) { console .error ("未找到组件" + comp.compName ); } let compInstance = this ._compPools .get (comp.tid )?.pop () || new this ._comps [comp.tid ](); return compInstance; } public getComps ( ) { return this ._comps ; } public broadcastCompAddOrRemove (entity: Entity, componentTypeId: number ) { let events = this ._compAddOrRemove .get (componentTypeId); if (events) { for (let i = events.length - 1 ; i >= 0 ; i--) { events![i](entity); } } } public createEntity<E extends Entity = Entity >(): E { let entity = this ._eneityPool .pop (); if (!entity) { entity = new Entity (); entity.eid = this ._entityId ++; } this ._eid2Entity .set (entity.eid , entity); return entity as E; } public removeEntity (id: number , entity: Entity ) { if (this ._eid2Entity .has (id)) { this ._eneityPool .push (entity); this ._eid2Entity .delete (id); } else { console .warn ("试图销毁不存在的实体" ); } } public getEntityByEid (eid: number ) { return this ._eid2Entity .get (eid); } public activeEntityCount ( ) { return this ._eid2Entity .size ; } public clear ( ) { this ._eid2Entity .forEach ((entity ) => { entity.removeSelf (); }); this ._groups .forEach ((group ) => { group.clear (); }); this ._compAddOrRemove .forEach ((callbackLst ) => { callbackLst.length = 0 ; }); this ._eid2Entity .clear (); this ._groups .clear (); } public createGroup<E extends Entity = Entity >(matcher : IMatcher ): Group <E> { let group = this ._groups .get (matcher.mid ); if (!group) { group = new Group (matcher); this ._groups .set (matcher.mid , group); let careCompIds = matcher.indices ; for (let i = 0 , len = careCompIds.length ; i < len; i++) { let child = this ._compAddOrRemove .get (careCompIds[i]); if (!child) { child = []; this ._compAddOrRemove .set (careCompIds[i], child); } child.push (group.onComponentAddOrRemove .bind (group)); } } return group as unknown as Group <E>; } public query (matcher: IMatcher ) { let group = this ._groups .get (matcher.mid ); if (!group) { group = this .createGroup (matcher); this ._eid2Entity .forEach (group.onComponentAddOrRemove , group); } return group.matchEntities ; } public getMatherId ( ) { return this ._matcherId ++; } public allOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().allOf (...args); } public anyOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().anyOf (...args); } public onlyOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().onlyOf (...args); } public excludeOf (...args: CompTypeUnion<IComp>[] ) { return new Matcher ().excludeOf (); } }
使用方法 Tag示例 1 2 3 4 5 6 7 import ECSManager from "../../../ECS/ECSManager" ;@ECSManager .getInst ().registerTag ()export default class StatusTags { public static StatusA = 0 ; public static StatusB = 1 ; }
Comp示例 1 2 3 4 5 6 7 8 9 10 11 12 import { Comp } from "../../../ECS/Comp" ;import ECSManager from "../../../ECS/ECSManager" ;@ECSManager .getInst ().register ("Transform" )export default class TransformComp extends Comp { public position : Laya .Vector3 = new Laya .Vector3 (); reset (): void { this .position .set (0 , 0 , 0 ); } }
Entity示例 1 2 3 4 5 6 import Entity from "../../../ECS/Entity" ;import TransformComp from "../Comp/TransformComp" ;export default class RoleEntity extends Entity { public Transform : TransformComp ; }
ComblockSystem示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { ComblockSystem } from "../../../ECS/ComBlockSystem" ;import ECSManager from "../../../ECS/ECSManager" ;import { IMatcher } from "../../../ECS/Interface/IMatcher" ;import { IEntityEnterSystem } from "../../../ECS/Interface/ISystem" ;import ServantComp from "../Comp/ServantComp" ;import WorkerComp from "../Comp/WorkerComp" ;import RoleEntity from "../Entity/RoleEntity" ;export default class WorkSystem extends ComblockSystem <RoleEntity > implements IEntityEnterSystem <RoleEntity > { entityEnter (entities : RoleEntity []): void { for (let e of entities) { } } filter (): IMatcher { return ECSManager .getInst ().anyOf (WorkerComp , ServantComp ); } update (entities : RoleEntity []): void { for (let e of entities) { } } }
RootSystem示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import Globals from "../../../Config/Globals" ;import { RootSystem } from "../../../ECS/RootSystem" ;import CarShopSystem from "../System/CarShopSystem" ;import CarSystem from "../System/CarSystem" ;import CustomSystem from "../System/CustomSystem" ;import DollSystem from "../System/DollSystem" ;import MoveSystem from "../System/MoveSystem" ;import NpcSystem from "../System/NpcSystem" ;import PathFindingSystem from "../System/PathFindingSystem" ;import WorkSystem from "../System/WorkSystem" ;export default class RootSlowSystem extends RootSystem { constructor ( ) { super (); this .add (new WorkSystem ()); } }
根系统调用示例 1 2 3 4 5 6 7 8 9 10 11 12 onAwake ( ){ this ._rootSlowSystem = new RootSlowSystem (); this ._rootSlowSystem .init (); } onUpdate ( ){ this ._rootSlowSystem .execute (Laya .timer .delta ); } onDisable ( ) { this ._rootSlowSystem .clear (); }
代码 由于本项目代码较多,因此请移步 GitHub 查看详细代码。