Skip to content

Commit 609c29b

Browse files
committed
feat(ECS模式): update
1 parent 203165f commit 609c29b

File tree

3 files changed

+84
-42
lines changed

3 files changed

+84
-42
lines changed

packages/ECS模式/article.md

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,8 @@ worldState的superHeroes中有一个超级英雄数据的position为[6,6,6],
350350
## 概述解决方案?
351351

352352

353-
通过下面的改进来解决重复和继承的问题:
354-
使用组件化的思想,用组合代替继承,具体如下:
353+
<!-- 通过下面的改进来解决重复和继承的问题: -->
354+
基于组件化的思想,用组合代替继承,具体如下:
355355
将英雄抽象为GameObject
356356
将英雄的行为抽象为组件,并把英雄的相关数据也移到组件中
357357
英雄通过挂载不同的组件,来实现不同的行为
@@ -848,67 +848,90 @@ worldState的gameObjects包括了4个gameObject的数据;
848848

849849

850850
## 提出问题
851-
TODO continue
852851

853852
- 组件的数据分散在各个组件中,性能不好
854-
现在所有人物的position的数据一对一地分散保存在各个PositionComponent组件中,那么在遍历所有的position数据时,会因为CPU中不容易缓存命中而带来性能损失
853+
如position数据现在是一对一地分散保存在各个positionComponent组件中(即一个positionComponent组件保存自己的position),那么如果需要遍历所有组件的position数据,则需要遍历所有的positionComponent组件,分别获得它们的position
854+
因为每个positionComponent组件的数据并没有连续地保存在内存中,所以会造成缓存命中丢失,带来性能损失
855855

856+
<!-- 因为在遍历每个positionComponent组件时,需要将它的所有数据都载入CPU的二级缓存中 -->
857+
<!-- 当它的大小大于CPU的二级缓存的大小时,就无法载入而造成缓存无法命中,从而带来性能损失 -->
858+
<!-- 当它的大小大于CPU的二级缓存的大小时,就无法全部载入,而需要
859+
而造成缓存无法命中,从而带来性能损失 -->
856860

857-
- 如果超级英雄增加一个“跑”的行为,该行为不仅会更新position,还会修改速度velocity,那么该行为对应的run函数应该放在哪个组件中呢?
858-
因为run函数需要同时修改PositionComponent组件和VelocityComponent组件的数据,所以它放在其中的任何一种组件都不合适,需要增加一种新的组件-RunComponent,对应“跑”的行为,实现run函数
859-
该函数需要通过RunComponent挂载的gameObject来获得PositionComponent和VelocityComponent组件,然后再修改它们的数据
861+
- 如果超级英雄增加一个“跳”的行为,该行为不仅会更新position,还会修改速度velocity,那么该行为对应的jump函数应该放在哪个组件中呢?
862+
因为jump函数需要同时修改PositionComponent组件和VelocityComponent组件的数据,所以将它放在其中的任何一种组件都不合适
863+
因此需要增加一种新的组件-JumpComponent,对应“跳”这个行为,实现jump函数
864+
该函数会通过JumpComponent挂载的gameObject来获得挂载到该gameobject上的PositionComponent和VelocityComponent组件,修改它们的数据
860865

861-
如果有更多的涉及多种组件的行为,就需要为每个行为增加一种组件。
862-
因为组件比较重,既有数据又有逻辑,所以增加的开发成本较高
863-
另外,组件与GameObject是聚合关系,而GameObject和World也是聚合关系,它们都属于强关联关系,所以增加组件会较强地影响GameObject和World模块,也增加了成本
866+
如果增加更多的涉及多种组件的行为,就需要为每个行为增加一种组件。
867+
因为组件比较重,既有数据又有逻辑,所以增加组件的开发成本较高
868+
另外,因为组件与GameObject是聚合关系,而GameObject和World也是聚合关系,它们都属于强关联关系,所以增加组件会较强地影响GameObject和World,这也增加了开发成本
864869

865870

866871

867872
# [给出使用模式的改进方案]
868873

869874
## 概述解决方案
870875

