妖尾历经几年开发,终于在今年6月底顺利上线,至今运营两个多月,笔者从2017年初参与开发,主要负责妖尾战斗系统开发,一路解决了一些技术问题,踩了一些坑,感觉有不少点是值得记录和分享的,希望能借几篇文字,系统性总结MMORPG战斗系统的开发经验。
本文主要介绍战斗录像系统,战斗录像基本是所有MMORPG游戏的标配系统,它同时也能成为开发调试利器,在整个开发阶段扮演重要角色。

首先是调试利器


一些项目组开发战斗系统时,可能会优先开发涉及表现的相关功能,迭代的新增战斗表现,修复Bug,直到整个战斗表现看起来相当完整了,到了后期再应策划要求,补充战斗录像系统。笔者项目是在开发中期加入战斗录像功能的,在经历完整个战斗开发阶段后,得出的经验是
尽可能在基础框架搭建、前后台开始联调阶段就同步开发战斗录像系统,利用战斗录像来辅助系统开发、调试
。到了项目中后期,战斗录像会发挥更大的用处,此时战斗系统已经提交到SVN版本控制,项目组所有人都可以体验到战斗系统,所有人都或多或少地扮演测试人员的角色,项目群会频繁地反馈战斗系统的表现问题,诸如报错、卡死,单位诈尸等等,什么反馈都会有,总之发挥你的想象力。当时开发会频繁地奔波于各个项目组成员的电脑面前,沟通、查看日志,尝试弄清问题,有了战斗录像后,我们会让对方给录像文件,在本地环境重放战斗录像,重现现场慢慢定位问题。
战斗录像能多大程度的辅助开发调试,取决于相关工具链有多完善,下面介绍妖尾项目对于工具链的打造。首先简单看下战斗录像框架:


一般来说,网络底层往上还会有一层业务网络层,妖尾的业务网络层分成两个,一个负责普通业务逻辑,一个网络层供战斗专用。通过在战斗网络层接口插桩,战斗录像模块就能收集一场战斗的所有数据。战斗结束后,自动将该场战斗数据保存成本地录像文件,当然,我们还要提供手动保存录像接口,以便战斗中途卡死了也能保存录像。虽然有了战斗数据,还要配备一套完整功能的GUI工具才能提高调试效率,因此笔者基于Unity开发了战斗录像播放器工具。
 


上图是战斗录像播放器经过几次迭代后的截图,除了实现最基本的播放录像、查看数据功能,还有查看设备数据,上传/下载录像、生成战斗播报、差量构建指定回合战场包等功能。笔者觉得在
开发初期,先实现播放录像、查看数据的功能就能满足大部分调试需求了
,开发时间成本只有2-3天,但它会在之后1-2个月甚至更久的前期开发阶段帮你缩短调试定位时间,节省更多时间早(bu)点(cun)下(zai)班(de),或帮策划做更多需求,重要的是解放心态,不再疲于沟通Bug,构造现场,因为现场就在录像里。

简单描述这套调试工具的使用姿势:

* 开发过程中遇到了战斗Bug,如果在第一时间无法判断Bug原因,先保存录像,再逐步分析问题。
*
选择报错的战斗录像,通过时间戳/快速模式重跑战斗,逐步缩小问题范围:观察战斗录像播到第几个回合报错,是资源加载、选招还是表演阶段报错,通过报错前的日志,逐步定位是哪个类哪个接口的问题,再猜测并验证某行代码,直到问题解决。
* 如果不是卡死报错,战斗也能跑完,但策划反馈某个技能/Buff表现与预期不同,就要查看关键表演包的数据,看是后台传的有问题,还是前台表现没做对。
* 上面两类问题的排查通常是无法一步到位的,排查过程会不断追踪代码给可疑代码打Log,会临时修改某些变量,会临时修改某段代码逻辑,依靠不断重跑战斗来验证。
*
解决Bug的过程也少不了跟后台的沟通,在这之前,后台重数据轻表现,前台重表现轻数据,导致一种现象就是后台找前台问表现,前台找后台问数据,沟通成本比较高。有了这套工具,前台开发对于这场战斗包括服务器、角色ID、战斗ID、战场ID,协议数据等信息都了如指掌,快速分析出是前台问题就直接修复,是后台问题就告诉对方去修复哪块数据。
这里另外分享1个Bug调试修复的经验。个人认为Bug修复总时间 = 问题沟通时间 + 问题定位时间 + 代码修改时间 +
编译验证时间,像战斗这类大型系统,可能会经历多轮问题定位、代码修改、编译验证才能修好1个Bug。Lua代码做好Hot
reload开关,最好做到修改某处代码,重进战斗就能验证最新代码。每次重启游戏至少花费30+秒,1个Bug平均几次重启验证就是几分钟时间,做好Hot
reload节省下的时间相当可观。


