联机编程对战游戏《代码入侵者》开发分享


基于Unity的MVVM客户端框架及开发经验

项目简介

GM17是一款3D的AI编程游戏,特点是多人协作编程,美术风格是中国风科幻。
游戏以房间为单位进行,玩家进入游戏后,在场景内收集资源,编辑己方机器人AI,从而与敌方机器人战斗。
客户端引擎是Unity2019,我们原本想用自研引擎,但是策划和美术更倾向Unity,因此最后还是选择了Unity。
渲染管线是LWRP,能够在移动端也有很好的画面表现,消耗也比较低。

客户端文件组织结构

客户端脚本文件可分为 框架 和 项目代码 两个部分,分别存在SFramework和ProjectScript文件夹下。
SFramework是我们本次项目的客户端框架,是我为本次MINI项目设计的一个轻量级Unity游戏框架,易于协作和快速开发。
它包括游戏主循环部分、管理者部分、网络部分、UI部分和其他功能支持部分。
ProjectScript是我们的项目开发逻辑部分,在这里我们根据项目需求选用了MVVM架构,我们觉得这是一个很适合Unity开发思维的架构,可以使用Unity的组件系统和其他Monobehavior特性,比如在编辑器中可视化的调试和修改数据、物理引擎、动画帧事件等。

SFramework-Lite框架

我将我们本次项目的客户端框架称为SFramework-Lite,因为它比原本的SFramework更轻量、更易于开发。它基于设计模式组建,用了Manager of Manager的思想,主要包含以下模块:
  • 上图中从GameLoop到GameMgr为游戏的主循环部分,我定义了一套框架对象的生命周期,如Awake,Initialize,Update,Release等,编写脚本时可以遵守这个标准。
  • GameMgr开始是游戏的主程序部分,管理其下的许多子Mgr,每一个Mgr提供了一些通用的功能,或管理一些相关的对象。有些Mgr不需要GameMgr管理其生命周期,因此是独立的Mgr,其他的为有生命周期的Mgr。
  • 除此之外,框架还预先提供好了辅助Unity开发的脚本,如SystemDefine里的layer、tag、enum全局设置,UnityHelper辅助算法和Editor编辑器扩展等。
GameLoop是游戏的入口类,特点是在每个场景,我们只需要创建一个挂载了GameLoop脚本的物体,并选择需要动态加载的场景状态,就可以直接测试目标场景,而无需修改代码、或是从第一个场景开始测试流程。
GameMgr的亮点是:你可以使用GameMgr.Get来访问框架的任何功能!没错,GameMgr是一个外观模式的单例,你所需要获取的功能或对象,都可以通过这个主Mgr来获取。
EventMgr实现全局事件分发,集中管理游戏中的各个事件,是我们后面实现MVVM架构的核心。
CouroutineMgr主要是方便非Mono对象使用协程的。因为我们框架不基于Unity的生命周期,而是用自己的生命周期来控制程序的顺序执行,因此需要提供一些对象来方便使用Monobehavior的功能。
ResourcesMgr负责场景中游戏对象或资源的动态加载,加载的对象来源于Resources文件夹,被加载的对象可使用缓冲池技术,获得当前可用的对象。
UiManager是一个UI框架,主要提供ShowUi和CloseUi等常用接口。UI的开发者只需要设置UI的属性(如显示方式、窗体类型、遮罩类型等),之后调用Ui时框架会自动帮你完成层级控制和生命周期管理,像有的UI窗体需要实现弹出的功能,我们就会用一个栈去管理弹窗的顺序。非常方便使用。
还有的Mgr比如PlayerMgr等就和逻辑挂钩了。它记录着一个从eid到player对应的字典,并且提供了Create、Get、Remove等API来操作对象,还有Hurt、Dead等角色行为的分发控制。这样的设计可以与服务端保持同构,从而更好的将逻辑控制交给服务端。
还有很多功能在此就不细说了……目前GitHub上是有SFramework的完整代码和相关文档的,但是SFramework-Lite的暂时还没有弄,有需要的话可以联系我~

MVVM的开发架构

我们组使用的引擎是Unity,这是一款面向对象的引擎。在mini第一周设计客户端架构的时候,初版策划案是用编程实现技能组合的玩法,因此我们最先考虑的是ECS架构。经过预研发现,Unity自带的ECS目前还很不完善,只是0.1的预览版,支持的组件很少不足以开发,因此我们如果用ECS就得自己实现ECS架构。但是后来策划案变了,改成了AI编程制作机器人的玩法,因此我们最终选择了MVVM架构。
我们参考了Unity的uFrame框架,它将逻辑分为Controller-ViewModel-View三层,效果如下:
  • VIewModel是一个MonoBehaviour,对应一个GameObject实体对象。它只存放数据,并会在Init方法中获取GameObject所拥有的组件数据。所有对数据和组件的操作均通过VIewModel。
  • Controller是一个非Mono类,它会与一个ViewModel相关联。Controller会封装一些行为,来实现对数据和组件的操作。Controller可能会有一些私有状态来辅助行为执行。
  • View主要是游戏中的UI部分,它基于一个叫ViewBase的基类。View会在创建时或用户操作时触发事件,也会接收事件改变UI的显示。
我们的MVVM架构:
只有Controller可以修改ViewModel的数据,View仅能获得数据但无法直接修改。两者间通过事件进行通信,如果这个操作经过了网络层,那么也可以由网络层作为中介来通信。