871-
使用Data Oriented的思想进行改进:
872-
将所有的gameObject、每种组件的数据集中起来,保存在各自的连续空间中
873-
其中,gameObject与挂载的组件的对应关系则保存在Hash Map中;
874-
将组件的角色分为Data Oriented组件(每个组件都有数据,且组件的数量较多)和其它组件(每个组件都没有数据,或者组件的数量很少)。这里具体说明一下:
875-
目前一共有四种组件,它们为:PositionComponent、VelocityComponent、FlyComponent、InstanceComponent。其中Instance组件因为没有组件数据,所以属于“其它组件”的角色;而另外三种组件则都属于“Data Oriented组件”的角色;
876-
Data Oriented组件的数据保存在各自的ArrayBuffer中;
877-
将GameObject和各个Component扁平化,使得GameObject是一个number类型的id值,Component是一个number类型的索引。其中GameObject是gameObject与挂载的组件的对应关系这个Hash Map的Key;Component既是这个Hash Map的Value,又是ArrayBuffer中的索引
876+
<!-- 通过下面的改进来提高性能: -->
877+
基于Data Oriented的思想进行改进,具体如下:
878+
<!-- 基于Data Oriented的思想进行改进,将gameObject所有的数据和每种组件的数据分别集中起来,保存在各自的一块连续空间中 -->
879+
<!-- 其中,gameObject的数据是指gameObject挂载了哪些组件,我们将其保存在一个Hash Map中; -->
880+
组件可以按角色分为Data Oriented组件和其它组件,前者的特点是每个组件都有数据,且组件的数量较多,后者的特点是每个组件都没有数据,或者组件的数量很少
881+
这里具体说明一下各种组件的角色:
882+
目前一共有四种组件,它们为:PositionComponent、VelocityComponent、FlyComponent、InstanceComponent。InstanceComponent组件因为没有组件数据,所以属于“其它组件”;另外三种组件则都属于“Data Oriented组件”
883+
884+
将属于Data Oriented组件的三种组件的所有组件数据分别集中起来,保存在各自的一块连续空间中,具体就是分别保存在三个ArrayBuffer中
885+
886+
将GameObject和各个Component扁平化,使得GameObject不再有数据和逻辑了,而只是一个number类型的id值;Component也不再有数据和逻辑了,而只是一个number类型的索引值
887+
<!-- 其中GameObject是gameObject与挂载的组件的对应关系这个Hash Map的Key;Component既是这个Hash Map的Value,又是ArrayBuffer中的索引 -->
888+
889+
我们增加Component+GameObject这一层,将扁平的GameObject和Componet放在该层中
878890

879891

880-
我们增加Manager这一层,来维护和管理GameObject和组件的数据
881-
其中GameObjectManager负责维护所有的gameObject的数据
882-
四种组件的Manager负责维护自己的ArrayBuffer,操作属于该种类的所有组件
892+
我们增加Manager这一层,来管理GameObject和组件的数据
893+
这一层有GameObjectManager和四种组件的Manager,其中GameObjectManager负责管理所有的gameObject
894+
四种组件的Manager负责管理自己的ArrayBuffer,操作属于该种类的所有组件
883895

884-
值得注意的是将保存gameObject与挂载的组件的对应关系的Hash Map放在哪里?
885-
它们可以放在GameObjectManager中,也可以分散放在组件的Manager中。
886-
考虑到为了方便组件直接就近获得自己挂载到的GameObject,所以我们选择将其分散放在组件的Manager中
896+
值得注意的是:
897+
<!-- 将“gameObject挂载了哪些组件”的对应关系的Hash Map放在哪里? -->
898+
将“gameObject挂载了哪些组件”的数据放在哪里?
899+
它们可以放在GameObjectManager中,也可以分散地放在各个组件的Manager中。
900+
考虑到为了方便组件直接就近获得自己挂载到的GameObject,所以我们选择后者
887901

888902

889903
我们增加System这一层,来实现行为的逻辑。
890-
一个System对应一个行为,比如我们加入MoveSystem、FlySystem来分别实现移动、飞行的行为逻辑
904+
一个System对应一个行为,比如这一层中的MoveSystem、FlySystem分别实现了移动和飞行的行为逻辑
891905

892906

893-
值得注意的是一种组件的Manager只对该种组件进行操作,而System可以对多种组件进行操作
907+
值得注意的是:
908+
1.GameObject和Component的数据被移到了Manager中,它们的逻辑则被移到了Manager和System中,其中只操作自己的逻辑(如getPosition、setPosition)被移到了Manager中,其它逻辑(即行为逻辑,通常需要操作多种组件)被移到了System中
909+
2.一种组件的Manager只对该种组件进行操作,而一个System可以对多种组件进行操作
894910

