Skip to content

Commit 0c9f457

Browse files
committed
feat: merge to 再看设计原则.md
1 parent 3c05ae3 commit 0c9f457

File tree

16 files changed

+714
-39
lines changed

16 files changed

+714
-39
lines changed

packages/ECS模式/article.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
[TOC]
2-
31
# 普通英雄和超级英雄
42

53
## 需求
64

75

86
我们需要开发一个游戏,游戏中有两种人物:普通英雄和超级英雄,他们具有下面的行为:
7+
98
- 普通英雄只能移动
109
- 超级英雄不仅能够移动,还能飞行
1110

1211
我们使用下面的方法来渲染:
12+
1313
- 使用Instance技术来一次性批量渲染所有的普通英雄
1414
- 一个一个地渲染每个超级英雄
1515

@@ -49,18 +49,22 @@ World是游戏世界,由多个普通英雄和多个超级英雄组成。World
4949

5050
首先,我们看下Client的代码;
5151
然后,我们依次看下Client代码中前两个步骤的代码,它们包括:
52+
5253
- 创建WorldState的代码
5354
- 创建场景的代码
5455

5556
然后,因为创建场景时操作了普通英雄和超级英雄,所以我们看下它们的代码,它们包括:
57+
5658
- 普通英雄移动的代码
5759
- 超级英雄移动和飞行的代码
5860

5961
然后,我们依次看下Client代码中剩余的两个步骤的代码,它们包括:
62+
6063
- 初始化的代码
6164
- 主循环的代码
6265

6366
然后,我们看下主循环的一帧中每个步骤的代码,它们包括:
67+
6468
- 主循环中更新的代码
6569
- 主循环中渲染的代码
6670

@@ -333,6 +337,7 @@ OneByOne渲染 SuperHero...
333337
5.打印了WorldState
334338

335339
我们看下打印的WorldState:
340+
336341
- WorldState的normalHeroes中一共有两个普通英雄的数据,其中有一个普通英雄数据的position为[2,2,2]而不是初始的[0,0,0],说明该普通英雄进行了移动操作;
337342
- WorldState的superHeroes中一共有两个超级英雄的数据,其中有一个超级英雄数据的position为[6,6,6],说明该超级英雄进行了移动和飞行操作
338343

@@ -405,17 +410,21 @@ World是游戏世界,由多个GameObject组成。World负责管理所有的Gam
405410

406411
首先,我们看下Client的代码;
407412
然后,我们依次看下Client代码中前两个步骤的代码,它们包括:
413+
408414
- 创建WorldState的代码
409415
- 创建场景的代码
410416

411417
然后,因为创建场景时操作了普通英雄和超级英雄,所以我们看下它们的代码,它们包括:
418+
412419
- 移动的相关代码
413420
- 飞行的相关代码
414421

415422
然后,我们依次看下Client代码中剩余的两个步骤的代码,它们包括:
423+
416424
- 初始化和主循环的代码
417425

418426
然后,我们看下主循环的一帧中每个步骤的代码,它们包括:
427+
419428
- 主循环中更新的代码
420429
- 主循环中渲染的代码
421430

@@ -791,11 +800,13 @@ OneByOne渲染 SuperHero...
791800

792801
通过打印的数据,可以看到运行的步骤与之前一样
793802
不同之处在于:
803+
794804
- 更新4个英雄现在变为更新4个positionComponent
795805
- 打印的WorldState不一样
796806

797807

798808
我们看下打印的WorldState:
809+
799810
- WorldState的gameObjects包括了4个gameObject的数据,其中有一个gameObject数据的positionComponent的position为[2,2,2],说明它进行了移动操作;
800811
- 有一个gameObject数据的positionComponent的position为[6,6,6],说明它进行了移动和飞行操作
801812

@@ -859,6 +870,7 @@ GameObject不再有数据和逻辑了,而只是一个全局唯一的id。组
859870

860871

861872
- 增加System这一层,来实现行为的逻辑
873+
862874
一个System实现一个行为,比如这一层中的MoveSystem、FlySystem分别实现了移动和飞行的行为逻辑
863875

864876

@@ -867,6 +879,7 @@ GameObject不再有数据和逻辑了,而只是一个全局唯一的id。组
867879

868880

