MEGALOVANIA MEGALOVANIA

Audaces fortuna iuvat:命运眷顾勇敢之人

目录
一套ActionCombat系统的分享
/    

一套ActionCombat系统的分享

最近在写一个 FPS 视角的游戏,但是涉及到多武器/动画/近战武器/状态的问题,所以打算把之前在 Unity 里迭代了几版的 Combat 系统总结分享一下。

起源:TempestCombatFramework

最早是还想做动作游戏的时候,找了一些 UE 上的动作游戏模板来参考,比如这个 TempestCombatFramework

具体的设计可以参考他的文档

当然,本身这个框架的设计初衷是制作严格的动作游戏。

它本身示例中制作了一个类魂,一个类血缘,一个类只狼。

但是后来我开发的几个项目里其实都没有这么强的输入判定,并且对动画衔接要求也没有这么高,大部分精度上更接近于大菠萝/火炬之光之类的俯视角简单的动作 ARPG,因此输入 Buffer 这里我做了非常多的简化。

并且因为在动画工具来说,不管是 Unity 还是 Godot 都离 UE 的动画工具有着极大差距.(Unity 也许用一些插件如 Animanicer 之类的能接近),因此这里只是一个控制思路参考

因此具体到一些 TCF 里的细节实现如动画通知之类,可能就视项目情况而定,可能实现得非常简单了

状态管理 StateMgr

状态管理其实就是一个有限状态机

但是区别于纯粹的动画状态机,这里的 StateMgr 更多是一个更加广义/抽象的概念。

举例来说,一款拥有轻攻击/重攻击的游戏角色,不管发动什么攻击,进入的都是 AttackState,而不会具体区分到底是轻攻击还是重攻击。

其他的就同其他的 FSM 一模一样,有 Tick,有 OnEnter,OnExit,有 Check

Tag 索引

在 TCF 的实现中,所有的状态是静态的。一开始所有状态,就通过 WeaponDataAsset 创建好了,就放在一个数组里。

因此在实现的时候要注意状态复用的问题。

因为已经实现好了,所以索引状态的时候就不用工厂方法或者泛型之类的东西了。

在 TCF 中是用 UE 的 Tag 系统来标记,比如 AttackState 标记为 State.Attack。

那么转换状态的代码可能是

1StateMgr.TryPerformState(TagManager.GetTag("State.Attack"))

Tag 索引 TCF 中用得特别多,不限于 State 本身的一些转换和实现,包括输入,Ability 等等逻辑,也是用这种 Tag 的方式来实现,避免了一些乱七八糟的构建问题工厂问题

能力管理 AbilityMgr

我觉得这个设计是 TCF 最优秀的设计之一。

前面提到的例子,一个拥有轻攻击/重攻击的角色,在其 StateList 中是不会区分攻击类型的,只有 AttackState。而逻辑的区分正是通过 Ability,通过设计一个 LightAttackAbility 和 HeavyAttackAbility,使得拥有一些聚合性,又最大限度保证了各个逻辑的灵活性。

放一下文档的原文可能更容易理解

与状态管理器组件类似,它具有与状态管理器组件一样的基本功能。

如果是这样的话,为什么我们有一个全新的组件?答案很简单,就像我在状态管理器组件部分解释的“攻击”一样,攻击非常广泛,因此我们需要更多地指定我们将要执行的攻击的“类型”,这就是能力系统组件的闪光点。一种更具体的攻击类型。

通常,能力会在状态本身内触发。因此,您拥有的某些状态将需要不同类型的能力才能运行。

还有一种不需要任何跑步能力的类型,例如步行或短跑。这些是状态,但没有任何与之相关的能力。

为什么?因为走路就是走路,没有别的了,对吧?

所以,有些状态需要能力,这就是能力系统很方便的地方,但同时,有些状态不需要任何能力。

Ability 通常是跟动画绑定的。我的理解里是一套类型的动画就对应一套 Ability。

我觉得这个概念是提供了一种聚合/拆分的灵活性。

例如,对于一个 DeathState,可能有多种死亡方式:攻击致死,毒药致死。设计师希望在角色被毒死的时候播放一些特殊的动画和粒子特效。

那么聚合的做法是,把这部分逻辑都写到 DeathState 里,DeathState 根据死亡的伤害数据来做判断,直接判断播放哪些动画逻辑。

但当这些情况逐渐变多,例如有 100 中特殊死亡的状态。那么就适合做到 Ability 里。State 持有一个 AbilityList,每个 Ability 有一个 CheckFunction。当触发 DeathState 的时候,对着 AbilityList 依次检查就行。

因此,Ability 是需要知道所属的 State 的

输入缓冲区

前面提到了,如果不是输入逻辑比较复杂的游戏,不需要实现这一部分,但是姑且还是说下思路。

首先,WeaponDataAsset 里会配置能够接受的输入。如持有某个武器的时候不允许跳跃之类。

之后,会根据当前的状态来判断当前的输入是否被允许,如角色死亡的时候,屏蔽大部分输入。

每个状态需要构建一个允许的输入的列表,类似下面的蓝图

这个部分是比较繁琐的。我认为对于大部分动作性没那么强的游戏,可以允许大部分的输入逻辑,只有某些特定状态屏蔽到指定操作。

如果所有的检测都通过了,那么这个输入会作为 LastFiredInput 被存起来,然后在另一个 Update 中用来触发其他的状态转换逻辑。

根据游戏类型,你可以把这部分做得很复杂,比如一些组合技搓招的游戏,比如类似格斗游戏的联防。