初期在项目组内推行用录像反馈战斗Bug时,我们让大家把保存下来的录像文件单发给战斗开发来调试,很快发现用户体验并不友好,不是所有人都是开发,大家不清楚录像保存到哪个目录了,找到目录,他们也弄不清楚要发哪个录像给开发。在忍受了一段时间的灵魂三连问后,笔者又加上了录像上传/下载功能。


上面两张图是录像上传/下载流程及录像下载页面。我们将Bug反馈操作简化成游戏内一键反馈,点击按钮就能自动保存录像文件,并将二进制文件数据Base64编码成字符串,利用魔方质管组帮忙搭建的Web服务,通过Http请求将数据上传到Web服务器保存数据库,开发通过Web页面就可以搜索/下载base64字符串格式的录像文件,最后录像播放接口做适配,支持二进制/base64字符串两种格式数据的录像播放,整个环节就打通了。


开发阶段我们自行开发了战斗录像来辅助调试,确实也是到了战斗系统基本稳定后,策划们才前后提了战斗录像的正式需求,先做了一版基于服务器保存的活动录像,又做了一版基于客户端保存的战斗录像大厅。


前后做这两版录像需求,虽然都是观看录像,但其实现大不相同,因此需要谨慎设计整个录像模块,让两套逻辑独立并行,能共用底层功能,并尽量保持外部接口一致性。上图是整个战斗录像的模块划分,可划分为实现战斗录像基础功能的核心模块,及涉及界面UI的两版业务功能模块。BattleReplayManager是核心类,它对外接收录像相关的控制请求,对内调度其他核心模块类,获取/保存/构造数据,控制录像播放流程,并通过给战斗网络层发送协议数据影响战斗表现。

服务器录像


基于服务器保存的活动录像,所有数据都由服务器提供。前台首先发送观看录像请求,接收录像概要数据包,获取战斗波次/回合等信息用于显示和跳回合。收到初始战场包后进入战斗,在每回合表演完后请求下一回合表演数据。正常播放录像时,收到的协议数据跟普通战斗是一样的,但如果在战斗中途跳回合,除了新回合的表演包,还会收到新回合的战场包,用于恢复新回合初的战场单位状态。这个过程跟战斗断线重连恢复战场是同一套逻辑,因此把战斗断线重连的坑填完,实现服务器录像基本没有难点。

客户端录像

相对服务器录像,实现基于客户端保存的录像功能要考虑比较多问题:

* 确定录像数据结构,用什么数据结构存储一场战斗的所有协议及相关信息较优?
* 保证录制数据完整性。网络抖动、切出游戏再切回来等场景可能会导致少了某回合表演数据怎么办?
* 如何实现跳回合。一场正常战斗的协议包,除了初始战场包,每个回合只有表演包,没有战场包,跳回合怎么恢复战场状态?
* 录像上传/下载的传输策略。协议收发有64kb限制,录像文件大小超过了怎么办?
* 保证用户体验。评估极限情况的录像文件大小,保证流畅的录像观看体验。
模块开发初期就考虑这些问题,就可以避免基础设计出错,后期积重难返的尴尬情况。

1. 录像文件结构