869881
值得注意的是:
882+
870883
- GameObject和组件的数据被移到了Manager中,逻辑则被移到了Manager和System中。其中只操作自己数据的逻辑(如getPosition、setPosition)被移到了Manager中,其它逻辑(通常为行为逻辑,需要操作多种组件)被移到了System中
871884
- 一种组件的Manager只对该种组件进行操作,而一个System可以对多种组件进行操作
872885

@@ -890,6 +903,7 @@ World是游戏世界,虽然仍然实现了初始化和主循环的逻辑,不
890903

891904
我们看下System这一层:
892905
有多个System,每个System实现一个行为逻辑。每个System的职责如下:
906+
893907
- CreateStateSystem实现创建WorldState的逻辑,创建的WorldState包括了所有的Manager的state数据;
894908
- UpdateSystem实现更新所有人物的position的逻辑,具体是更新所有PositionComponent的position;
895909
- MoveSystem实现一个人物的移动的逻辑,具体是根据挂载到该人物gameObject上的一个positionComponent和一个velocityComponent,更新该positionComponent的position;
@@ -974,21 +988,26 @@ Manager层:
974988

975989
首先,我们看下Client的代码;
976990
然后,我们看下Client代码中第一步的代码:
991+
977992
- 创建WorldState的代码
978993

979994
然后,因为创建WorldState时会创建Data Oriented组件的Manager的state,其中的关健是创建各自的ArrayBuffer,所以我们看下创建它的代码
980995

981996
然后,我们看下Client代码中第二步的代码:
997+
982998
- 创建场景的代码
983999

9841000
然后,因为创建场景时操作了普通英雄和超级英雄,所以我们看下它们的代码,它们包括:
1001+
9851002
- 移动的相关代码
9861003
- 飞行的相关代码
9871004

9881005
然后,我们依次看下Client代码中剩余的两个步骤的代码,它们包括:
1006+
9891007
- 初始化和主循环的代码
9901008

9911009
然后,我们看下主循环的一帧中每个步骤的代码,它们包括:
1010+
9921011
- 主循环中更新的代码
9931012
- 主循环中渲染的代码
9941013

