 战斗系统，在大型游戏中，基本就是最复杂的系统；在项目中，通常由专门的战斗策划和战斗程序负责。相信读者中也有维护过战斗系统的，我想问几个问题：

1. 有站在技能和Buff的更高层审视过你们的战斗系统吗？
    
2. 项目的战斗系统有明确的架构吗？
    
3. 项目的战斗系统有缺陷吗？知道问题的成因吗？
    
4. 能实现你玩过的游戏中的所有技能和Buff效果吗？
    

    我不知道大家的回答如何，但如果让刚离开第一个项目时候的我来回答这些问题，我几乎需要全部答NO...

    我后来发现，其实不仅我是这样，有大量的开发者都对战斗系统缺少系统性的认知和思考，即使当前项目的战斗框架是他实现的。为什么呢？因为他不是最初的设计者，而只是搬运工，他并不真正了解他手头的工具。

    其实，对战斗系统有系统性的认知非常重要，有许多设计问题，在我们只盯着部分模块的时候是发现不了的 —— 你只会觉得很难受，但不知道为什么。

    本篇就带着大家看一下常见的战斗框架设计，以及我的心血之作，相信你定有所收获。

---

1.独立抽象（反面教材）

![图片](https://mmbiz.qpic.cn/mmbiz_png/c2vcRRONwG3h0wDrWt6I5vxd8o4k17icEXNiceUGBCAAj5PldYlMMdMricbH4dVwsecl62LUzP17YmMib4fhd68IgA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)

    我的第一和第二个项目，都将主动技能、被动技能、Buff进行了独立的抽象。

    第一个项目的战斗系统是由我Leader搭建的，一开始仅包含主动技能和Buff两个模块；后来，在我接手战斗系统的维护工作后，策划提出了被动技能的需求，我便增加了被动技能模块。

    第二个项目的战斗系统是谁搭建的，我并不知道，但由我的同事（旸仔）维护；我记得一开始项目里也只有技能和Buff模块，后来，旸仔在处理被动技能需求时，也增加了被动技能模块 —— 果然是同龄人。。。

项目共性

    如果看细节的话，这两个项目的代码差异是非常大的；但从整体（框架）上看，两个项目的内核其实是相同的。

    首先，从整体上看，角色都可以分为5大块：

1. 属性：属性即角色身上的数据，包括：攻防血、坐标朝向、模型、等级、名字...
    
2. 主状态机：用于管理角色的的主要状态切换，通常与角色的模型控制相关
    
3. 主动技能：通常由玩家操作施展，且通常会控制角色的模型动作（Action）
    
4. 被动技能：通过养成系统或装备获得的效果，获得后立即生效，无时间限制
    
5. Buff：角色身上的增益或减益状态，通常是临时性的
    

    其次，主动技能都是基于FSM + Action的框架实现的，FSM管理技能的主流程，Action实现具体的效果；至于Buff和被动技能，也都是采用类似Action的方式实现的，只是接口抽象上稍有不同。

```
// 技能执行上下文
```

Q：为什么坐标朝向和模型被标粗体？

A：坐标、朝向、模型（Model）及其动作（Action）与受击包围盒和攻击包围盒的计算相关，非常重要。

主状态机

    其实，这两个项目的服务端都没有显式定义角色主状态机，而是根据角色的属性（移动状态、死亡标识、Buff等）来处理的互斥，我觉得是一个失误。

    为什么应该定义主状态机？

    从逻辑层的角度讲，将行走、施法、死亡、冰冻等纳入同一个状态机，可以让角色的主要状态及其互斥逻辑更为清晰。

    从表现层的角度讲，角色模型一次只能播放一个动作，因此也需要一套状态机来管理；而要想表现层的状态机更容易编写，逻辑层涉及到模型控制相关的逻辑最好也在同一套状态机中。

![图片](https://mmbiz.qpic.cn/mmbiz_png/c2vcRRONwG3h0wDrWt6I5vxd8o4k17icEsbBCl6g9wZF449ico4Ob48w2Lx4jIAupnGibic28CWZSabvicyWnhjZIng/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)

主要缺陷（重复感）

    在我实现需求和维护的过程中，我感受到的最大缺陷是：重复感。

    被动技能和Buff效果存在交集，被动和Buff的Action代码存在一定的重复。这种重复感一直困扰着我，我分不清这是真实的重复，还是表面的重复 —— 既想合并它们，又觉得它们不该合并。

    此外，我明确的感知到，项目的战斗框架，无法支持DNF那般强大的战斗系统 —— 距离还很远。

PS：其实主动技能的效果和Buff、被动也会有一些重复，但重复率不高；而且由于主动技能流程的特殊性，我们总是认为主动技能就应该是独立的。

  

核心问题是什么？

    开发者站在了用户的角度来设计架构。

    我们在玩游戏的过程中，建立了主动技能、被动技能和buff概念；在实现需求时，便不假思索地设计了对应的抽象。可我们所谓的主动技能、被动技能、Buff之间的差异，真的存在吗？它是表面的还是真实的？

    这样去做战斗系统的人 —— 比如我，大概率是没有思考过这个问题的。

---

2.被动是特殊的Buff

![图片](https://mmbiz.qpic.cn/mmbiz_png/c2vcRRONwG3h0wDrWt6I5vxd8o4k17icEKKVExdk4icyL4azCd4QiaPPVYPCiaibP2ia78jxxLpk9Krib86Ppia3zBBFIg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)

    虽然被动和Buff具有较大的差异，但仔细思考可以发现：被动技能可以是Buff的子集，被动技能的那些特性，Buff也是可以拥有的；而且用Buff支持被动技能，并不需要太大的改动量 —— 主要处理对站街面板的加成等。

    将被动技能实现为Buff后，不仅消除了被动技能和Buff之间的重复感，也扩展了Buff系统。

    该架构比较符合大家认知，采用率较高。不过，采用该架构的项目并不全是从前一种架构演进来的，也有一开始就用Buff实现被动技能的 —— 这是明智的；如果是从前一种架构演进来的，想必已有一点体会：过于明确的抽象，对战斗系统是有害的。

    PS：关于抽象带来的扩展性障碍，阅读《[角色框架：GameObject类型实现](https://mp.weixin.qq.com/s?__biz=Mzk0MjcwNTY4MA==&mid=2247484167&idx=1&sn=08876da72c7042b8031d96b815ad4e20&scene=21#wechat_redirect)》可能会有所帮助。

---

3.被动技能后台施展

![图片](https://mmbiz.qpic.cn/mmbiz_png/c2vcRRONwG3h0wDrWt6I5vxd8o4k17icEprofn1P2L43BLWpkyDI0LegPZQaaXBeetIAtCOpRSmYjU5NsyAmuhQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)

    被动技能和Buff合并的最大障碍是数据依赖不同，被动技能效果依赖的是SkillData，而Buff效果依赖的是BuffData。

    既然被动技能也依赖SkillData，那么是不是可以让被动技能也走主动技能的流程呢？即被动技能挂载后，按主动技能流程自动施展，但不占用角色主状态机。

```
public class SkillComponent {
```

PS：当年在第一个项目的时候，我就没想到这么做 —— 还是能力不足。

    将被动和主动一视同仁，意味着支持多技能同时施展，有以下优点：

1. 支持被动技能转主动技能施展，被动技能在满足条件的情况下可以手动施展
    
2. 支持主动技能间断性施法，eg：技能A第一击，技能B第一击，技能A第二击...
    

后台技能 VS Buff

    将被动技能看做主动技能，更符合代码层抽象 —— 程序员们会更容易理解。

    但将被动技能看做Buff，更符合业务层的抽象 —— 策划们会更容易理解；此外，用Buff实现被动，还避免了效果交叠带来的重复代码。

思考题

    被动技能和主动技能有共同点，又和Buff有共同点，那是不是说：主动技能和Buff有共同点？

---

4.一切皆Buff

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

    在市面上，还存在一种架构：一切皆Buff。

    从字面上看，我的理解是：主动技能在施展的时候，也是给自己添加一个Buff，然后通过Buff来实现所有的功能。

    我相信，肯定有读者在看见这句话的时候心生抵触！被动技能可以说是Buff，但主动技能怎么能是Buff呢？

    是啊！不仅仅是读者觉得主动技能和Buff不一样，俺也一样，甚至那些说着“一切皆Buff”的团队也认为不一样！

    所以，他们最终的实现并不是我理解的那样。根据职责转移的粒度，分两种情况：

1. 技能流程由Buff构成
    
2. 技能效果皆是Buff
    

---

4.1 技能流程由Buff构成

![图片](https://mmbiz.qpic.cn/mmbiz_png/c2vcRRONwG3h0wDrWt6I5vxd8o4k17icEsvyk3f185a81MZNISwwj8w9ZlzIawlNHIfic5pGSnAiaZbTQGFBrjeQg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)

    在技能设计中，技能被分为多个阶段，每个阶段由一组SkillAction构成；SkillAction中包含了打击点、触发间隔、攻击包围盒等信息；而在Buff中，同样存在这些配置。

    因此可以将技能的SkillAction看做Buff，这样技能便可以看做是Buff的集合，在特定的阶段挂载特定的Buff即可。

```
// 技能执行上下文
```

主要缺陷（交互）

    并非所有的技能都是简单的时间线施法，因此技能框架仍然需要是FSM式的；而由于SkillAction的消失，Skill成为空壳，因此Buff需要负责技能的阶段切换；所以，Buff系统和技能系统存在复杂的交互逻辑。

    此外，将SkillAction委托给Buff，概念上接受起来还是有难度。

4.2 技能效果皆是Buff

    技能加Buff是很常见的效果，那么造成伤害、治疗这些瞬间的行为为什么不能通过Buff（绕一下）实现呢？

    所以，这里又有另一种实现：技能管理流程，Buff执行效果；即技能执行到打击点时，总是通过给目标挂载Buff来实现效果。

```
public class SkillAction {
```

    不过，由于从技能转移过来的效果大多是瞬时Buff，为了避免不必要的同步，BuffMgr需要屏蔽这些Buff的同步。

主要缺陷（Buff量）

    用Buff来执行技能效果，其实是将Buff作为了适配器，适配是有代价的。

1. 造成伤害、治疗等效果的体量非常大，全部配置在Buff表会导致非常大的Buff量。
    
2. 技能效果的上下文变得更复杂，会增加效果的实现难度
    
3. Buff的增删频率极大，Buff需要池化
    

    可惜，将主动技能真正实现为的Buff，是更正确的方案 —— 终究是我们先入为主的观念阻碍了我们。

---

5.返璞归真，一切皆状态（State）

![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

    在第三个项目，在经过较长时间的探索后，我成功将第一个项目的战斗架构演进为这套“一切皆状态”的架构。

    它何以称得上返璞归真？继续阅读，你便知晓。

状态组件

```
public class StateComponent {
```

    状态组件可以和传统架构的Buff组件类比，它存储着角色身上的所有状态和状态槽。

状态

```
public class State {
```

什么是状态？

    状态在业务层面的含义，读者可以自行理解。我想告诉大家的是：状态即脚本！只有将状态看做脚本，你才能理解这套设计的强大。

    现在，不论是主动技能、被动技能，还是Buff，都变得很简单，就是给GameObject添加一个状态，也就是给GameObject挂一个脚本，视野是不是一下就打开了？

    这像不像我们在Unity下通过MonoBehavior扩展GameObject的行为？所以，在业务层面，State就是GameObject的行为组件。

状态脚本的实现

    状态的最终逻辑由行为树（任务树）承担，可阅读《[行为树：事件驱动版实现](https://mp.weixin.qq.com/s?__biz=Mzk0MjcwNTY4MA==&mid=2247483909&idx=1&sn=c5e746e8bd1e01da5e4d12ec4dae3b4d&scene=21#wechat_redirect)》《[行为树：返璞归真TaskTree](https://mp.weixin.qq.com/s?__biz=Mzk0MjcwNTY4MA==&mid=2247483940&idx=1&sn=91cafb326ee1c6c4b528e1fc9898d9d6&scene=21#wechat_redirect)》。

    行为树并不是唯一选择，但是是很好的选择；如果不选择行为树，只需要提供一个类似的脚本抽象即可，核心抽象方法就两个：

```
public abstract class Task<Blackboard> {
```

为什么是“状态”，而不是“Buff”或其它？

    回答几个问题即可：

1. 跳跃是不是Buff？死亡是不是Buff？施展技能是不是Buff？
    
2. 跳跃是不是状态？死亡是不是状态？施展技能是不是状态？流血是不是状态？冰冻是不是状态？一切持续性的效果是不是都可以定义为状态？
    
3. Buff是不是状态的子集？（Buff是角色获得的增益或减益状态）
    

状态带来的改变

    前面的框架有一个公共问题：主状态机的状态和技能、Buff的互斥不容易处理。

    在前面的框架下，处理死亡等逻辑时通常都较为麻烦，通常需要编写特殊的代码来处理。这其实是因为主状态、技能和Buff不属于同一概念，不同概念下的事物无法进行比较，也就无法简单处理互斥；而将一切都纳入状态后，我们便可以通过统一的互斥规则代替这些特殊的互斥代码。

主动技能真的也可以吗？

    被动技能和Buff的处理，想必大家不会有太多疑问，但当前施展的主动技能需要支持外部查询，该怎么处理呢？

    需要“状态发布”，即状态在启动时将自己发布到关联的组件。至于状态发布的方式，我们在后面的篇章详细讨论。

```
public class SkillComponent {
```

---

  

状态槽

```
public class StateSlot {
```

静态槽和动态槽

    静态槽表示预设的状态槽，在创建GameObject时即分配，动态槽则在运行的时候根据需要创建。

什么是状态槽？

    我们可以通过电脑的进程和网络端口来辅助理解，先看图：

![图片](https://mmbiz.qpic.cn/mmbiz_png/c2vcRRONwG3h0wDrWt6I5vxd8o4k17icEDufXonxVDYdicY3aGf423l4ykBG9mNXnz193oF1tDz2H88r0TaIdSoQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)

    状态必须先绑定到状态槽才可以执行，好比网络应用需要先绑定网络端口；当多个状态需要绑定在同一个状态槽时，将发生状态槽冲突，将挤掉既有的状态。

    一些状态只能挂载在指定槽上，如：跳跃、死亡、冰冻等只能挂载在1号槽（静态槽），也就是说，我们的1号槽充当了前面架构中的主状态机。因此，我们也可以通过1号槽查询角色的主状态。

    所以状态槽就是个资源句柄，有两个作用：

1. 实现状态的互斥
    
2. 提供额外的查询
    

---

演进过程

    虽然我在上面举了Unity的MonoBehavior例子，但该框架并不是直接通过组件模式演进来的，而是慢慢演进为组件模式的 -- 过程曲折。我在重新实现战斗框架的时候，着重关注三件事：

1. 将行为树引入技能和Buff系统，见《[行为树：序](https://mp.weixin.qq.com/s?__biz=Mzk0MjcwNTY4MA==&mid=2247483881&idx=1&sn=aa7a90d8cccc72e071855de64432f35d&scene=21#wechat_redirect)》
    
2. 如何消除主动技能、Buff、被动技能效果交叠带来的重复感
    
3. 如何实现技能修改器
    

    将行为树引入技能和Buff系统，由于行为树的实现错误，这件事花了很长时间才完成；在这之前，我为了消除重复感，尝试过一个方案：为主动技能、被动技能、Buff提取一个公共的效果（Effect）抽象。

![图片](https://mmbiz.qpic.cn/mmbiz_png/c2vcRRONwG3h0wDrWt6I5vxd8o4k17icEcI27ic0jkbvWHJV2XeFDMskAjpJYH9GU0u2yIelPicKgniceibbaWE0nPA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1)

    为了尽可能保证接口的通用性，接口参数仅一个黑板，如下：

```
public interface IEffect {
```

    但在实现具体效果的过程中发现，由于技能执行上下文（SkillContext）和Buff是不同的抽象类型，导致Effect还是需要独立实现 -- 需要从不同的类型上读取数据。

    这个问题困扰了我好久，由于我坚持认为技能和Buff是不同的东西，因而无法合并它们的抽象；但随着行为树和黑板在项目中的应用越来越多，范围越来越广，我增加了些体悟，如：

1. 要想提升系统的灵活性，应尽量使用黑板代替明确的数据结构定义 —— 关注业务，而不是Class和Interface
    
2. 在使用黑板的时候，应尽可能直接将数据set到黑板 —— 即依赖注入，避免反向依赖
    

    在这一系列思想的影响下，我发现技能和Buff也不是不能合并了；在偶然的灵感之下，我决定使用状态（State）来表达合并后的抽象。

    在确定使用状态这个概念后，我便联想到了跳跃和死亡等主状态机上的状态，我发现它们也可以纳入到新的状态管理中。为方便管理主状态的互斥以及查询，于是增加了状态槽（StateSlot）。

    在获得最终的结构后，我发现了两点不同：

1. 我能证明设计的正确性！不论是拿计算机下的进程来举例，还是拿组件模式来举例，都能证明它的正确性；而在以前的项目中，我都没有这个感觉，只是觉着应该是这样的。
    
2. 我看待GameObject的视角被拉高了。在之前，我设计技能和Buff时，就只是盯着技能和Buff模块看，而现在是盯着整个GameObject在设计 —— 所以结果必然更正确。
    

    “证明设计的正确性”是我在该项目获得的重要习惯，这为后面的技能修改器设计起到了极大的作用。

---

结语

    “一切皆状态”这套架构有极强的扩展性和灵活性，在我看来，基本不会有更好的抽象了。

    读者要想掌握它，关键在于打破通过玩游戏获得的先入为主的观念 —— 不要从玩家的角度去设计战斗系统。