@@ -350,8 +350,8 @@ worldState的superHeroes中有一个超级英雄数据的position为[6,6,6],
350
350
## 概述解决方案?
351
351
352
352
353
- 通过下面的改进来解决重复和继承的问题:
354
- 使用组件化的思想 ,用组合代替继承,具体如下:
353
+ <!-- 通过下面的改进来解决重复和继承的问题: -->
354
+ 基于组件化的思想 ,用组合代替继承,具体如下:
355
355
将英雄抽象为GameObject
356
356
将英雄的行为抽象为组件,并把英雄的相关数据也移到组件中
357
357
英雄通过挂载不同的组件,来实现不同的行为
@@ -848,67 +848,90 @@ worldState的gameObjects包括了4个gameObject的数据;
848
848
849
849
850
850
## 提出问题
851
- TODO continue
852
851
853
852
- 组件的数据分散在各个组件中,性能不好
854
- 现在所有人物的position的数据一对一地分散保存在各个PositionComponent组件中,那么在遍历所有的position数据时,会因为CPU中不容易缓存命中而带来性能损失
853
+ 如position数据现在是一对一地分散保存在各个positionComponent组件中(即一个positionComponent组件保存自己的position),那么如果需要遍历所有组件的position数据,则需要遍历所有的positionComponent组件,分别获得它们的position
854
+ 因为每个positionComponent组件的数据并没有连续地保存在内存中,所以会造成缓存命中丢失,带来性能损失
855
855
856
+ <!-- 因为在遍历每个positionComponent组件时,需要将它的所有数据都载入CPU的二级缓存中 -->
857
+ <!-- 当它的大小大于CPU的二级缓存的大小时,就无法载入而造成缓存无法命中,从而带来性能损失 -->
858
+ <!-- 当它的大小大于CPU的二级缓存的大小时,就无法全部载入,而需要
859
+ 而造成缓存无法命中,从而带来性能损失 -->
856
860
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组件,修改它们的数据
860
865
861
- 如果有更多的涉及多种组件的行为 ,就需要为每个行为增加一种组件。
862
- 因为组件比较重,既有数据又有逻辑,所以增加的开发成本较高
863
- 另外,组件与GameObject是聚合关系 ,而GameObject和World也是聚合关系,它们都属于强关联关系,所以增加组件会较强地影响GameObject和World模块,也增加了成本
866
+ 如果增加更多的涉及多种组件的行为 ,就需要为每个行为增加一种组件。
867
+ 因为组件比较重,既有数据又有逻辑,所以增加组件的开发成本较高
868
+ 另外,因为组件与GameObject是聚合关系 ,而GameObject和World也是聚合关系,它们都属于强关联关系,所以增加组件会较强地影响GameObject和World,这也增加了开发成本
864
869
865
870
866
871
867
872
# [ 给出使用模式的改进方案]
868
873
869
874
## 概述解决方案
870
875
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放在该层中
878
890
879
891
880
- 我们增加Manager这一层,来维护和管理GameObject和组件的数据
881
- 其中GameObjectManager负责维护所有的gameObject的数据 ;
882
- 四种组件的Manager负责维护自己的ArrayBuffer ,操作属于该种类的所有组件
892
+ 我们增加Manager这一层,来管理GameObject和组件的数据
893
+ 这一层有GameObjectManager和四种组件的Manager,其中GameObjectManager负责管理所有的gameObject ;
894
+ 四种组件的Manager负责管理自己的ArrayBuffer ,操作属于该种类的所有组件
883
895
884
- 值得注意的是将保存gameObject与挂载的组件的对应关系的Hash Map放在哪里?
885
- 它们可以放在GameObjectManager中,也可以分散放在组件的Manager中。
886
- 考虑到为了方便组件直接就近获得自己挂载到的GameObject,所以我们选择将其分散放在组件的Manager中
896
+ 值得注意的是:
897
+ <!-- 将“gameObject挂载了哪些组件”的对应关系的Hash Map放在哪里? -->
898
+ 将“gameObject挂载了哪些组件”的数据放在哪里?
899
+ 它们可以放在GameObjectManager中,也可以分散地放在各个组件的Manager中。
900
+ 考虑到为了方便组件直接就近获得自己挂载到的GameObject,所以我们选择后者
887
901
888
902
889
903
我们增加System这一层,来实现行为的逻辑。
890
- 一个System对应一个行为,比如我们加入MoveSystem、FlySystem来分别实现移动、飞行的行为逻辑
904
+ 一个System对应一个行为,比如这一层中的MoveSystem、FlySystem分别实现了移动和飞行的行为逻辑
891
905
892
906
893
- 值得注意的是一种组件的Manager只对该种组件进行操作,而System可以对多种组件进行操作
907
+ 值得注意的是:
908
+ 1.GameObject和Component的数据被移到了Manager中,它们的逻辑则被移到了Manager和System中,其中只操作自己的逻辑(如getPosition、setPosition)被移到了Manager中,其它逻辑(即行为逻辑,通常需要操作多种组件)被移到了System中
909
+ 2.一种组件的Manager只对该种组件进行操作,而一个System可以对多种组件进行操作
894
910
895
911
## 给出UML?
896
912
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) -->
898
914
915
+ ** 领域模型**
916
+ TODO tu
899
917
900
918
901
- 整个UML主要分成三个层级:System、Manager、Component+GameObject,它们的依赖关系为System依赖Manager,Manager依赖Component+GameObject
902
919
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了
904
927
905
928
906
929
我们看下System这一层:
907
930
有多个System,每个System实现一个行为逻辑
908
- 其中,CreateStateSystem实现创建WorldState的逻辑,WorldState包括了所有的Manager的state ;
931
+ 其中,CreateStateSystem实现创建WorldState的逻辑,创建的WorldState包括了所有的Manager的state数据 ;
909
932
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 ;
912
935
RenderOneByOneSystem实现渲染所有超级英雄的逻辑;
913
936
RenderInstancesSystem实现渲染所有普通英雄的逻辑
914
937
@@ -922,30 +945,49 @@ PositionComponentManager、VelocityComponentManager、FlyComponentManager、Inst
922
945
923
946
PositionComponentManager的batchUpdate函数负责批量更新所有的positionComponent的position
924
947
925
- PositionComponentManager维护了gameObjectMap、gameObjectPositionMap这两个Hash Map,前者为positionComponent与gameObject的对应关系,后者为gameObject与positionComponent的对应关系
948
+ <!-- PositionComponentManager维护了gameObjectMap、gameObjectPositionMap这两个Hash Map,前者为positionComponent与gameObject的对应关系,后者为gameObject与positionComponent的对应关系 -->
926
949
927
950
928
951
因为VelocityComponentManager、FlyComponentManager与PositionComponentManager类似(只是没有batchUpdate函数),故在图中省略它们的数据和函数
929
952
930
953
931
954
932
955
我们看下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
935
960
936
961
937
962
## 结合UML图,描述如何具体地解决问题?
938
963
939
- - 现在组件的数据都集中保存在各自的Manager的ArrayBuffer中,从而在遍历同一种组件的所有组件的数据时增加了缓存命中,提高了性能
964
+ - 现在各种组件的数据都集中保存在各自的Manager的ArrayBuffer中。因为ArrayBuffer的数据是连续地保存在内存中的,所以在遍历同一种组件的所有组件数据时缓存命中不会丢失,从而提高了性能
940
965
941
- - 涉及多种组件的行为放在对应的System中 。因为System很轻,没有数据,只有逻辑,所以增加和维护System的成本较低;另外,修改System也不会影响它的下一层-Manager层
966
+ - 现在将涉及多种组件的行为放在对应的System中 。因为System很轻,没有数据,只有逻辑,所以增加和维护System的成本较低;另外,因为System位于最上层,所以修改System也不会影响Manager层和Component+GameObject层
942
967
943
968
944
969
945
970
946
971
## 给出代码?
947
972
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
949
991
``` ts
950
992
let worldState = createState ({ positionComponentCount: 10 , velocityComponentCount: 10 , flyComponentCount: 10 })
951
993
```
@@ -1486,8 +1528,8 @@ positionComponentManagerState的positions有3个连续的值是2、2、2,说
1486
1528
1487
1529
## 补充说明
1488
1530
1489
- “组合代替继承”是指组件化思想
1490
- “集中管理组件数据”是指Data Oriented思想
1531
+ “组合代替继承”是基于组件化思想
1532
+ “集中管理组件数据”是基于Data Oriented思想
1491
1533
“分离逻辑和数据”是指提出System、Manager、Component+GameObject三层,其中System实现行为逻辑,Manager维护数据,组件和GameObject只是一个number类型的索引或者id值
1492
1534
1493
1535
0 commit comments