@@ -1047,6 +1066,7 @@ export type state = {
10471066
```
10481067
10491068
这是PositionComponentManager的state的类型定义,它的字段解释如下:
1069+
10501070
- buffer字段保存了一个ArrayBuffer,它用来保存所有的positionComponent的数据。目前每个positionComponent的数据只有position,它的类型是三个float
10511071
- positions字段保存了ArrayBuffer的一个视图,通过它可以读写所有的positionComponent的position
10521072
- maxIndex字段是ArrayBuffer上最大的索引值,用于在创建一个positionComponent时生成它的index值
@@ -1468,6 +1488,7 @@ OneByOne渲染 SuperHero...
14681488

14691489

14701490
我们看下打印的WorldState:
1491+
14711492
- WorldState的gameObjectManagetState的maxUID为4,说明创建了4个gameObject;
14721493
- WorldState的positionComponentManagerState的maxIndex为4,说明创建了4个positionComponent;
14731494
- WorldState的positionComponentManagerState的positions有3个连续的值是2、2、2,说明有一个positionComponent组件进行了移动操作;有另外3个连续的值是6、6、6,说明有另外一个positionComponent组件进行了移动操作和飞行操作;
@@ -1624,15 +1645,18 @@ Component+GameObject层:
16241645
首先,我们看下属于用户的抽象代码
16251646
然后,我们看下World的抽象代码
16261647
然后,我们看下System层的抽象代码,它们包括:
1648+
16271649
- CreateStateSystem的抽象代码
16281650
- OtherSystem的抽象代码
16291651

16301652
然后,我们看下Manager层的抽象代码,它们包括:
1653+
16311654
- GameObjectManager的抽象代码
16321655
- DataOrientedComponentManager的抽象代码
16331656
- OtherComponentManager的抽象代码
16341657

16351658
最后,我们看下Component+GameObject层的抽象代码,它们包括:
1659+
16361660
- GameObject的抽象代码
16371661
- DataOrientedComponent的抽象代码
16381662
- OtherComponent的抽象代码

packages/依赖隔离模式/article.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[TOC]
1+
22

33
# 编辑器使用引擎创建场景
44

@@ -33,6 +33,7 @@ Three.js是Three.js引擎
3333

3434

3535
我们依次看下每个模块的代码,它们包括:
36+
3637
- Client的代码
3738
- Editor的代码
3839

@@ -184,6 +185,7 @@ Client通过依赖注入的方式注入Engine接口的一个实现(BabylonImpl
184185
## 给出代码
185186

186187
我们依次看下各个部分的代码,它们包括:
188+
187189
- 用户的代码
188190
- 编辑器的代码
189191
- 容器的代码
@@ -325,6 +327,7 @@ BabylonImplement的implement函数使用了Babylon.js引擎,返回了Engine接
325327
<!-- ,从而消除了外部依赖带来了副作用 -->
326328

327329
哪些依赖属于外部依赖呢?依赖的各种第三方库、外部环境等都属于外部依赖。具体来说:
330+
328331
- 对于编辑器而言,引擎、UI组件库(如Ant Design)、后端、文件操作、日志等都属于外部依赖;
329332
- 对于引擎而言,各种子引擎(如物理引擎、动画引擎、粒子引擎)、后端、文件操作、日志等都属于外部依赖
330333

@@ -398,6 +401,7 @@ BabylonImplement的implement函数使用了Babylon.js引擎,返回了Engine接
398401

399402

400403
我们依次看下各个部分的抽象代码,它们包括:
404+
401405
- 用户的抽象代码
402406
- 系统的抽象代码
403407
- 容器的抽象代码
@@ -495,6 +499,7 @@ export let api1 = function () {
495499

496500

497501
依赖隔离模式主要遵循下面的设计原则:
502+
498503
- 依赖倒置原则
499504
系统依赖于外部依赖的抽象(Dependency)而不是外部依赖的细节(DependencyImplement和DependencyLibrary),从而外部依赖的细节的变化不会影响系统
500505
- 开闭原则
@@ -578,6 +583,7 @@ export let api1 = function () {
578583
洋葱架构与传统的三层架构的区别是颠倒了层之间的依赖关系:洋葱架构将三层架构中的最下层(外部依赖层)改为最上层;将三层架构中的倒数第二层(领域模型层)下降为最下层
579584

580585
洋葱架构的核心思想就是:
586+
581587
- 将变化最频繁的外部依赖放在最上层从而不影响其它层
582588
- 将领域模型放在最下层,使其不受其它层的影响。虽然它的变化会影响其它层,但是它通常比较稳定,不容易变化
583589

@@ -633,18 +639,21 @@ export let api1 = function () {
633639

634640
我遇到过这种问题:3D应用开发完成后,交给3个外部用户使用。用了一段时间后,这3个用户提出了不同的修改外部依赖的要求:第一个用户想要升级3D应用依赖的引擎A,第二个用户想要替换引擎A为引擎B,第三个用户想要同时使用引擎B和升级后的引擎A。
635641
如果3D应用没有使用依赖隔离模式,而是使用调用引擎这个外部依赖的话,我们就需要将交付的代码修改为3个版本,分别满足3个用户的需求。交付的3个版本的代码如下:
642+
636643
- 使用了升级A的3D应用代码
637644
- 使用了B的3D应用代码
638645
- 使用了B和升级A的3D应用代码
639646

640647
因为每个版本都需要修改3D应用中与引擎相关的代码,所以导致工作量很大。如果使用了依赖隔离模式进行解耦,那么就只需要对3D应用做下面的修改:
648+
641649
- 修改AImplement和ALibrary,实现升级
642650
- 增加BLibrary
643651
- 增加BImplement,它使用BLibrary
644652
- 增加ABImplement,它使用BLibrary和升级后的ALibrary
645653
- DependencyContainer增加保存B、AB的闭包变量和对应的get、set函数
646654

647655
这样的话,交付的代码就只有1个版本,从而减少了很多工作量。只是该版本的代码需要在Client中分别为这3个用户注入不同的DependencyImplement,具体如下:
656+
648657
- Client为第一个用户注入升级后的AImplement
649658
- Client为第二个用户注入BImplement
650659
- Client为第三个用户注入ABImplement

packages/再看设计原则/依赖倒置原则/article.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
依赖倒置原则具体是指模块或者函数之间不应该直接依赖,而应该是依赖于它们的接口或者类型。所以说,符合依赖倒置原则的编程可以看作是“面向接口/类型编程”
1818

1919
符合依赖倒置原则有下面的好处:
20+
2021
- 减少细节之间的耦合性
2122
- 便于替换细节
2223
- 提高系统的稳定性

0 commit comments

Comments
 (0)