[独立游戏-万字解析游戏战斗系统开发 - 知乎](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做原型设计

### 日志系统

> 重点是日志的全面打点&显示统计和输出,后续更新