首先是确定录像文件格式,由于妖尾协议基于pb通信,录像文件一开始就没有打算自定义二进制格式,而是直接基于pb定义数据结构,这样有几点好处:

* pb传输效率高,而且开发熟悉pb,不像自定义格式还有理解成本,开发效率也高。
*
协议与录像文件采用同种格式,比较容易根据查看列表,上传/下载录像等业务去反推最优的录像文件数据结构。让每份录像文件既可以有战斗录像数据,也有关于录像大厅的业务数据,一次设计,解决两个问题。
* pb支持数据结构嵌套,列表,能做出录像头、录像数据块设计,上传/下载协议也容易切分录像文件做分块传输。

基于几点考虑,录像文件由BattleReplayFile录像头、BattleReplayFileBlock录像数据块两部分组成。BattleReplayFile的blocks字段用于存放BattleReplayFileBlock列表,BattleReplayFile其他字段是概要信息。这样查看录像列表时,后台只需要返回不带blocks数据的BattleReplayFile列表即可。上传/下载录像时也可以先传录像头、再批量分次传录像数据块。
message BattleReplayFile
{
    optional string name = 1;                       // 录像文件名
    repeated BattleReplayFileBlock blocks = 2;      // 协议文件块
    optional uint32 block_num = 3;                  // 协议文件总块数
    repeated string ext_info_keys = 4;              // 录像额外信息参数Key
    repeated string ext_info_values = 5;            // 录像额外信息参数Value
    ... // id、时间、双方成员、回合、波次等录像概要信息
    ... // 简介、点赞、收藏等录像大厅业务信息
}

message BattleReplayFileBlock
{
    optional uint32 index = 1;                  // 协议块序号
    optional string name = 2;                   // 协议类名
    optional bytes data = 3;                    // 协议数据
    ... //时间、回合等其他信息
}

2. 录像文件校验


网络抖动、切出游戏再切回来等情况导致断线重连,可能导致战斗录像数据损坏,因此保存本地前先做录像文件校验,判断有没有丢关键协议包,包括初始战场包、入包表演包、各回合表演包及退出战场包,保证协议包序,通过校验才保存录像文件,不通过就提示玩家录像数据损坏无法保存。

3. 录像回合跳转


一场战斗录像单靠收到的协议包,可以正常顺序播放整个战斗,却不能跳转回合播放,因为中间跳过了几回合的表演演算,战斗逻辑层无法将战场数据修正成跳转回合的状态。服务器录像可以依靠后台发跳转回合战场包做恢复,客户端录像就要靠前台自己处理,用录像表演包演算出跳转回合的战场状态。


第一直觉是在战斗逻辑层处理跳出的表演包,只是跳过表演,直接做数据演算,但稍加思考会发现有很多问题:战斗逻辑层里,数据与表现基本耦合在一起,毕竟这样的编码实现方式最直观。想抽离表现只演算数据,只能在原有代码里加ifelse分支,重写数据演算逻辑。几十个表演类,新增这么多分支,编码再加调试,必然失去对代码的把控,也破坏了原有系统稳定性。即使哼哧哼哧硬写下来,也会发现只实现了向后跳转回合,没实现向前跳转回合,因为战斗逻辑层实现的是按回合往下演算的逻辑。


跳出这个误区,我们认为战斗录像数据应该要有每个回合的战场包,跳转时供战斗逻辑层重置回合战场,因此后台修改了战斗逻辑,每回合都会发当回合战场包,这些战场包做了特殊标记,只用于录像存储,不会影响战斗逻辑,实现起来很快,但也清楚有明显效率问题。


