string(7723) "{"docs":[{"id":"158579","text":"\u3010Python\u3011Tkinter\u56fe\u5f62\u754c\u9762\u8bbe\u8ba1\uff08GUI\uff09","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"HGNET","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183196","_id":"158579"},{"id":"158620","text":"python\u4e4bgui-tkinter\u53ef\u89c6\u5316\u7f16\u8f91\u754c\u9762 \u81ea\u52a8\u751f\u6210\u4ee3\u7801","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"darkspr","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183190","_id":"158620"},{"id":"158603","text":"python3.6 +tkinter GUI\u7f16\u7a0b \u5b9e\u73b0\u754c\u9762\u5316\u7684\u6587\u672c\u5904\u7406\u5de5\u5177","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"chenyuebai","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183187","_id":"158603"},{"id":"27850","text":"Python GUI\u4e4btkinter\u7a97\u53e3\u89c6\u7a97\u6559\u7a0b\u5927\u96c6\u5408\uff08\u770b\u8fd9\u7bc7\u5c31\u591f\u4e86\uff09 - \u6d2a\u536b","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"shwee","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183186","_id":"27850"},{"id":"158605","text":"Python GUI\u7f16\u7a0b(Tkinter) windows\u754c\u9762\u5f00\u53d1","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"itfat","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183184","_id":"158605"},{"id":"28228","text":"tkinter python\uff08\u56fe\u5f62\u5f00\u53d1\u754c\u9762\uff09","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"yudanqu","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183159","_id":"28228"},{"id":"158613","text":"Tkinter\u56fe\u5f62\u754c\u9762\u8bbe\u8ba1\uff08GUI\uff09","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"pywjh","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183158","_id":"158613"},{"id":"341361","text":"\u91cf\u5316\u5206\u6790\u83b7\u53d6\u6570\u636e\u76843\u79cd\u59ff\u52bf\uff08\u538b\u7bb1\u5e95\u7684\u795e\u5668Tushare\uff09","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"casual","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183069","_id":"341361"},{"id":"238879","text":"\u9762\u5411\u4ea4\u6613\u7684\u65e5\u5185\u9ad8\u9891\u91cf\u5316\u4ea4\u6613\u5e73\u53f0\u7b14\u8bb0","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"TaiYangXiManYouZhe","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183067","_id":"238879"},{"id":"238890","text":"2021 \u6700\u65b0\u91cf\u5316\u6295\u8d44\u4ea4\u6613\u8d44\u6e90\u6c47\u603b","intro":"\u76ee\u5f55\n\nECharts\n\u5f02\u6b65\u52a0\u8f7d\n\n\n\nECharts\r\n\u6570\u636e\u53ef\u89c6\u5316\u5728\u8fc7\u53bb\u51e0\u5e74\u4e2d\u53d6\u5f97\u4e86\u5de8\u5927\u8fdb\u5c55\u3002\u5f00\u53d1\u4eba\u5458\u5bf9\u53ef\u89c6\u5316\u4ea7\u54c1\u7684\u671f\u671b\u4e0d\u518d\u662f\u7b80\u5355\u7684\u56fe\u8868\u521b\u5efa\u5de5\u5177\uff0c\u800c\u662f\u5728\u4ea4\u4e92\u3001\u6027\u80fd\u3001\u6570\u636e\u5904\u7406\u7b49\u65b9\u9762\u6709\u66f4\u9ad8\u7684\u8981\u6c42\u3002\r\nchart.setOption({\r\n color: [\r\n ","username":"xgqfrms","tagsname":"","tagsid":"","catesname":"","catesid":"","createtime":"1641183063","_id":"238890"}],"count":535118}" array(2) { ["docs"]=> array(10) { [0]=> array(10) { ["id"]=> string(6) "158579" ["text"]=> string(46) "【Python】Tkinter图形界面设计(GUI)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(5) "HGNET" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183196" ["_id"]=> string(6) "158579" } [1]=> array(10) { ["id"]=> string(6) "158620" ["text"]=> string(60) "python之gui-tkinter可视化编辑界面 自动生成代码" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "darkspr" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183190" ["_id"]=> string(6) "158620" } [2]=> array(10) { ["id"]=> string(6) "158603" ["text"]=> string(66) "python3.6 +tkinter GUI编程 实现界面化的文本处理工具" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(10) "chenyuebai" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183187" ["_id"]=> string(6) "158603" } [3]=> array(10) { ["id"]=> string(5) "27850" ["text"]=> string(80) "Python GUI之tkinter窗口视窗教程大集合(看这篇就够了) - 洪卫" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(5) "shwee" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183186" ["_id"]=> string(5) "27850" } [4]=> array(10) { ["id"]=> string(6) "158605" ["text"]=> string(45) "Python GUI编程(Tkinter) windows界面开发" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(5) "itfat" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183184" ["_id"]=> string(6) "158605" } [5]=> array(10) { ["id"]=> string(5) "28228" ["text"]=> string(39) "tkinter python(图形开发界面)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "yudanqu" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183159" ["_id"]=> string(5) "28228" } [6]=> array(10) { ["id"]=> string(6) "158613" ["text"]=> string(34) "Tkinter图形界面设计(GUI)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(5) "pywjh" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183158" ["_id"]=> string(6) "158613" } [7]=> array(10) { ["id"]=> string(6) "341361" ["text"]=> string(68) "量化分析获取数据的3种姿势(压箱底的神器Tushare)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(6) "casual" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183069" ["_id"]=> string(6) "341361" } [8]=> array(10) { ["id"]=> string(6) "238879" ["text"]=> string(51) "面向交易的日内高频量化交易平台笔记" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(18) "TaiYangXiManYouZhe" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183067" ["_id"]=> string(6) "238879" } [9]=> array(10) { ["id"]=> string(6) "238890" ["text"]=> string(41) "2021 最新量化投资交易资源汇总" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "xgqfrms" ["tagsname"]=> string(0) "" ["tagsid"]=> string(0) "" ["catesname"]=> string(0) "" ["catesid"]=> string(0) "" ["createtime"]=> string(10) "1641183063" ["_id"]=> string(6) "238890" } } ["count"]=> int(535118) } 记录战斗记录你,详解妖尾战斗录像系统 - 爱码网
leoin2012