MVVM举例

以我们游戏中相机为例,FreelookCam类是一个Controller,它的数据存放于FreelookCamData中,它控制相机的实际行为逻辑。而相关的UI也就是UITouchInput,会在监听相机的相关事件,来将玩家输入传给相机。
Controller:
ViewModel:
View:
不足:其实在我们的MVVM框架中,View和ViewModel的Data-Binding,是有一定提升空间的。uFrame是借助UniRx实现依赖注入和响应式编程的,而我们由于mini开发周期有限,没有实现基于ViewModel属性变化的事件机制。但是在我们的设计中,只有Controller可以改动ViewModel的值,因此只需要让Controller和View之间通信就可以了。在实际开发场景中,这样的思路也更容易让队友理解和使用,实现我们的游戏逻辑绰绰有余。

角色位置同步及手感

我们游戏的服务端是TCP协议+状态同步。一开始,我们的位置同步方案选择的是服务端先行的方式。参考了弱网环境下的移动同步优化这篇文章。
服务端先行:玩家摇杆输入->客户端发送计算后的移动方向->服务端收到方向信息->服务端计算对象坐标->客户端收到坐标开始移动
服务端先行存在的问题
1. 追及问题
若客户端移动速度快,则会比服务器先行,会被服务器拉回。
若服务端移动速度快,则会比客户端先行,客户端会延时到达目标点。虽然不会被拉回,但体验有点不顺畅。
追及问题可以由客户端和服务端使用相同的tick时间+预测计算的方法来解决。我们测试了一个最合适的移动速度,算出在无延迟下客户端和服务端速度比例(抹平同步所需时间),从而实现位置同步(比例系数是必须要算的,因为如果不处理消息包的延迟,角色移动就肯定会有卡顿现象)。相当于服务器移动速度要比客户端快一些。
2. 固定速度
摇杆同步为了避免追及问题,角色移动速度完全由服务端决定,不能实现主机上根据摇杆输入幅度控制移速的效果。
3. 碰撞问题
这个其实是服务端先行普遍存在的问题,服务端没有碰撞信息,因此一旦客户端发生碰撞,两者位置就会不同步。对玩家的体验就好像撞墙后移动会被强制拉到另一个位置一样。
4. 延迟问题
由于服务端是TCP,相对UDP来说延迟还是要高一些,因此即使在没有前面几个问题的情况下,摇杆输入后还是有一点延迟,相比客户端本地移动要稍微迟钝一点。
由于存在以上问题,我们最终还是选择了客户端先行,主要解决了碰撞问题,同时又能给玩家带来更好的手感。
客户端先行
玩家摇杆输入->客户端进行移动->客户端发送当前坐标->服务端收到坐标进行验证->服务端坐标广播->其他客户端移动
客户端先行不会再发生追及问题,移动速度也可以变化了,碰撞交给客户端处理,本机移动如果通过验证的话是0延迟的。服务端先行的问题一下子都解决了。
客户端先行的缺点是:安全性降低,其他客户端移动会有点延迟。
另外还有一个可能的问题,由于服务端没有碰撞,因此客户端角色如果因被碰撞而改变了原本的位置的话,就会和服务端位置产生较大的误差。这可以通过取消角色间的相互碰撞、或实现角色相互碰撞不移动的方式来解决。
手感优化
在客户端的移动方式确定之后,手感的优化点就主要体现在UI输入上了。
我们可以分别把 摇杆 和 相机划屏 做成两个UI,划定他们的输入区域,比如左下角是摇杆,中间和右侧是相机划屏。
摇杆是Floating的,当玩家在摇杆区域按下触摸屏,摇杆就会在按下的位置显示出来,并且在手指抬起前保持固定。这也是目前大部分手游摇杆所采取的方式。
划屏原理是获取到每一帧手指滑动的位移值,再按照位移值进行相应的相机旋转。相机有实现碰撞检测,以避免某些物体遮挡住相机导致看不到主角的情况,能在发生碰撞时自动拉近视角到合适位置。
另外我们有实现InputMgr,它可以同时支持键盘移动和摇杆移动,同时支持鼠标控制视角和划屏控制视角,这为PC端的测试调试提供了很大便利。
实际测试的效果表明,客户端先行的位置同步方案效果不错,UI响应敏捷,手感顺滑,且几乎观察不出角色移动的误差。

其他经验

首先是要注意使用LWRP管线的话,默认是不支持多摄像机的,因此原本有些需求的实现受到了限制。Ui也不得不使用overlay的方式渲染。
打包到移动端时可以使用UI可视化日志的形式方便自己调试,这样很容易就能发现遇到的问题了。
优化主要用到了遮挡剔除、批处理、对象池和UI图集打包,程序方面有些地方tick频率不高,就用协程代替了Update。原本美术给的场景drawcall在900+,优化后可以保持在200左右,即使在中配机上稳定跑30帧也是没问题的。
因为开发过程中就很注意内存管理,因此无论是运行时内存占用还是打包的包体都不大,平时有良好的开发习惯的话就能为自己后面省很多坑。
另外比较重要的就是协作经验了,建议团队一定要用相同的开发环境。遇到bug,先确定底层是否健壮,避免因为提供底层支持的队友出问题而自己一直在上层找问题。

GM17的其他资料
包括Review.ppt,Miniday.ppt,服务端架构,源码,UX尖叫度反馈报告,UI设计等













comments powered by Disqus