游戏开发手记:战斗模块设计

对战是咱们游戏中的核心功能,能够说是全部模块中的重中之重。这周准备着手进入开发了,今天仔细分析了下需求,制定出来了大致方案。
特别说明一下,本文所描述的战斗模块设计方案源自于实际项目(卡牌手游)的需求,可能并不适用于MMORPG、ARPG等类型游戏。本文所涉及战斗的基本形态是:从游戏环境中收集战斗所须要的数据,随后在一个独立封闭的环境中进行若干次迭代计算(其间可能会读入玩家的指令输入),最后达成某些条件后分出胜负战斗结束。若是感受这个描述太抽象,能够参考一下三国志系列的战斗或者QQ斗地主:P
本文重点分析战斗系统在模块这个级别上的设计和取舍,不涉及具体游戏的战斗计算逻辑。程序员

需求分析

项目战斗采用的是自动回合制战斗,即势力双方派出若干角色按照特定的队形排好后依次进行攻击,直到其中一方全部角色都阵亡则战斗过程结束。为了提升战斗的趣味性和策略性,咱们加入了手动释放技能做为补充,即玩家可主动选择释放技能的时机(参考刀塔传奇)。
从程序的角度来看,若是战斗过程全自动不须要玩家干预,那么能够用“秒算”的方式来作。也就是直接一个循环瞬间计算出完整的战斗过程,战斗过程保存下来后再慢慢播放。一旦引入玩家的手动操做,秒算就再也不凑效了,由于战斗过程的计算会受实际操做的影响。
按照策划的设计,目前游戏中战斗的类型能够分为3种。副本(PvE):由玩家势力和配置的NPC势力对战,玩家能够操做技能释放;排位赛(PvP):由两方玩家事先派出的势力对战,由于玩家不必定在线,因此技能是自动释放的,再也不支持手动操做;挑战赛(PvP):当两方玩家都在线时发起对战,这时双方均可以操做技能释放。
其中1)副本战斗要求在网络链接不稳定时也能正常进行,须要由客户端进行战斗计算,等到结束后再将结果上传到服务器验证。2)排位赛存在玩家不在线的可能,因此是服务器进行“秒算”,并保存战斗录像供以后播放。3)挑战赛在服务器计算,在战斗过程当中客户端须要同步操做至服务器。
对于第1点需求我是存在异议的,为了网络不稳定状况下的体验,致使游戏中最复杂的战斗部分须要服务器和客户端各实现一遍,工做量增长不说,最重要的模块的复杂程度急剧上升,须要同时支持三种模式:a)客户端计算,服务器验证 b)服务器秒算,客户端播放 c)服务器计算,客户端播放过程和读取操做,并实时同步。此外战斗逻辑自己是很是复杂的,几乎必定会出bug而且不容易测试、发现bug也不容易重现、重现了也不容易调试和修正,服务器和客户端还要各自实现一遍致使这一系列成本成倍增长。(项目中服务器和客户端编程语言不一致没法共用代码)
战斗模块的设计,重点就在于为这两个棘手的问题提供一个解决方案。编程

从外部看战斗模块

从外部看战斗模块,就是规划模块的外部接口,划定模块与外部系统的界线。
用极简的视角来看战斗模块,能够认为它就是一个函数,输入是战斗须要的全部数据(包括双方势力的战斗单位布局,每一个战斗单位的出手速度、攻防血、技能等属性),输出是战斗结果(胜负状况,可能还包括战斗过程记录)。
战斗模块不关心的是:战斗从哪里触发;战斗结束后要更新哪些数据;战斗势力是玩家仍是NPC;战斗可否发生,如玩家是否有足够的体力,玩家等级是否知足副本等级,是否领取了对应的任务等。
战斗模块关心的是:战斗过程的迭代,战斗结果的断定,战斗过程当中数据的网络同步(若是须要),战斗过程的展现(客户端)。
特别注意战斗做为一个独立模块不该该有任何外部依赖。例如某单位的攻击力是由配表中的数值加上其等级进行计算,再综合各类加成得出的,那么应当是在战斗模块外部算出最终数值后再交给战斗模块,而不该该由战斗模块去调用外部接口进行计算。这是很天然的,由于咱们必定不想因为某张配表变化或者某模块数据结构的调整致使须要修改战斗模块的代码,最后引入bug带来没必要要的麻烦。服务器

从内部看战斗模块