妖尾历经几年开发,终于在今年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是核心类,它对外接收录像相关的控制请求,对内调度其他核心模块类,获取/保存/构造数据,控制录像播放流程,并通过给战斗网络层发送协议数据影响战斗表现。

服务器录像

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

客户端录像

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

  1. 确定录像数据结构,用什么数据结构存储一场战斗的所有协议及相关信息较优?
  2. 保证录制数据完整性。网络抖动、切出游戏再切回来等场景可能会导致少了某回合表演数据怎么办?
  3. 如何实现跳回合。一场正常战斗的协议包,除了初始战场包,每个回合只有表演包,没有战场包,跳回合怎么恢复战场状态?
  4. 录像上传/下载的传输策略。协议收发有64kb限制,录像文件大小超过了怎么办?
  5. 保证用户体验。评估极限情况的录像文件大小,保证流畅的录像观看体验。

模块开发初期就考虑这些问题,就可以避免基础设计出错,后期积重难返的尴尬情况。

1. 录像文件结构

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

  1. pb传输效率高,而且开发熟悉pb,不像自定义格式还有理解成本,开发效率也高。
  2. 协议与录像文件采用同种格式,比较容易根据查看列表,上传/下载录像等业务去反推最优的录像文件数据结构。让每份录像文件既可以有战斗录像数据,也有关于录像大厅的业务数据,一次设计,解决两个问题。
  3. 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或其他类型游戏战斗的设计开发,提供一些借鉴经验。

附上我们的游戏官网[妖精的尾巴:魔导少年],快来玩吧~

分类:

技术点:

相关文章:

  • 2021-08-30
  • 2021-09-10
  • 2021-12-28
  • 2021-10-15
  • 2021-09-22
  • 2021-06-21
  • 2021-06-22
  • 2021-10-19
猜你喜欢
  • 2019-11-12
  • 2021-09-21
  • 2019-04-01
  • 2021-10-12
  • 2021-08-13
  • 2021-12-28
  • 2021-10-19
相关资源
相似解决方案