这里就不再赘述了。

基于动画的逻辑

对于一个动作游戏,一般来说有这么几个需要跟随动画来执行逻辑的部分。

  • 伤害逻辑。不管是跟随武器检测,还是简单的通过在角色身前投射形状,还是发射飞行道具来投射,游戏需要知道在动画的哪个位置来触发具体的逻辑。
  • 输入逻辑。对于比较动作设计精细的游戏来说,要区分前摇/执行中/后摇,并且某些可以打断当前动画的动作(如翻滚,闪避)何时可以执行。
  • 其他乱七八糟的逻辑,播放程序化的动画/粒子之类

TCF 这里用的是动画蓝图。

动画蓝图可以理解为一段在动画时间轴上执行的代码,比如圈起来的 ANS_BufferInput。在动画时间轴播放到这里的时候,可以执行一段蓝图逻辑。

因此,不管是伤害逻辑还是其他程序动画逻辑,都能很简单通过这种方法来在动画中做编辑。

对于输入逻辑,要参考结合前面输入缓冲区的逻辑,配合 State 中的输入判定来制作。

动画蒙太奇

UE 中的 Montage 是一个非常不错的概念。

Montage 我的理解是一段对 AnimationClip 的封装,不仅能够自定义动画的某些执行轨道(如上述需要的程序轨道)

但是在使用的时候可以当做普通的 AnimationClip 来看待,可以作为某些序列化资产的子资产。

在 Unity 中并没有对应的概念。如果真要实现的话可能就要用 Timeline。可能某些插件能支持这样的功能。

Godot 缺乏单独做序列化的能力,没有办法把单独一个 Animation 摘出来做序列化。

因此在这部分的设计里,各位可以根据自己所用引擎的特点和游戏的类型特点自己处理,TCF 这个设计也是依赖于 UE 非常强大的动画工具链。

数据资产:PlayerDataAsset 和 WeaponDataAsset

上述主要是三个部分:State,Ability,Input,Animation(Montage)

这三部分的联系关系是通过 DataAsset 来设计的。

如角色的 DataAsset 需要配置:

  • 允许的输入
  • 所有可能播放的动画****Montage
  • 基础的 State
  • 基础的 Ability

这部分是不包含战斗或者某些和物品关联的逻辑的。比如攻击,远程射击之类的。

要想拥有攻击能力,不管是射箭,挥砍,~精防核弹 ~,都需要在 WeaponDataAsset 中设计

与 PlayerDataAsset 类似,WeaponDataAsset 只是多了些关于战斗的相关数据,大体上还是这四类。

当切换武器的时候,WeaponDataAsset 的所有 State,Ability 会立即被重新创建,如同我们前面所提到的那样。

题外话

之前在 Unity 里复刻的版本,也根据自己的游戏类型做了一些改良

感觉还是比较依赖各种插件的。比如 Odin 这种增强 Inspector 的插件。如果没有的话,你要一行行手写这些序列化会无比痛苦。

不禁想到了几年前开始做项目的时候,傻傻地手写 DotweenTimeline 编辑器的时候。

他妈的,其实直接上 Odin 就好了,太傻了。

有的努力和痛苦可能就是几年以后回头一看,妈的,傻逼吧

现在打算在 Godot 里复刻一套,感觉 Godot 的工具链稍稍比 Unity 有好一点,但是比不上带 Odin 的 Unity。

大概就这样

Godot 里的系统

其实还有很多以前做过的,也在几个项目之间迭代了一些思路的系统,比如之前在驭光档案中开发的 Buff 系统。基础应该是来自一个知乎的文章,然后经历了两三个没做完的项目的迭代。最后这个 Buff 系统已经可以通过配表来配置出大部分的技能效果了,我个人非常满意。

有些曾经在 Unity 写过的比较熟悉的方案,可能在 Godot 里还没有找到什么很好的适合的方案来做,比如说之前我用下来觉得还可以的 VContainer+MessagePipe.但是因为 GDScript 的很多内容还是属于一个完全没开发的状态,所以直接整一套 DI 框架估计也是自己搞自己。

有一套一直在做,但是一直不知道到底应该做成什么样好的系统,就是 Actor 系统。感觉自从离职以后一直在做各种各样稀奇古怪的 Actor 系统,有自己跑的,有战旗的,有第一人称的,有第三人称的,有俯视角控制的。但是一直没有一个特别靠谱的思路和流程。

尤其在 Godot 我还不是很熟悉 GDScript 的正常姿势的时候,中间有过一个 2DIsometrc 的项目的时候,Actor 系统写得非常挣扎。

后来又看了双影奇境的分享,感觉双影奇境那套思路挺有意思的。但是还没有具体实践过。而且目前来说的游戏类型似乎也没有双影奇境那种那么多复杂变化的 GameplayActor 机制。

因此在真正整理出一套可用的思路方案之前,应该还是先不写这部分了。

之前看过一个思路,说是要面向“复用模块”做游戏:即使是这个游戏没有办法成功做完,或者是做完了,但是游戏并没有成功。那么开发者应该做的其实是面向这些复用的思路和模块开发,不管是设计思路,编程模块,美术流程,这些都是应该单独当做开发项目来做和迭代的。

很久没写正经的技术 Blog 了,也是因为受了这个观点的启发。

通过分享来思考,通过思考来记录,通过记录来迭代


标题:一套ActionCombat系统的分享
作者:matengli110
地址:https://www.sunsgo.world/articles/2025/11/01/1761977088148.html