[独立游戏-万字解析游戏战斗系统开发 - 知乎](https://zhuanlan.zhihu.com/p/694146117) ## 背景 写这篇文章的背景来自于去年做的一款**[星穹铁道](https://zhida.zhihu.com/search?content_id=242384834&content_type=Article&match_order=1&q=%E6%98%9F%E7%A9%B9%E9%93%81%E9%81%93&zhida_source=entity)**的**[战斗模拟器](https://zhida.zhihu.com/search?content_id=242384834&content_type=Article&match_order=1&q=%E6%88%98%E6%96%97%E6%A8%A1%E6%8B%9F%E5%99%A8&zhida_source=entity)**: 【耗时3个月,自制星铁战斗系统!就为作出最强攻略!】 [耗时3个月,自制星铁战斗系统!就为作出最强攻略!_手机游戏热门视频](https://link.zhihu.com/?target=https%3A//www.bilibili.com/video/BV1Er4y1R7y4/%3Fshare_source%3Dcopy_web%26vd_source%3Da00e3a4a1a7552a159a18dddb0a14fb0)。 后来结合自身经验又将此战斗系统**扩展复用**到了**多种类型**的游戏Demo中。在此来简单总结分享下其中组成,也不期待能帮助到大家什么,仅当在如今游戏行业的氛围中聊以慰藉吧。 做这个模拟器的背景在视频里其实有交代的蛮清楚的,感兴趣的会看不感兴趣的也不用多介绍了~。来说一下实现了什么样的功能: 1. 完全模拟了**星穹铁道**([卡牌向](https://zhida.zhihu.com/search?content_id=242384834&content_type=Article&match_order=1&q=%E5%8D%A1%E7%89%8C%E5%90%91&zhida_source=entity))的所有核心战斗机制,技能体系、数值体系、能量、装备系统、行动出手逻辑等。 2. 截止xx前**所有角色**的**战斗逻辑**(和官方技能描述一致)。 3. 实现了随机/伪随机的**可重入**操作(也就是视频里经常提到的**万次模拟**)。 4. **伤害公式,数值公式**,甚至**buff被动**等时序以及打出来的伤害和官方角色实际体验**保持一致**。 5. 截止xx前完全拆解并实现了当前版本的所有特殊角色的特殊逻辑。 6. 加了点前端界面,有**战斗过程,出手演示,行动演示,数据LOG演示,调试**等界面。 **为什么要从卡牌讲起?** - 因为这是我个人经历中比较少见的给"**自己**""**商用**"的战斗体系搭建,是从**0开始搭建**并**成功落地**验证了**扩展性**的结构。因为平时就热爱游戏开发,这套战斗体系我同样移植给了自己做的**"音游""肉鸽""休闲游戏""割草游戏"**等多个Demo中。所以其实可以看到这并**不局限于游戏类型**,只要维护核心观念的扩展性即可(**配置驱动**)。 **概览下战斗系统里包含什么?** - **战斗核心机制**是 "**技能**" "**Buff**" "**子弹**" 体系的三者联动, 辅以"被动""装备""法球"等机制用来配合实现特定逻辑。 - **逻辑与表现分离**: 这是避免不同步的基础,也是后续万次重入结论一致的基础,在设计之初就应该考虑的。 - **[C#配置表工具](https://zhida.zhihu.com/search?content_id=242384834&content_type=Article&match_order=1&q=C%23%E9%85%8D%E7%BD%AE%E8%A1%A8%E5%B7%A5%E5%85%B7&zhida_source=entity):** 在工作中有做了一个把**excel**表导出成**c#数据表**的工具,生成c#之后可以让代码里直接调用对应的excel类的属性,所见即所得可以说非常好用了。 - **出手逻辑:** 也就是程序如何循环的。这里不同类型的游戏会定制化一些。 - **伤害机制:** 伤害公式确认好之后,重点就是用buff和属性、被动等共同组织起各种加成与时序算法。 - **人物属性:** 单独拿出来说是因为人物属性需要做到动静分离,实时变更。影响因素较多(装备、被动、buff等),做到高效组织是需要点功力在的。 - **触发机制:** 其实大部分就是**被动**带来的, **注册+触发**执行。 - **日志系统:** 包含及其丰富细致的日志系统,让所有数据有迹可循。 - **战斗表现:** demo做的粗糙,真正商业化游戏配合着**技能编辑器**来,根据时间轴做好战斗前摇、出手、后摇阶段的排版。 - **操作手感:** 呦,抽象了喔。这是我最擅长的部分,单独拿出来作为一个文章不为过,解决过王者上千个战斗bug,走A,寻敌,位移,指示器等等非常多,也感慨优化了大部分的英雄。后面得空在好好讲讲这里吧。 - **打击感:** 抛砖引玉: 技能缓存、飘字、震动、血条表现、顿帧、硬直等等。 - **音频:** 不是我关注的重点.但是玩家体验的关键一环。 - **网络:** 帧同步|状态同步。 - **Debug:** 快捷的调试工具也是节约开发时间的"利器"。 - **渲染表现:** 风火雷电冰,雨雪阴晴,草水等等. 不作为本文重点。 - **AI:** 本身AI&怪物AI - **UI:** 战斗UI,可能讲一下有趣的地方~ 比如3DUI, 资源后处理Event生成等 - **资源加载:** 和战斗关联比较大,顺便提一下 ## 战斗系统雏形构建思路(星铁为例) > 这是写给我自己做的独立游戏的,并不适用于所有项目/大厂,正式的战斗系统会更为复杂严谨,各位资深道友看见了觉得写的草率了就图一乐吧~。 **下面的内容组织思路?** 从星铁战斗系统的**整体构建**来总览回顾一步步还原**从0制作**一款卡牌**战斗雏形**的过程。 1. 首先**构建初始时序** 2. 讲一下**个人风格**的**战斗体系的组成**,重点是如何实现**高扩展性**。 3. 讲一下**战斗相关**的**系统扩展**,比如**战斗表现|日志系统|资源加载**等。 4. 将战斗系统**扩展**应用到**多类型**游戏中,验证扩展性和鲁棒性。 ## 构建初始时序 > 根据**游戏类型**(卡牌),做了下**出手执行顺序**的草图,这是游戏运转前的基础验证,后续的所有战斗逻辑皆以此为基础扩展。 ![](https://pica.zhimg.com/v2-d3ea5b40decde25f454c6bc76456898c_1440w.jpg) 出手执行顺序草图 可以看到星铁的核心出手机制定义为**行动值**,其实行动值就是速度,每个角色在程序中的运行速度的差异决定了**普攻出手**的先后。然后辅以每个角色特殊的**战技出手**条件&**能量出手**条件,形成了一个角色**全部的行动规则。** 下面提供**角色更新**伪代码(为**常规模拟模式**稍微做了些2D表现,所以使用了**协程wait.**): ```csharp // 通过协程分帧处理表现 public IEnumerator ExcuteInner() { // 前置需要添加Boss | 角色 | 日志系统 | 开局事件触发等 var maxRunCount = 全局配置表["行动值上限"]; while (curRunCount++ < maxRunCount) { // 更新人物 foreach (var actor in actorList) { actor.Update(); yield return new WaitForSeconds(0.5f); } // 更新怪物 or 对手等.. yield return new WaitForSeconds(0.01f); } } ``` **角色出手逻辑伪代码:** ```text public void Update() { // 判断死亡,死亡不会执行后续 if (IsDie()) return; // 更新角色携带的宠物 m_pet?.UpdatePet(); // 更新人物路程 ChangeWay(speed); // 检查大招释放规则(能量更新在角色出手外,可以做到差帧更新: 即大招能量条满了不会同帧抢技能执行) CheckPlayEnergySkill(); // 判断是否可以出手(当前行动路程大于预设) if (m_curWay >= GlobalConfig.S_SumWay) { // 回合开始事件等派发 EventCenter.Instance().Notify(EVENT_NAME.ROUND_BEGIN, this, arg); // 路程归零(这里可以加速度余量,没加大概是强度使然) SetWay(0); // 更新持续伤害 UpdateLastDamageBuffs(); // 判断是否存在冻结等行动停滞效果 if (IsFrozen()) { // 冻结之后路程变成5000后面且不会出手,但是可以触发buff } else { ActorFight(); } // 检查大招释放规则 CheckPlayEnergySkill(); // 更新Buff(挂在角色身上的正面负面都算) UpdateBuffs(); // 更新被动技能 UpdatePassiveSkills(); // 清理当前轮次的攻击对象等回合相关信息 // ClearRoundData(); } // 移除延迟buff列表(有些buff不会在当前更新即刻移除,要放在DelayRemoveBuff列表中延迟移除) // 移除被动(同理) // 角色更新完成 } ``` 补充**ActorFight**判断是否是战技出手即完成完整**角色出手**流程: ```text public void ActorFight() { // 敌人出手前更新韧性 // 出手前判断buff列表是否有回合开始时需要执行的buff for (int i = 0; i < m_buffList.Count; i++) { buffExcuteDic[m_buffList[i].m_buffType].RoundBeginExcute(m_buffList[i]); } // 判断技能出手条件是否满足 决定此刻是普攻/战技出手 if (!CanSkill()) { PlayAttack(); } else { PlaySkill(); } } ``` ## 构建角色属性系统 > 角色属性是伤害公式调用的基础,伤害公式就是根据配置动态修改各种属性的业务算法,而各种属性又决定了玩家的游戏策略,所以这个时候可以优先选择构建属性系统。 以星铁早期的人物属性举例,可以拆解为: **速度、阵营、角色类型、攻击力、增伤、伤害加成、暴击率、无视防御、防御、暴击加成、血量、等级、穿透、抗性、韧性、嘲讽值、最大能量值、攻击属性、弱点、抗性弱点**。 下面这个配置表也可以参考一二,为什么用的是**拼音**啊?**Up是我室友他学俄语的啊**!!!要蚌埠住了,实在没有勇气用俄语来做表头和代码注释,索性在部分属性上折个中用拼音了。 ![](https://pic2.zhimg.com/v2-47ba38d3d0e577981e9e18a18c833bd1_1440w.jpg) 后来才悔恨的加了一行中文注释.. 晚了 这里有3个点: - Q: 为什么是会出现5000这种数字? - A: 为什么是会出现5000这种数字?A: 为了保证精度,用万分比保留2位小数部分 - Q: 为什么是会出现大面积的0? - A: 0代表了角色身上的初始属性确实没有,但是可以支持后续带有特定属性的角色更新 - Q: 为什么逻辑上又出现中文? - A: 不用怕,针对有强迫症的程序来讲,是准备了一张**中文转换表**的,可以将中文**适配成对应的枚举**,但是给"策划"展示依然是中文方便阅读配置(不然你填个2谁知道是啥啊喂)。 至此,关于属性的基础创建就完成了,选择了对应的角色就可以直接使用属性系统了。 ## 构建装备系统 > 本来不想扩散的,不过上一章节配置表中有很多的0,看不习惯,那就增加一个章节讲一下属性系统的实际数值是如何填充的(其中一种方式)-**"装备系统的构建".** 这一节很短,快的离谱 Q: 装备很多,属性很杂,还有装备各种特殊效果,套装加成等,关于加成的配置和装备的代码一定很麻烦吧? A: 核心代码几十行代码就搞定.只需要 "**通用的属性加成**" 与 "**被动添加移除机制**",下面提供伪代码 ```text // 获取装备属性 public int GetEquipAdd(PropertyType _property) { // 自行判空 异常处理 return m_configData[propertyType]; } // 添加被动 public void AddPassiveSkill() { // 自行判空 异常处理 var passiveID = m_configData["PassiveID"]; foreach (var p in passives) { if (p > 0) { // 参数分别代表: 1.给谁上被动 2.谁上的被动 3.被动的id UTils.AddPassive(m_srcActor, m_srcActor, p); } } } // 移除被动 public void Remove() { // 自行判空 异常处理 var passiveID = m_configData["PassiveID"]; foreach (var p in passives) { if (p <= 0) { continue; } if (m_srcActor.ContainsPassive(p)) { m_srcActor.RemovePassive(p); } } } ``` ![](https://pic2.zhimg.com/v2-3a1a7f91afaaa7f8a83f009bec94f2af_1440w.jpg) 装备的配置加成属性其实可玩项【非常丰富】 ok,至此已经讲完属性系统与装备填充的部分,接下来来关注比较核心的伤害公式部分(每个游戏此部分应不尽相同,特别是成长类型游戏,是数值策划保证游戏生命周期的必修课) ## 根据伤害公式构建伤害系统 > 来拆解一下伤害公式(早期的部分伤害公式中文描述,后续我们是有微调迭代的~,每一种伤害公式我们后期都和原版游戏运行进行了大量对比验证): 关于**伤害公式**的选择有非常多种,这里因为复刻的星铁就以**星铁的公式为引子**介绍,其余公式可以根据情况自行推导. 星铁的伤害公式是**乘法公式** DMG=a*ATK*F(targetDEF) , 这种公式可以形容为“**折损伤害**”,比较适合ATK与DEF的成长空间无限大(无限成长扩展那种),通过构建F(DEF),可以得到边界递减的收益效果: ![](https://pic1.zhimg.com/v2-99a8569c4a5d65b42827186083fe1b76_1440w.jpg) 示意 简单知道了公式原理后,我们来拆解一下星铁存在哪些伤害公式(十多种,不截全了**,早期Demo期间推导**的战斗公式需求表,内容在后续实战测试验证后有调整) ![](https://pic3.zhimg.com/v2-e52b95ecdb85e81f42f597b649a590a2_1440w.jpg) 星铁伤害公式在早期模拟系统Demo期间的拆解示意图 这里大家可以看到,正式游戏中的伤害公式不止一种,为了增加游戏的复杂多变性,往往就需要增加各种打破常规的计算方式来增加伤害的数值乐趣。这点如果做独立游戏的话根据平衡性自己设计吧,或者就完全拆解已经被验证过数值曲线的数值公式。 接下来来看战斗机制部分. ## 战斗体系的核心组成 > 核心为 技能 buff 子弹的组织循环, 再加入"被动""印记""法球"等效果, 基本可以实现"所有"想象的到的战斗技能效果. ![](https://picx.zhimg.com/v2-9f039a6b2456fd3739245c9572243901_1440w.jpg) 技能系统简易示意图 ### 如何实现**高扩展性?** > 这套体系的优势就在高扩展性,将逻辑原子化提供节点给"策划"调用,扩展参数以达到强大的复用效果。 在B站的评论区经常有人会问: **米哈游再出一个英雄,**你是不是要**重新全部实现**一次英雄的技能呢? 如果是这样,累也怕是累死了,星铁战斗团队十数人一个版本的内容我**完全手敲代码复刻要死**的。 如何做到的? 两点: 1. [技能配置文件](https://zhida.zhihu.com/search?content_id=242384834&content_type=Article&match_order=1&q=%E6%8A%80%E8%83%BD%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6&zhida_source=entity) 2. 优秀的抽象逻辑节点 ### 技能配置文件 这部分既是指技能编辑器产出的技能组成,也同样指我们技能表中的配置。那为什么会存在两份结构不一样的配置表呢? 一个比较直观的解释: **技能配置表**更像是数据库.你可以随时**读取数据**. 技能配置文件则是组织技能出手后的复杂执行逻辑&效果的配置文件。他们的核心都为**"[数据驱动](https://zhida.zhihu.com/search?content_id=242384834&content_type=Article&match_order=1&q=%E6%95%B0%E6%8D%AE%E9%A9%B1%E5%8A%A8&zhida_source=entity)",**所以也可以说: 只要做到了足够丰富抽象的节点设计,使用**"数据驱动"**就可以让策划自己编排所有后续的战斗需求了。 ![](https://pic1.zhimg.com/v2-39e442532f60a87e0455ae8e9113cb2e_1440w.jpg) ### 技能配置表 > 技能配置表单拿出来是因为大家基本都会使用的到,一般也会转成二进制数据而存在.这里我想为小团队/独立游戏制作者推荐一个个人制作的c#转表工具。 > 实现的原理很简单就不讲了,有需要的我可以把工具贴到Git上。 使用结果上可以将配置表的数据转成**c#类**中清晰可见的**数据结构**并且有相应**数据填充**,实现了c#类的配置表信息存储。适用于小团队是因为其**足够的方便**,导出后可以在业务侧直接使用对应配置表类的数据。并且点击类名可以**跳转**到对应类中**查看所有有效数据**,**减少查看原始Excel**的繁琐操作,更改**临时数据**更是可以做到**1s搞定**。 下面是c#配置表的示例(原始数据为excel表,为了避免不必要的麻烦使用的是自己的Demo游戏配置数据): ![](https://pic4.zhimg.com/v2-121ae2f4004eee63292ffe8f77cf486f_1440w.jpg) 技能配置表相关数据 ```text public partial class NCONFIG_CSHAP { public static Dictionary CfgBuff = new Dictionary { [100101] = new CfgBuffData { ID = 100101, Name = "通用伤害", Desc = "测试", SkillSrc = "XX技能", LastTime = 1f, BuffType = "通用伤害", Param1 = 0, Param2 = 0, Param3 = 0, PerCentParam = 10000, target = "对方单体", BuffTypeAddBuff = 0, LastBuff = false, DieJia = "", MaxCount = 1, DelayBuff = false, TriggerCD = 0, EndAddBuffs = new int[] { }, }, [100102] = new CfgBuffData { ID = 100102, Name = "普通攻击伤害", Desc = "测试", SkillSrc = "XX技能", LastTime = 1f, BuffType = "通用伤害", Param1 = 0, Param2 = 0, Param3 = 0, PerCentParam = 30000, target = "对方单体", BuffTypeAddBuff = 0, LastBuff = false, DieJia = "", MaxCount = 1, DelayBuff = false, TriggerCD = 0, EndAddBuffs = new int[] { }, }, } } ``` ### 抽象逻辑节点 > 当建立了一定体量的逻辑功能节点后,"策划"就可以通过简单改变配置来组合达到不同的技能效果了。 在讲之前,可以先分析以下两个技能的区别: 1.释放技能,对随机敌人造成x点伤害,并叠加一层火焰伤害,每回合每层火焰伤害造成x点伤害,最高上限5层。 2.释放技能,对选中敌人造成五段冰霜伤害,最后一段伤害必定造成暴击。 这两个技能看起来风马牛不相及,实际上同属【**伤害**】这个概念中.我们可以从技能描述中抽离出**"随机/选中敌人""火焰/冰霜""单回合/多回合""1段/5段伤害""暴击"**这些差异点。 那我们**只需要构建一个**通用的**伤害Buff**,在执行的过程中根据以上**五种差异元素**做配置判断即可,根据配置: - 将**选敌逻辑**抽象出来: 可以选择单个、多个、随机的**敌人.**可以选择血量最低、伤害最高、距离最近的**友军**。 - 将**元素逻辑**抽象出来: 可以配置冰、火、暗、光属性等,并单独结算属性伤害&韧性计算(关于破韧等逻辑使用条件触发即可)。 - 将**回合逻辑**抽象出来: 控制生命周期,根据配置单回合生效即销毁还是执行多回合。 - 将**叠加逻辑**抽象出来: 分段伤害可以分为几种: - 如果是纯伤害叠加可以使用**buff叠层** - 如果要分开结算(比如暴击率独立判定),则配置多个伤害buff,或者buff执行衔接buff等方式都可以 - 将暴击**逻辑**抽象出来: 配置可以无极调整的暴击率参数就Ok. 用来简化的表达如下, 技能产生buff, Buff通过配置表参数传递特殊数据,而具体的执行逻辑节点是注册机制,使用Map索引即可。 ![](https://picx.zhimg.com/v2-8613fcbae63468b0a769a93132238771_1440w.jpg) Buff执行节点示意 ![](https://pic4.zhimg.com/v2-1e3626998d48a4a702a0e549c4c3d7a3_1440w.jpg) 通用Buff逻辑执行节点注册示意 具体逻辑节点要做的事情千变万化了,也是高扩展性的核心竞争力 。这里就不展开讲了,注意配置的扩展就好,接下来来聊一下战斗相关的系统扩展。 ## **战斗相关**的**系统扩展** > **战斗相关**的**系统扩展**,涉及到星铁的比如**战斗表现|日志系统**等, 涉及到常规游戏的,如音频管理,资源加载等,不作为重点,**后续更新.** ### 战斗表现 > 战斗表现因为是卡牌又是Demo,实际上没什么好讲的,这个章节会比较短,后面会补上图或者视频.(其实分享链接里面也有,就是个演示界面。 从设计开始, 使用xx做原型设计 ### 日志系统 > 重点是日志的全面打点&显示统计和输出,后续更新