Unity手游实战:从0开始SLG——ECS战斗(三)逻辑与表现分离

时间:2024-04-08 19:11:56

上一篇大概讲了ECS的设计思想,有提到优势也有提到劣势,优势是设计层面的,劣势是实现层面的。那么一套好的框架就是要保证如何保持优势的设计,而在实现时规避劣势所带来的问题

逻辑和表现分离、有时候也叫业务和数据分离。在讲这部分内容的时候我好想先讲一讲网络同步这部分的内容(关联性还是挺大的),但是一展开就要跑题了,目前这个系列预期写6章,最后看看如果要不要加一章跟ECS不相关的部分来讲状态同步和帧同步。

这个概念非常好理解,如果简单描述的话,就想象一下主机和显示器的关系。所有的运算、输入、结果全都来自于主机,并且它完全不关心你用的是显示器还是电视机亦或是传真、打印机。谁适配了我的接口,谁就可以按照自己的意愿去输出自己个性的表现。

如果复杂了描述的话就想象一下状态游戏里,服务器和客户端之间的状态同步(所以我想先讲网络同步。。。)。服务器告知客户端,场景(0,0)位置有个玩家,穿着大裤衩,背着双肩包,一双洞洞鞋,并且发际线还很少。客户端根据服务器下达的指令加载了一个程序员。下一秒,一个策划走了过来,修改了一个需求,程序员走到位子上打开电脑开始工作。

在不影响逻辑的情况下,表现层可以自己发挥。比如,策划修改了一个需求之后,程序员可以先“呸”的一声吐口谈,然后再走到位子上。如果他“呸”的这一下会导致策划暴打程序,那么这个过程就必须交给逻辑层去控制,如果没有影响,就可以表现层自己发挥(这就跟我们在战斗里,逻辑层不使用RVO,而表现层可以添加一样)。

如果在往更复杂一点的地方想象就是帧同步的游戏下,服务器完全不参与计算了,只同步所有玩家的输入给客户端,客户端需要自己在内部去维护一个逻辑层来控制确保计算精准(不精准就会导致不同步),表现层根据逻辑层的状态来表现和展示。

为什么要做分离?

拿帧同步来讲,如果做逻辑表现分离,等于要在客户端额外写一套服务器出来。花这么大的代价,那么带来的收益是什么呢?

  • 解耦逻辑和表现分离的就基本原则就是逻辑层能掌控一切,表现层受逻辑层驱动,在不影响逻辑的前提下自主表现,那么就要求逻辑层一定要能完全脱表现层独立存在。如果我们把这个逻辑层放在服务器,那就是Client-Server模式,如果放在客户端那就是帧同步的制作方式(王者荣耀就是这么干的)。

  • 安全既然能解耦了,那么就可以让业务更加安全。安全来自两个部分,一个是CS模式下对数据和外挂的安全,一个是帧同步模式下,表现层的BUG影响到逻辑的安全。

  • 倍速如果只操作逻辑层,不考虑表现那么就可以通过加快或者减慢逻辑帧率,快速实现0.5倍,2倍,4倍等变速运算,也可以进行秒算验证结果。

  • 回放 数据是独立存在的,运算和输入也是固定的,那么只要保证逻辑计算一致,那么得到结果必然一致。所以只需要保存很少的初始变量,和中间输入就可以完成整体回放,数据量还贼小。(想想WAR3 一场战斗40分钟,录像文件只有200K。怀念上学的时候,去网吧下载录像回家看的日子。)不过这里要提一个缺点,那就是版本和数据必须一致,否则计算就会不一致。

  • 移植表现层可以根据自己使用的开发引擎做快速移植,而不需要修改整体逻辑。呃~可能还有其他好处,但是我一时想不到了。。。

PVP和PVE架构

说架构之前,先说战斗需求。我们是一个SLG的游戏,PVP的战斗在于大地图的掠夺,是自动战斗无需玩家操控的。PVE的战斗是手动(当然也可以自动),区别也不大,就是英雄技能是手动施放还是自动施放。综合需求的话,可以得出,PVP由服务器计算(涉及离线和安全),PVE由客户端计算,然后将技能释放的相关信息记录,一起发往服务器验证。

来看2个流程图:

PVP

 

Unity手游实战:从0开始SLG——ECS战斗(三)逻辑与表现分离

PVE

 

Unity手游实战:从0开始SLG——ECS战斗(三)逻辑与表现分离

这种实现方式,战斗完全在客户端写,又高效又安全,查BUG简单,还节省服务器人力。

逻辑帧和表现帧

帧的概念大家都很清晰了,那么逻辑帧的意思就是逻辑层的帧率,表现帧就是在表现层的帧率,那么为什么要区分它们呢?因为设备的性能存在差异,同时逻辑帧的一致性才能确保计算准确。

一般设备能流畅跑游戏的帧率是24帧,但是大部分时候我们会以30帧作为标准。高端机器上,会开60帧或者不设上限(高帧率意味着计算量更大,耗电和发热量也会更大)。

而逻辑帧往往是用不了这么高的,士兵攻击频率1秒1次,是不用每16ms(60帧)去计算一次的,我的项目设置为15帧已经可以满足了。那么表现层其实是需要对某些表现做插值处理,最明显的就是移动。

移动速度假如是60m/s,逻辑15帧每帧跨度4m,如果不补帧看起来就像是卡顿。所以表现层是要根据自己的帧率对移动进行插值,保证平滑。

逻辑帧是独立驱动的,所以它有自己的核心逻辑。

看下代码:

Unity手游实战:从0开始SLG——ECS战斗(三)逻辑与表现分离

TotalPassTime是当前已经过去的总时间,下面接着是一个While循环,循环的判定条件就是当前pass的总时间只要大于下一帧的时间就执行逻辑帧。这这样的目的是就是为了解决不会因为某些帧的间隔过大而导致逻辑帧的波动。简单的来说就是追帧。战斗到现在已经过去10秒了,理论上有下一帧是151帧,而这个时候实际因为某些原因才计算到100帧,那么接下来会在while里循环直到追平当前帧。这也是服务器能够秒算(给一个初始非常大的pass值),以及客户端能够实现倍速战斗的原因。看下下面的代码:

Unity手游实战:从0开始SLG——ECS战斗(三)逻辑与表现分离

UpdateLogic的客户端逻辑是由deltaTime和ClientFrameDelta两个部分来控制的,deltaTime*控制倍率能控制每次补偿的时间差(实现倍率播放),ClientFrameDelta则是初始化帧的进度值(实现秒算)。

好了,战斗最核心最难的地方已经讲完了。

下一篇看看实战代码里的部分优化。