基本上,战场包都会比表演包大,甚至大很多,如果某个回合技能不太复杂,那表演包数据其实非常小,为了实现跳回合,由后台给每个回合加发战场包,会非常影响战斗的协议数据量,保存录像文件变大,也会增加上传/下载录像时的负担。这么实现不合理的点在于,每回合战场包其实是冗余数据,每回合状态是可以通过初始战场包加表演包推算出来的。为了优化这个问题,前台实现了一个战场包构建器,以初始战场包、回合1~n-1表演包为输入,输出目标回合n的战场包。这样在保存录像时不需要保存回合战场包,录像跳转回合时由构造器动态生成战场包即可。编写调试战场包构建器时,要注意检查前后台的战场包差异,我们会打印战场包数据,通过Beyond
Compare查看差异,不断调整代码,直到构建的关键数据一致为止。战场包构建器调试好后,只要后续不新增表演类型,就可以保证构建器可信可用,即使新增表演,代码工作量也很少。

优化完做下简单测试,打了一场40回合的5v5
pvp战斗保存录像,比较两种方案的保存录像文件大小:优化后文件大小是优化前的65%,减少了252KB,由于5v5pvp表演复杂,因此回合表演包数据本身也非常多,换做是一般的战斗,数据优化比率会更高。

4. 录像上传/下载策略


妖尾一次协议收发有64KB大小限制,看前面的数据可知,回合数比较多的战斗录像文件大小肯定会超过64KB,我们既不希望上传/下载录像单次传输的数据量超过64KB,又不希望单次传输数据量太少,导致协议发送次数过多,浪费太多时间在RRT上,因此采用的录像传输策略是,首次传输单独发送录像头,后续传输录像数据块切块传输,保证每次传输的所有BattleReplayFileBlock的data总大小不超过50KB。采用这样的策略,5回合以内的小型战斗基本都能分2次传输完毕,像上面的5v5
pvp大型战斗则需要进行11次传输。这就引出了下个问题思考,大型战斗的录像观看会不会有体验问题。

5.流式传输及录像缓存


战斗录像大厅的设计初衷,是让玩家可以自主分享/观看他们觉得满意的战斗录像,所以我们猜测玩家会比较多的上传/下载/观看大型pvp战斗录像,对于上传而言并不会有什么问题,因为就是一次性操作,但对下载/观看场景就要尽量进行优化,我们不希望玩家每次看录像,都要有感知地等待一会,等上10次网络回包,下载完录像文件才能观看录像,也不希望玩家每次看录像都得重复下载文件,对玩家的手机流量也很不友好。

针对这两点问题,战斗录像参考网络视频的做法,加上了流式传输及录像缓存的特性。

如上图所示,流式传输的目的在于优化玩家观看新录像的体验,不管一个完整的录像有多大,需要多少次传输才能完成,只需要先获得部分头部数据,就能观看录像。
前台只需要头2次回包,获取录像概况、初始回合战场包和表演包,就足以表演第1回合的战斗,进入录像战斗后,静默下载其余的录像数据,一般后续的录像数据下载速度远远快于战斗表演速度,这样完全不影响整场战斗的录像观看。假设网络环境极端恶劣,表演完当前回合战斗后,后续录像数据还没返回,BattleReplayManager会每帧轮询等待下个回合表演数据,即使网络断掉了拿不到数据,玩家仍然可以点击按钮退出战斗录像。

录像缓存的目的则在于优化玩家重复观看录像的体验,减少流量消耗
。当看过一次录像,下载了完整的录像数据后,前台就会把录像保存到本地缓存起来了,尽管录像头里存储了部分战斗录像大厅的字段,比如点赞、收藏数等,这些字段数据会失效,但战斗数据是不会变的。查看大厅的录像列表时,后台会返回只有录像头BattleReplayFile,没有数据块BattleReplayFileBlock的列表,玩家请求观看时,判断本地缓存有没有该录像缓存,有就不再走原来的下载流程,直接读取缓存文件播放即可。


洋洋洒洒写了一些关于战斗录像的总结,也确实是因为录像系统对战斗开发调试有所帮助,作为一个功能系统,也需要在早期考虑一些问题,做设计和优化,希望本文能对MMORPG或其他类型游戏战斗的设计开发,提供一些借鉴经验。

附上我们的游戏官网[妖精的尾巴:魔导少年] <https://yw.qq.com/>,快来玩吧~

友情链接
ioDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信