Files
obsidian-notes/InBox/战斗/独立游戏-万字解析游戏战斗系统开发.md
2025-06-11 18:37:23 +08:00

418 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[独立游戏-万字解析游戏战斗系统开发 - 知乎](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<int, CfgBuffData> CfgBuff = new Dictionary<int, CfgBuffData>
{
[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做原型设计
### 日志系统
> 重点是日志的全面打点&显示统计和输出,后续更新