从内部看战斗模块,也就是制定模块的实现方法,重点是要同时支持3种战斗模式。
思考问题的过程其实特别快,有时候想法的产生来自于直觉没有什么特别的理由,因此这里只介绍最后想出来的方案……
战斗模块从内部划分为这么几个组件:数据,计算,展现(仅客户端),输入。
数据部分是从模块外部传入的,一部分数据会交给计算模块进行迭代(战斗角色的排布和属性等),一部分数据会交给展现组件用于界面展现(双方名字等级等)。
计算组件负责战斗迭代,它的输出是一系列战斗过程。
展现组件接收战斗过程,并在场景中展现出对应的模型、动画、UI。
输入组件负责读取用户输入。
接下来咱们看看这些组件如何组合起来知足3种战斗模式。
1)副本:副本战斗彻底在客户端运行,计算组件输出的战斗过程发往展现组件,输入组件读取到的操做发往计算组件影响后续迭代。
2)排位赛:排位赛由服务器秒算,服务器直接计算出全部战斗过程后返回给模块外部存储下来。客户端查看战斗记录时,计算好的战斗过程以数据的形式发往客户端战斗模块,此时客户端的计算组件退化为“播放器”,只须要将服务器生成的战斗过程依次发往展现组件。
3)竞技场:竞技场模式操做和战斗过程都是经过网络实时同步的。服务器这边:输入由客户端经过网络发送过来,计算出的战斗过程经过网络发往客户端的展现组件。客户端这边:展现组件从网络接收战斗过程,输入组件读取到输入后发往服务器。网络

战斗过程的记录方式

战斗过程同时用于组件间通讯和战斗录像的保存,其数据结构有必要探讨一下。
首先记录方式应该是基于“打谱”而不是“快照”。二者的区别是这样的,打谱相似于“回合1 A攻击B产生10点伤害,回合2 B攻击A产生5点伤害”,而快照相似于“回合1 A 100生命 B 90生命,回合2 A 95生命 B90生命”。基于打谱的缘由有二:其一是一般打谱产生数据量要远小于快照,不妨想一下象棋的棋谱,几十分钟的一局对弈用棋谱记下来不过半页纸;其二是基于事件的记录形式更便于展现组件展示过程。
可是打谱的记录方式很容易夹带一些隐晦的问题,究其缘由战斗播放是依据“谱”的一个复现过程,播放到任意时刻的状态是由前面一系列的步骤推演出来的,只要有一步出现误差最后的结果就可能截然不同。为了保证一致,咱们的战斗过程记录必定要足够直接。例如受到的伤害值减掉防护值得出HP的损耗,就必须直接记录HP的损耗,而不能只记录伤害值交给播放组件来进行计算。更好的作法是将打谱法和快照法结合着使用,同时记录HP的损耗和最后剩余的HP,这样即便不慎出现了不一致也能很快恢复。
可能有同窗不理解,损耗=伤害-防护,如此一个简单的计算怎么会发生不一致呢?可能还有同窗会以为自动战斗根本不必记录战斗过程,由于没有操做的影响,直接拿初始数据从新推演一下不就出来了?这是新人常见的思惟漏洞,他们忽略了一个重要因素,就是线上网络游戏是在不断演化的。在今天损耗=伤害-防护,下个版本可能就变成损耗=攻击-防护,录像数据仍是原来的数据,因而版本一更新战斗录像的过程就全变了,这可就太坑爹了。网络游戏迭代更新很快,必定要时刻提防着数据兼容的问题。数据结构

测试的困境

前面还提到了另一个棘手的问题,复杂的战斗逻辑被服务器和客户端各实现一遍,给测试带来了不小的压力。其实换个思惟方式就能很巧妙的解决,一旦想通后甚至有一种“因祸得福,焉知非福”的感受!
首先要注意不论是服务器仍是客户端,战斗计算组件的输出都是同样的:一份完整的战斗过程记录。那么若是两边都正确实现功能的话,给这两个计算组件以相同的输入,则必定能够获得相同的输出(这里不考虑计算过程当中的随机数,或者能够认为随机数种子看成参数传入);反之若是对于相同的输入获得了不一样的输出,那就说明至少有一方的实现是有问题的。
基于这个思路,咱们能够把两边的战斗模块单提出来独立编译(由于战斗模块的实现不依赖于其余模块,单提出来是很容易作到的),再写个测试程序不断随机生成战斗初始数据分别发往两份实现,收回两边的输出后进行校对,这样容易就能发现bug了。等到两份实现能一致地处理大量随机数据时,咱们基本上就能够认为两份实现都是正确的了。毕竟两个程序员分别使用不一样的编程语言,很巧合地设计出了类似的代码结构,并更加巧合地犯了同一个错误,这个几率应该是足够小的。编程语言

一点题外话:这个系列已经写了陆续写了好几篇了,从一些评论能够看出,文中会出现一些描述不清或详略不当的状况。原先设想是根据评论反馈状况不断改进优化正文的,后来以为这样有点费精力,并且读者有疑问仍是须要以回复的形式解答的,总不能说我改了下正文你再去重读一下吧……我如今的想法是在评论里进行沟通,而后按期把比较有参考意义的评论附加在正文后面。因此若是有疑问或者意见或者想法就写在评论里哈~函数

相关文章
相关标签/搜索