895911
## 给出UML?
896912

897-
![image](https://img2023.cnblogs.com/blog/419321/202304/419321-20230407093913041-1386894617.png)
913+
<!-- ![image](https://img2023.cnblogs.com/blog/419321/202304/419321-20230407093913041-1386894617.png) -->
898914

915+
**领域模型**
916+
TODO tu
899917

900918

901-
整个UML主要分成三个层级:System、Manager、Component+GameObject,它们的依赖关系为System依赖Manager,Manager依赖Component+GameObject
902919

903-
World不再管理所有的gameObject,但是仍然实现了初始化和主循环的逻辑
920+
总体来看,分为五个部分:用户、World、System层、Manager层、Component+GameObject层,它们的依赖关系是用户依赖World,World依赖System层,System层依赖Manager层,Manager层依赖Component+GameObject层
921+
922+
923+
我们看下用户、World这两个部分:
924+
Client是用户
925+
926+
World是游戏世界,虽然仍然实现了初始化和主循环的逻辑,不过不再管理所有的GameObject了
904927

905928

906929
我们看下System这一层:
907930
有多个System,每个System实现一个行为逻辑
908-
其中,CreateStateSystem实现创建WorldState的逻辑,WorldState包括了所有的Manager的state
931+
其中,CreateStateSystem实现创建WorldState的逻辑,创建的WorldState包括了所有的Manager的state数据
909932
UpdateSystem实现更新所有人物的position的逻辑,具体是更新所有PositionComponent的position;
910-
MoveSystem实现一个人物的移动,具体是使用了挂载到该人物对应的 gameObject上的一个positionComponent和一个velocityComponent,更新了该positionComponent的position
911-
FlySystem实现一个人物的飞行,具体是使用了挂载到该人物对应的gameObject上的一个positionComponent、一个velocityComponent、一个flyComponent,更新了该positionComponent的position
933+
MoveSystem实现一个人物的移动的逻辑,具体是根据挂载到该人物gameObject上的一个positionComponent和一个velocityComponent,更新该positionComponent的position
934+
FlySystem实现一个人物的飞行的逻辑,具体是根据挂载到该人物gameObject上的一个positionComponent、一个velocityComponent、一个flyComponent,更新该positionComponent的position
912935
RenderOneByOneSystem实现渲染所有超级英雄的逻辑;
913936
RenderInstancesSystem实现渲染所有普通英雄的逻辑
914937

@@ -922,30 +945,49 @@ PositionComponentManager、VelocityComponentManager、FlyComponentManager、Inst
922945

923946
PositionComponentManager的batchUpdate函数负责批量更新所有的positionComponent的position
924947

925-
PositionComponentManager维护了gameObjectMap、gameObjectPositionMap这两个Hash Map,前者为positionComponent与gameObject的对应关系,后者为gameObject与positionComponent的对应关系
948+
<!-- PositionComponentManager维护了gameObjectMap、gameObjectPositionMap这两个Hash Map,前者为positionComponent与gameObject的对应关系,后者为gameObject与positionComponent的对应关系 -->
926949

927950

928951
因为VelocityComponentManager、FlyComponentManager与PositionComponentManager类似(只是没有batchUpdate函数),故在图中省略它们的数据和函数
929952

930953

931954

932955
我们看下Component+GameObject这一层:
933-
因为PositionComponent、VelocityComponent、FlyComponent属于Data Oriented组件,所以它们的值是一个index,也就是各自ArrayBuffer中的索引;
934-
而因为InstanceComponent属于其它组件,所以它的值是一个id。它是InstanceComponentManager维护的gameObjectMap的Key和gameObjectInstanceMap的Value
956+
因为PositionComponent、VelocityComponent、FlyComponent属于Data Oriented组件,所以它们是一个index,也就是各自Manager中维护的ArrayBuffer中的索引值;
957+
因为InstanceComponent属于其它组件,所以它是一个id,它是InstanceComponentManager维护的gameObjectMap的Key和gameObjectInstanceMap的Value
958+
959+
GameObject是一个id,它是各个组件Manager中的gameObjectMap的Value和gameObjectXxxMap中的Key
935960

936961

937962
## 结合UML图,描述如何具体地解决问题?
938963

939-
- 现在组件的数据都集中保存在各自的Manager的ArrayBuffer中,从而在遍历同一种组件的所有组件的数据时增加了缓存命中,提高了性能
964+
- 现在各种组件的数据都集中保存在各自的Manager的ArrayBuffer中。因为ArrayBuffer的数据是连续地保存在内存中的,所以在遍历同一种组件的所有组件数据时缓存命中不会丢失,从而提高了性能
940965

941-
- 涉及多种组件的行为放在对应的System中。因为System很轻,没有数据,只有逻辑,所以增加和维护System的成本较低;另外,修改System也不会影响它的下一层-Manager层
966+
- 现在将涉及多种组件的行为放在对应的System中。因为System很轻,没有数据,只有逻辑,所以增加和维护System的成本较低;另外,因为System位于最上层,所以修改System也不会影响Manager层和Component+GameObject层
942967

943968

944969

945970

946971
## 给出代码?
947972

948-
Client代码:
973+
TODO update
974+
首先,我们看下用户的代码;
975+
然后,我们看下创建WorldState的代码
976+
然后,我们看下创建场景的代码
977+
然后,我们看下GameObject操作组件的代码
978+
然后,我们看下移动的相关代码
979+
然后,我们看下飞行的相关代码
980+
然后,我们看下初始化和主循环的代码
981+
然后,我们看下主循环中更新的代码
982+
然后,我们看下主循环中渲染的代码
983+
最后,我们运行代码
984+
985+
986+
### 用户的代码
987+
988+
TODO continue
989+
990+
Client
949991
```ts
950992
let worldState = createState({ positionComponentCount: 10, velocityComponentCount: 10, flyComponentCount: 10 })
951993
```
@@ -1486,8 +1528,8 @@ positionComponentManagerState的positions有3个连续的值是2、2、2,说
14861528

14871529
## 补充说明
14881530

1489-
“组合代替继承”是指组件化思想
1490-
“集中管理组件数据”是指Data Oriented思想
1531+
“组合代替继承”是基于组件化思想
1532+
“集中管理组件数据”是基于Data Oriented思想
14911533
“分离逻辑和数据”是指提出System、Manager、Component+GameObject三层,其中System实现行为逻辑,Manager维护数据,组件和GameObject只是一个number类型的索引或者id值
14921534

14931535

packages/多线程模式/article.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ TODO tu
381381

382382
## 概述解决方案
383383

384-
通过下面的改进来提高性能:
384+
<!-- 通过下面的改进来提高性能: -->
385385
目前所有的逻辑都运行在主线程
386386
因为现代CPU都是多核的,每个核可以运行一个线程,所以现代CPU都支持多个线程并行运行
387387
因此,可以开一个渲染线程和一个物理线程,其中前者负责渲染,后者负责物理计算

packages/管道模式/article.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ tonemap for WebGL2
183183

184184
## 概述解决方案?
185185

186-
通过下面的改进来提高开发效率:
186+
<!-- 通过下面的改进来提高开发效率: -->
187187
1.分离PC端和移动端的逻辑为不同的模块,具体是从Engine中提出两个模块:EngineInPC、EngineInMobile,分别实现PC端和移动端的引擎逻辑;
188188
然后在Engine中判断运行环境,调用对应的模块
189189
2.将每个步骤提出成单独的模块,这样可减少步骤之间的耦合,便于维护
@@ -476,7 +476,7 @@ tonemap for WebGL2
476476
## 概述解决方案
477477

478478

479-
通过下面的改进来实现“通过配置来指定初始化和渲染的步骤”:
479+
<!-- 通过下面的改进来实现“通过配置来指定初始化和渲染的步骤”: -->
480480
将EngineInPC和EngineInMobile改为三个管道模块,分别为:EngineInPCPipeline、JiaEngineInMobilePipeline、YiEngineInMobilePipeline,其中前两个由甲负责,最后一个由乙负责
481481
将每个步骤模块改为一个Job
482482
每个管道模块都有一个渲染管道以及可能会有的一个初始化管道,它包括了步骤模块修改而成的Job

0 commit comments

Comments
 (0)