在此图中,系统读取 Translation 和 Rotation 组件,将它们相乘,然后更新相应的 LocalToWorld 组件。 实体 A 和 B 具有 Renderer 组件而实体 C 没有,但系统对 A 、B 、C 都一视同仁,因为系统不关心 Renderer 组件。 如果我们将系统设置为需要 Renderer 组件,那么实体 C 会被系统忽视;如果我们将系统设置为排除具有 Renderer 组件的实体,则实体 A 和 B 会被系统忽视。可以看出系统在筛选实体和关联组件这两件事上具有一定的自由度。
此外,OW 开发者为了提高实体组件系统的适用性,降低代码复杂度,以应对游戏项目工程中的复杂情况,还提出了 Singleton Component 、 Utility 两个概念。 单例组件( Singleton Component ),相当于是一堆全局数据,这类组件归属于唯一的匿名实体,可以由不同的 System 进行访问。这种组件应用面很广,在 OW 有 40 % 都是单例组件。 工具函数( Utility ),用来解决 System 之间相互调用以及 System 内部包含多个层级的问题,例如处理敌友关系的逻辑,在许多 System 中都会用到,如果说把敌友关系的逻辑也包装成 System ,那么会出现 System 互相调用,一旦开了这样的先例,最后可能会导致各个 System 之间的调用关系像一团乱麻,不容易维护。这种处理敌友关系的逻辑可以实现为工具函数。一方面,尽量避免在工具函数内部使用 System (因为如果使用System,那么其实并没有把问题解决);另一方面,如果实在需要使用 System ,那么做好访问限制,尽量把副作用局限到可控范围内。
此外 OW 开发者还提出可以通过延迟技术来减少耦合的情况,有很多工作并不需要立马完成,对于这些工作,可以放到队列里面,等到一帧结束,或者合适的时机再一并处理。 2.2.1 ECS框架下的技能系统
如果使用 ECS 方式来设计技能系统,可以把技能系统涉及到的多种不同类型的数据拆分到不同的组件中,每个实体根据需要来挂接不同的组件。譬如:
玩家 A 和 B 互相对战,t1 时刻,A 向命中区域内的 B 施放了一个技能,服务端在 t3 收到技能请求,此时 B 还在 A 的命中范围内(服务器要等 t4 才处理 B 离开 A 的事件),服务端判定命中。在 t1 与 t3 之间的 t2,B 离开了 A 的命中区域,并在 t5 看到自己被 A 命中了, 从 B 的视角看自己明明已经远离了 A 但还是被命中了。从 A 的视角看,B 是在 t6 才离开自己的命中区域。
可以看到对于任何一个节点来说,从其他节点传来的数据都是不新鲜的。
此外,代码的编写方式也可能导致数据不新鲜的问题。
bool bEnemy = CheckEnemy(playerA,playerB);
// 代码段 1 ...
if (bEnemy)
{
// 代码段 2 ...
}如上面这段代码,将玩家 A 和 B 的敌对状态临时存在 bEnemy 中,如果在代码段 1 中存在改变敌对关系的逻辑,那么等到后面再使用 bEnemy 来判断时,这个数据已经不新鲜了。 3.3 时序依赖