过往项目中也有MMO游戏,网络同步这块自然走的是状态同步方案。
但因为只负责前端工作,对后端的逻辑只是知其然不知其所以然,所以对自己在状态同步上的理解不是很满意。
刚好圈内大佬麒麟子在cocos商店上架了一款“TGX联机对战全栈框架”,前后端使用的都ts语言,对我来讲是一个很好的学习后端状态同步细节的机会。
于是在这里对TGX框架展开学习,对状态同步方案做梳理、沉淀,并顺带讨论一点程序员高效学习进步的方法论。
---职场学习方法论---
进入职场之后,对个人的要求就是能够快速解决具体范围的问题,所以相对在校学习,职场学习有它迥异的特点,面临的挑战有如下5个——
1、真正能用
学习的知识要能够服务业务,落实到日常工作
2、突发学习
需求和问题通常是突发的,为了解决问题需要快速掌握所需技能
3、时间碎片
职场中不会预留学习时间,而只会给到解决问题的时间
4、零散杂乱
面向解决问题学习虽然能够完成工作,但它有一个很大的缺点,就是学到的知识会非常零散,难以提取和复用
5、不停在变
需求、项目、公司、行业都在变,掌握的技能如何做到可迁移,这是一个问题
根据职场学习的特点,我使用的方法论是 功利性学习 + 框架 = 可迁移
即利用“功利性学习”快速掌握技能,解决真正能用、突发学习的问题;使用“框架”组织知识点,解决时间碎片和零散杂乱的问题,最终实现技能的可迁移。
所以这次对状态同步的学习,首先我会1>只学习TGX框架中与状态同步相关的部分 2>将学习所得沉淀,加入我的技能框架 3>最终做到可迁移,有一套自己面向状态同步的核心算法和设计方案。
---带着问题---
在阅读TGX框架源码之前,我先回忆自己的研发经验,并找到一些资料对状态同步做调研,先对其有个初步的知识框架,然后从源码找答案来验证和补充框架。
0、业务背景
1>同场景中有多个实体
2>实体之间会产生交互,操作会互相影响
1、一句话套路总结
1>前端,所有实体都是演员,通过接收到的数据进行“表演”
2>后端,需要持有游戏世界所有实体的数据(状态),不管数据是从哪来的
3>后端,要做裁判,判定实体操作的合法性和资源的归属
2、状态同步,同步的是什么?
“状态”在这里是一个很抽象且宽泛的概念,可以理解为游戏世界中每一个可变实体的数据集合,可以通过数据还原实体,在实际开发中同步物体的全量或部分(变化)的数据皆可。
“同步”要做的就是将自己控制实体的数据发送给其他玩家,将其他玩家控制实体的数据发送给自己。
3、前后端的架构设计有哪些?
前端发操作,后端计算状态并发给所有前端(完全信任后端),状态完全来自于后端的计算,这是最理想的情况
应用项目
MMORPG(大型多人在线角色扮演游戏)
优点
安全
缺点
后端需要实现完整的逻辑,难度较大
------------
前端发状态,后端转发状态给所有前端(完全信任前端),状态完全来自于前端,应用好场景很狭窄
应用项目
在线教育1(老师)V多(学生),且学生间操作没有联系。学生每次操作时将自己端所有实体的状态打包成快照发给服务器用于转发。
优点
后端只用写转发,逻辑非常简单
缺点
不安全,分分钟外挂,但在线教育不存在外挂问题,算是特殊场景特殊设计。
------------
前端选主机,主机接收操作并计算状态,后端将主机的状态转发给所有前端(完全信任作为主机的前端)
应用项目
一款开房对战游戏,服务器不想写逻辑,又得有一定的反外挂能力
优点
后端只用写转发,逻辑非常简单
缺点
网络绕了一圈,速度更慢,完全不适用对及时反馈要求较高的游戏
所有人可能被主机的网络速度卡住
安全性仍然比较低,主机开挂没得解
想法
后端不想写逻辑,用帧同步最好了
这种特别不好,后端实现逻辑为最优设计。
如果实在不行,干脆抛弃服务器,用p2p通信得了,还能省掉一层服务器转发的时间。
综上,我认为将状态的计算放在后端是最优解,一是为了玩家登录时“还原现场”,二是玩家有资源竞争时需要后端做裁判,三是为了安全,反外挂;
4、如何反外挂?
这里只列举一下,展开讲的话篇幅太大,需要另外开文,况且当前我也不精通。
玩法设计上防外挂
比如当前多数卡牌游戏,抽卡结果是后端给的,战斗过程是假的,外挂无从下手。
或者瞬移、透视等效果在玩法设计上考虑好,使得作弊本身不会带来什么收益。
加密
本地代码、网络通信做好加密,并随着日常更新经常变一变,增加破解成本
数值放在后端
关键状态的计算放在后端,比如伤害数值等
玩家举报与外挂检测
交给玩家,当发现异常时玩家举报,后台通过记录的数据校验是否真的使用了外挂
法律手段
找到外挂制作、售卖方,抓之。但一般小公司没精力、成本做这件事。
--TGX框架学习--
进入正题!
做架构设计/复杂系统设计的最有效方式就是画 UML( 统一建模语言),从源码中反推架构设计和逻辑的运行流程也是如此。
这里选取TGX框架中的坦克大战项目来学习,聚焦于它核心战斗的设计,用类图和时序图来表达。
类图——
上面是核心战斗的类图,省略了非核心的类与非核心的接口。
被标注为绿色的类与坦克自身相关。
Tank
管理坦克自身数据,包括playerid、状态、动画、攻击、被击接口等。
TankMovement2D
将对移动的管理抽象为一个独立的类,坦克的移动以及移动速度等都由它管理。
NameHP
负责管理坦克的血条与名称的显示
TankBullet
负责子弹的位移和碰撞
被标注为灰色的类负责相机跟随
FollowTarget
相机跟随,只有一个lateUpdate()接口,职责就是每帧将相机坐标设置为坦克的坐标。
这个非常优雅,在没有2d摄像机的时代,可是通过相对于角色移动反向位移地图来实现地图滚动的,现在方便多了。
被标注为黄色的类负责控制坦克,包括移动、射击等
TankController
所以它同时和Tank与TankMovement2D关联
最后被标注为蓝色的是核心,状态同步最重要的逻辑在这
TankGameMgr
负责监听网络消息,然后通过监听者模式抛给业务层处理
TankGameScene
在玩家可见的屏幕上做同步,如其他玩家的移动,攻击、受击等。
时序图——
前面已经通过类图了解了坦克大战的静态设计,知道了有哪些类以及它们的职责。
接下来更重要啦——通过时序图搞懂类之间的动态关系,状态同步的核心也在这个动态关系里面。
关键的点有进入战场,新玩家加入、玩家离开、坦克移动,坦克攻击,坦克受击,下面一一分析
约定:绿色表示是服务器模块,蓝色表示是客户端代码
进入战场——
一句话,后端将全部实体的数据发给前端,然后前端通过数据还原现场。
当玩家加入战场时,会先触发后端的onUserEnter接口,它会把所有实体的数据发给前端
其中this._gameData的数据结构如下,可以看出,的确是全部实体的集合
然后前端触发onNet_game_data_sync_push()回调,拿到所有玩家的数据,足以还原整个战场
新玩家加入——
一句话,后端创造新玩家的状态数据,发送给前端,前端通过数据创造实体。
后端创造和发送数据到前端
前端收到并创建实体
玩家离开——
一句话,后端删掉对应实体的数据,然后通知前端删掉对应实体。
后端代码
前端代码
坦克移动——
一句话,每帧都同步实体坐标、角度到服务器,服务器转发给所有玩家
TankController的lateUpdate接口中判断当前实体在移动(speed大于0)时则每帧同步自己的坐标、角度数据给服务器
后端收到数据存起来,并转发给所有前端
其他前端收到数据,同步实体位置、朝向
坦克攻击——
一句话,前端实体攻击,将数据发给后端,后端再转发给所有前端
当实体攻击时前端先行创建一个子弹,然后把子弹的起点等数据发给后端
后端原样转发给所有前端
前端判断如果不是自己,则播放攻击动画,创建一个子弹
坦克受击——
一句话,前端做碰撞检测,发给后端,后端算好被攻击方气血,发给前端做表现。
TankBullet在lateUpdate中检测自己是否与敌方坦克碰撞,如果碰撞则发给后端说击中目标。
后端收到消息则计算伤害数值
--我从TGX源码得到的验证--
一句话,TGX框架已经提供了优秀的状态同步的核心算法和架构设计模板,在具体项目的研发时只需要根据业务,把状态的计算适当地放在后端即可。
套路——
1、前端,的确全是演员。除自己外的所有实体(坦克,即Tank类)都是根据后端发来的状态出场(创建)、下场(销毁)和表演(更新状态)。
在TGX框架中,自己的状态更新是不等待网络回包的,这是状态同步优化的经典做法,即“预测与回滚”,是为了更好的游戏体验。
2、后端,的确持有游戏世界所有实体的数据(状态),不管数据是从哪来的。
新加入的实体由后端生成数据;坐标、朝向、攻击、碰撞判定的数据由前端生成;气血和得分的数据由后端生成。
3>后端,要做裁判,判定实体操作的合法性和资源的归属
体现在实体气血和得分的计算上,是后端做裁判的。
架构设计——
比较符合这张图的设计,区别在于有些状态是由前端计算得来的。
但为了游戏体验和减轻服务器计算压力,将部分状态的计算交给前端处理也是合理的。
反外挂——
尽量往后端放就好了,只涉及业务逻辑开发,架构一点都不用改。
后端计算坐标,前端就没办法做瞬移挂;后端计算伤害数值,就没办法做秒杀挂;后端计算气血就没办法做锁血挂。
--如果是我,我会怎么做?--
学习源码,我的另外一个心得是——抽算法,写架构。
即核心算法可以用源码的,但是对算法的封装要自己写,写一个符合自己设计习惯的架构。
所以我的架构设计是
实体作为演员,由Entity类管理,它有两个子类,即坦克与子弹。
我习惯用状态机管理逻辑,所以为每个实体集成了两个状态机,分别管理逻辑和动画,做到逻辑和表现分离。
由SelfEntityControll作为控制输入,原则上它把操作发给服务器,再由EntityMgr接收来自服务器的回包,最后驱动Entity去“表演”。
一些设计tips
1>状态的计算尽量往后端放
2>自己也尽量等待后端回包后再行动,“移动”这种为了优化体验可以前端先预测,其他操作则要仔细斟酌。
鸣谢
有TGX感觉我也能全栈啦~
阅读TGX源码,和自己的经验做比较,是一种和大佬之间的高效对话。
心动了么?点击下方链接跳转购买(没广告费,是真的好)
《TGX联机对战全栈框架》 https://store.cocos.com/app/detail/5504
可爱的伙伴,都看到这了~ 右下角点个“赞”和“在看”呗