ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

虚幻引擎GAS实战:构建ARPG战斗系统框架的核心机制

虚幻引擎GAS实战:构建ARPG战斗系统框架的核心机制 1. 先捋清楚ARPG战斗到底需要框架解决什么1.1 战斗系统的硬需求清单做ARPG动作角色扮演游戏的人都知道战斗模块是项目里最容易变成屎山的地方。一开始你只是为了砍一只怪写了攻击动画、伤害数字、怪物掉血这三件事。然后你加了连招加了闪避加了技能冷却加了Buff加了暴击加了异常状态。半年之后再回头看动画回调里挂着伤害逻辑伤害逻辑里又掺着Buff判定Buff判定里还嵌套着冷却计时——已经没人敢动了。所以我一直主张一个观点动手写战斗代码之前先把战斗框架到底要解决什么一条条列出来。ARPG的战斗需求归纳起来其实是以下六大块状态管理角色在站立、移动、攻击前摇、攻击后摇、受击、闪避、死亡之间的切换以及这些状态之间的打断规则。这是手感的基础。技能系统不只是放个技能炫一下还包括连招、蓄力、派生、瞄准、多段伤害、召唤物这些复杂技能逻辑的组织方式。ARPG的技能复杂度明显高于MOBA和格斗游戏——你已经不需要推塔需要的是这套动作有没有打击感。属性与养成HP/MP/体力/攻击力/防御力/暴击率/暴击伤害/元素抗性……属性之间还有乘区、加区、百分比修正。角色升级、装备变更时这些数值要跟着动态变化而且变化过程要可回溯。状态效果Buff/Debuff燃烧、冻结、流血、眩晕、减速、护盾……每一种都有独立的持续时间和效果逻辑还要能叠加、刷新、免疫、驱散。伤害结算与反馈从攻击判定命中到计算减伤/暴击/抗性再产生飘字、音效、相机震动、受击动画、击退效果链路很深。多人联机下的同步与预测如果是单机这一条可以划掉。但只要沾了联机战斗的每一个环节都要考虑客户端表现和服务器判定怎么配合。1.2 GAS为什么能对应上这些需求GAS全称Gameplay Ability System是虚幻引擎整套战斗体系的官方答案。我第一次接触GAS时有一个很直观的感受它不是一个技能编辑器而是一套完整的战斗领域建模框架。它用四样东西构建了战斗的世界观——GameplayAbility描述能做什么GameplayEffect描述变化了什么AttributeSet描述有什么属性GameplayTag描述处于什么状态。这四样东西互相配合正好一一对应上面说的六大需求。当然你会问不用GAS行不行答案是行尤其是小项目。我自己做过纯蓝图的ARPG原型用一个枚举Switch节点维护状态机用一个数组存Buff用定时器做伤害判定——前期写起来很快30分钟就能跑通按一下砍一刀。但是当技能数量超过10个、Buff超过5种、角色超过3个、并且需要测试团队反复调手感时这套自研方案就开始失控了。每一次战斗逻辑改动都要在无数个蓝图节点之间找线索这比代码里的意大利面条更难处理。用GAS的代价是前期学习曲线陡峭——Ability、Effect、Task、Cue这些概念叠加起来确实劝退。但一旦熬过去战斗系统的每一块骨头都有了固定位置新角色就是拼积木新技能就是写配置这种感觉是自研方案给不了的。2. 拆开GAS这套骨架四件套怎么分工2.1 AbilitySystemComponent一切的中枢GAS的四个核心组件里第一个是AbilitySystemComponentASC。它挂在角色身上充当所有战斗数据的总线。属性注册、能力授予、效果应用、Tag查询全部经由它来操作。可以把它理解成一间房子的配电箱——你不需要知道灯是怎么接的只要打开配电箱的开关灯就会亮。GAS里所有系统之间的互动本质上都是通过ASC来转发消息。ASC挂在谁身上是有讲究的。对玩家角色业内常用方案是挂在PlayerState上而不是Character上。为什么因为PlayerState的生命周期是跟着玩家的会话走的角色死亡重生后PlayerState还在属性、经验、等级这些持久数据不会丢。而挂在Character上的好处是代码写起来简单图省事坏处是一旦角色被销毁重建所有属性和持有的技能全部归零。单机原型可以图省事联机项目还是建议一步到位挂PlayerState。初始化ASC时有一个关键调用InitAbilityActorInfo(OwnerActor, AvatarActor)。OwnerActor是拥有者通常是PlayerStateAvatarActor是实际表现体通常是Character。GAS内部大量逻辑依赖这两个角色的区分——能力在Avatar上播放Montage属性数据属于Owner。这个区分不理解后面排查技能放不出来会非常痛苦。2.2 AttributeSet只做数据的管家AttributeSet这个名字容易让人误解——它不是系统而是数据集合。你可以把它想成一张Excel表行是属性名Health、Mana、Attack、Defense……列是当前值。AttributeSet本身不写逻辑只有数据属性和属性变化时的回调函数。ARPG的AttributeSet设计通常分三层Primary属性力量、敏捷、智力这类基础数值决定成长方向。Secondary属性由Primary计算出的派生数值比如攻击力、暴击率、移动速度。Vital属性Health、Mana、Stamina这类战斗中实时变化的数值有上下限会频繁波动。我们项目里Vital属性和Secondary属性是分开存放的。比如Health这个属性在PostGameplayEffectExecute回调里做Clamp下限0、上限MaxHealth同时在这里判断如果Health降到0广播死亡事件。这里有一个很重要的细节不要在AttributeSet里直接改数值、做复杂逻辑它只负责通知我的值变了至于变了之后召唤分身还是触发剧情交给监听方处理。这样设计的好处是职责单一出问题时顺着回调链一查就能定位。2.3 GameplayAbility技能的行为定义GameplayAbility后文简称GA代表角色能执行的一个动作。普攻是一个GA重击是一个GA闪避是一个GA火球术是一个GA。GA里存放的是这个技能的全部行为逻辑——消耗什么、冷却多久、播放哪段动画、什么时候造成伤害、什么时候可以被打断。GA有一个很核心的实例化策略属性InstancingPolicy。动作游戏里的战斗技能几乎都该用InstancedPerExecution——每次执行都创建一个新实例。为什么不用默认的NonInstanced因为NonInstanced的GA本质是共享的静态逻辑实例之间没有独立数据存储空间。而ARPG技能经常需要记录当前是第几段连招蓄力了多长时间本次伤害打了谁这些状态在共享GA里没法存储一旦多个角色同时触发同一个GA数据就串了。2.4 GameplayEffect所有数值变化的总代理这是GAS里最反直觉的概念之一。很多新手以为GE是技能效果比如火焰特效、冰冻效果。错了。GameplayEffectGE是数值变化的载体——它本身不执行任何逻辑只是描述在某个条件下某几个属性要增加或减少多少。真正的变化通过Modifier修饰符作用到AttributeSet上。GE的Duration Policy分为三种对应三种截然不同的使用场景类型行为典型用途Instant立即生效数值永久改变或被即时消耗伤害、治疗、消耗魔法值Duration持续一段时间到期自动移除燃烧状态、短暂加速、持续掉血Infinite持续生效直到被外部主动移除装备加成、Buff栏常驻效果、被动技能每次理解GE这三种类型我脑子里浮现的都是一个降落伞包。Instant就像拉开伞包——结果立刻发生落地减速、被伤害没有后续过程Duration像伞绳上的弹簧——在一定时间内持续拉扯属性时间到了自动松开Infinite像背带——只要你穿着就从起始点一直拖着它走除非你主动解开扣带。用这个类比去跟策划解释GE的类型基本一次就能讲清楚。还有GameplayTag。它不参与属性计算只负责打标签和查标签。比如State.Stunned表示角色被眩晕State.Invincible表示角色无敌Cooldown.Skill_1表示技能1处于冷却中。战斗逻辑里大量使用Tag判定——能不能被眩晕查有没有Stun免疫Tag能不能攻击查有没有Dead Tag技能能不能释放查有没有正在释放其他技能的状态Tag。Tag的价值在于它是纯文本元数据可以被任何系统读写不受代码类型限制这让它成了各模块之间沟通的通用语言。3. 技能从按下到收招ARPG手感在GAS里怎么调出来3.1 一条完整的技能触发链路ARPG里玩家按下一个攻击键视觉上期待的是角色立刻做出可感知的挥砍动作。这条链在GAS里是这样走通的输入层Enhanced Input检测到按键触发InputAction回调。路由层回调里调用ASC-TryActivateAbilitiesByTag传入技能对应的Tag比如Ability.Attack.Light。这一步是把按键和技能解耦——同一个按键在不同状态下可以映射不同技能。激活层ASC找到所有带该Tag的GA逐个执行CanActivateAbility检查是否在冷却、是否消耗够、是否处于可释放状态。检查通过GA进入激活状态。执行层GA内部从ActivateAbility开始跑。先CommitAbility正式提交消耗和进入冷却再播放Montage同时挂上AbilityTask等待事件。我当初踩过的一个小坑用蓝图直接调用GA上的函数是没有用的。GAS里你不能直接执行一个GA必须通过ASC来激活——所有能力必须被ASC授予Grant Ability后才算注册在案。场景里摆了100个角色他们的ASC各自维护着一份可激活技能列表你在某个GA里直接调函数其他系统根本不知道。一切从ASC路由是GAS的铁律。3.2 前摇、后摇和Montage事件ARPG的打击感很大程度来自节奏而节奏又来自前摇出招准备和后摇收招硬直。GAS处理这个问题的标准姿势是播放Montage然后在Montage里插入AnimNotify事件。比如轻攻击的Montage在第3帧是前摇结束点第5帧是伤害判定点第8帧是后摇结束点。这三个位置分别插入NotifyGA内部用WaitGameplayEvent监听这些事件。收到伤害判定点事件执行伤害判定函数收到后摇结束点事件确认没有后续输入后EndAbility。这套流程把动画播放和战斗逻辑彻底解耦——换动画不用改逻辑改逻辑不用重新K动画。对于移动端的ARPG前摇通常做得很短0.2秒内后摇可以通过取消输入来跳过。这个取消后摇的逻辑也可以做成一个独立的Tag当角色处于后摇可取消的Tag状态时闪避技能可以强行打断当前GA。这比在每一个技能里单独写如果被闪避打断则……要干净得多。3.3 连招系统的三种实现方案连招是ARPG的招牌玩法GAS里实现它有三条路优劣差异非常大Montage Section切换把一段连招分成多个Section放在同一个Montage里每次触发切换Section。好处是不用拼接多个Montage坏处是连招越多Montage越臃肿一旦某个Section要插入特殊逻辑动画资源管理会变得很难受。Ability内部事件计数GA里维护一个整数变量每次攻击命中就1然后根据变量值播放不同的Montage。这种做法简单直接但一旦计数状态和动画状态不同步就会出各种灵异Bug尤其是死亡后重置、跨技能继承不了连击数。GameplayEvent驱动链推荐方案。每次攻击结束时在Montage尾部发一个ComboWindow事件GA监听该事件启动连击窗口——如果在0.3秒内再接一次输入则当前GA触发连击分支播放下一段Montage否则收招。这个方案把连第几招完全交给事件流驱动动画、音效、伤害可以分别挂在不同事件节点上扩展性最好。我们最终使用的是方案三。每个普通攻击技能被拆成了三段GALightAttack_1、LightAttack_2、LightAttack_3它们之间通过ComboWindow事件串联。每段GA都是独立的单拿出来也可以直接用这就让普通攻击三连这个技能组合既灵活又不会互相干扰。3.4 受击打断和技能取消ARPG中被怪物打断攻击非常常见这个在GAS里规范做法是怪物受击时给自己加一个State.HitReactTag同时广播GameplayEvent.HitReact。ASC在CanActivateAbility阶段会做检查——如果角色已经有State.HitReactTag当前正在播放的技能会被标记为可中断通过设置GA的CancelAbilitiesWithTag属性并且GAS会自动调用CancelAbility收掉正在执行的技能然后ASPlay受击Montage。这是GAS比传统状态机方案舒服很多的地方。传统方案你要在状态机里手动加攻击状态可以被受击状态打断的转移线几套技能下来画满了线。GAS里你只需要声明当前GA会被哪些TagCancel和当前GA在激活时给自己贴上哪些Tag打断关系由Tag规则自动推导。策划改打断规则时不动代码只调整Tag配置。4. 伤害数值与养成闭环砍一刀背后的计算链4.1 从命中判定到飘字一个标准的ARPG伤害流程在GAS里完整的链路是这样的命中判定攻击GA在Montage的伤害判定点触发一个Overlap查询或基于场景的Box Trace。这一步主要在服务器执行确保权威判定。数据装配攻击方GA把基础攻击力、暴击率、穿透率写入伤害GE的SetByCaller参数里。公式计算伤害GE应用时通过Execution Calculation执行计算简称ExecCalc读取攻防双方的AttributeSet套用伤害公式最终伤害 基础攻击力 × 技能倍率 × (1 暴击修正) × 防御减免 × 抗性修正。结果应用计算出的数值通过GE的Modifier作用到目标Health上触发Attr的PostGameplayEffectExecute回调。表现反馈回调里广播GameplayCue客户端收到Cue后播放飘字、命中特效、震屏、音效。这条链路里最容易翻车的是第2步和第4步的衔接。如果直接把攻击力数值写死在GE配置文件里同一个GE被不同角色用就会出问题。正确做法是GE只定义乘数和规则基础攻击力通过SetByCaller在运行时动态灌入。这一步不理解后面做技能倍率、角色Buff加成时会撞得满头包。4.2 Execution Calculation用在哪、不用在哪ExecCalcExecution Calculation是GAS里做复杂公式计算的地方本质是一个类你重写它的Execute_Implementation函数在里面可以读取攻击者和目标的所有属性算出最终数值再把这个数值写回Effect的Modifier。ARPG里强烈建议把伤害公式集中到一个ExecCalc里统一管理。这样策划调整减伤曲线、加入新乘区时你只需要改这一处所有技能自动生效。我见过把公式拆到十几个GA里的项目每次数值迭代都像是在雷区里漫游——这个GA忘了套公式那个GA多算了一个乘区体验极其痛苦。但ExecCalc不是万能的。如果你的伤害公式就是简单的攻击-防御甚至不用ExecCalc直接用GE里的Modifier定义加法和乘法就行。ExecCalc适合公式复杂、变量多、需要同时读取多个属性的场景。另一个原则是ExecCalc里只算数值不做多余的事。有些开发者喜欢在ExecCalc里顺便施加Buff或者做闪避判定这会严重破坏职责边界建议把那些事放回GA或Attr回调。4.3 暴击、抗性和受击反馈暴击在GAS里没有现成功能需要自己搭。常见做法是在ExecCalc里读取暴击率属性随机掷骰决定是否暴击暴击时把结果乘上暴击伤害倍率。关键是暴击这个结果要能被后续系统感知到——比如飘字时暴击显示黄色大数字。做法是在ExecCalc里给伤害GE打上一个CritTag然后飘字系统监听这个Tag。抗性系统更常见护甲、元素抗性、韧性本质上都是按百分比或固定值减伤。我实战中用得最多的公式是实际伤害 原始伤害 × (1 - 护甲/(护甲 常数K))。K是衰减常数用来控制减伤曲线的陡峭程度。这个公式的好处是护甲堆到任何数值都不会让伤害归零数值曲线平滑策划调起来手感好。受击反馈这里有一个GAS特有的捷径GameplayCue天然支持NetMulticast和本地预测也就是说你在服务器上执行了一次伤害计算Cue会自动在小范围内广播给所有客户端的表现层——飘字、音效、HitStop、慢镜头、震屏。如果不用Cue而是手动RPC逐个发光同步伤害表现这件事就能消耗掉一个程序员一整周。4.4 升级成长和装备变更怎么灌属性ARPG的养成闭环是击杀怪物获得经验 - 升级获得属性点 - 加点提升Primary属性 - 由Primary属性推导Secondary属性 - 最终体现在战斗力数值上。这一整套GAS并不内置你需要自己搭升级→加点的逻辑但最终属性点转化要用GE来做。比如获得5点力量时创建一个Infinite型GEModifier把力量5、同时把暴击伤害2%然后Apply到角色身上。装备系统同理——穿上一把武器就是Apply一个携带多个Modifier的Infinite GE卸下武器就是RemoveEffect。用GAS管养成的好处是所有数值来源都变成一个个可追溯的GE实例调试权重、修改平衡时一目了然。5. 多人联机视角客户端预测与状态同步的取舍5.1 ASC的挂载与复制策略GAS在单机项目上的优势可能只是代码组织得好但它真正的杀手锏是网络支持。只要是联机ARPG项目选择合适的网络策略能让后面的开发省掉一半返工时间。前面提到玩家角色的ASC建议挂PlayerState。这一个选择直接影响到联机的稳定性——角色死亡重生PlayerState不变属性、技能持有列表、冷却进度全部无损延续。如果挂在Character上你还要在每次重生时重新Grant技能、重新Apply装备GE稍不留神就会漏一个。ASC有一个属性复制模式Replication Mode的设置ARPG项目按需选型Full所有属性、GE、Tag全部复制数据最完整带宽消耗最大适合回合制或卡牌这类低频战斗。Mixed属性复制完整GE只复制能造成影响的推荐ARPG使用。普通攻击、技能冷却这些高频低量的数据都在本端表现只有关键状态死亡、眩晕、大硬直做全量同步。Minimal属性都只在Owner端复制其他角色一律不拿。适合大世界大量角色但交互少的情况动作游戏几乎不推荐。5.2 技能预兆、伤害判定的权威性联机ARPG里最麻烦的问题是延迟下的手感。如果你的服务器RTT有80ms玩家的攻击按键确认要经过客户端→服务器→客户端一整圈那么所有攻击都会有可感知的延迟感。GAS提供了一套相对成熟的预测机制——客户端先试着执行技能预测服务器确认后同步结果如果预测错了客户端回滚。但是ARPG的伤害判定不适合过度依赖预测。为什么因为伤害判定涉及大量瞬时的、不可预知的数值变化如果客户端预测一刀打了500伤害服务器算出来是480回滚时的数值跳变就会造成伤害数字闪烁这种很奇怪的表现。我的经验是技能释放和Montage可以走预测提升手感伤害判定必须走服务器权威。换句话说玩家按下攻击键客户端立即播放挥砍动画体验好实际扣血数值以服务器结算为准数值稳。这两种机制并不冲突——攻击动画是表现扣血是结果表现可以先行结果必须权威。5.3 Montage与Cue的广播机制GAS自带的Montage复制机制比较省心——只要你通过GA的PlayMontage来播动画GAS会自动把动画播放信息同步给所有客户端包括播放哪个Section、BlendIn时间是多少。但坑也藏在这如果你直接在角色蓝图里调PlayAnimMontage这个动画不会走GAS的复制通道其他客户端看到的就全是对不上节奏的鬼畜动作。这是排查联机动画不同步问题时第一个要检查的点。Cue的广播则非常成熟。GameplayCue的执行天然带了服务器调用、全员表现的语义。伤害飘字、技能爆炸特效、受击音效这类表现层的东西都可以做成Cue。Cue还有一个独有特性是与GE深度绑定——当一个GE被应用时你可以指定一个对应的CueGE移除时Cue自动消失。用在燃烧火焰附着这种持续视觉表现上非常顺手。6. 落地时避开这些坑从原型到生产环境的经验6.1 先跑通最小闭环再扩展如果你是第一次用GAS做ARPG我的建议很直白不要在项目一开始就想着我要用GAS写好所有技能。先做一个最小闭环——一个Character一个ASC一个属性集只放Health和Attack一个普攻GA一个伤害GE。把下面这条链路跑通按键 - TryActivateAbilitiesByTag - GA激活 - PlayMontage - AnimNotify发事件 - 事件触发伤害判定 - GE应用 - 目标Health减少 - 目标播放受击动画 - 血条UI刷新这条链路里包含了GAS的十个核心机制路由、激活、提交、动画、事件、判定、效果、属性、反馈、UI数据绑定。这个闭环通了你已经使用了GAS八成以上的底层能力剩下的技能、Buff、AOE、召唤全是这个环路的排列组合。我见过太多新人一上来就抄一个雷电技能的教程Get到一堆碎片知识最后连技能为什么没有伤害的报错都看不懂。6.2 AnimNotify的时机与Event订阅的竞态这个坑让我调试了整整一个下午。用Montage的Notify发GameplayEvent给GA时监听方使用WaitGameplayEvent能力任务。但在某些情况下——比如技能刚激活、Montage刚开始播放的前几帧Notify可能已经发出去了而技能里的WaitGameplayEvent还没来得及注册监听。这个事件就丢了伤害永远打不出来看起来像是一个随机不判定的离奇Bug。解决办法有两个。一是控制Notify和Task的启动顺序在激活GA时不要马上PlayMontage而是先创建Task再播放Montage保证监听先挂上。二是像C里常见的做法在GA激活后的CommitAbility阶段就预创建好Task再通过Task本身去启动Montage。经过这个坑之后我给自己立了一条规矩所有通过Event驱动的技能逻辑一律用Task先注册再执行绝不在ActivateAbility里裸放Montage。6.3 Tag清除和Ability结束的边界GAS里最隐蔽的Bug潜藏在状态残留里。技能激活时会给角色加上Ability.Attacking这种Tag如果技能在播放中被强制取消受击打断、死亡打断这个Tag有可能没被移除。结果就是角色已经躺在地上了UI上还显示攻击中其他技能也无法释放。规范做法是在GA的EndAbility回调里对本次激活所使用的Tag做一次彻底清理包括取消时、异常退出时、任务被强制End时。另一个经验是给GA设置合理的CancelAbilitiesWithTag白名单让打断谁这种关系提前定义好而不是在逻辑里到处找地方调CancelAbility。6.4 属性同步和UI数据绑定的细节GAS的属性复制是整包复制的客户端拿到属性更新后UI血条的更新应该监听AttributeSet的属性修改委托而不是每帧轮询。这个委托在属性变化回调中触发帧率再高也不会丢更新。还有一个血泪教训血条UI不要在Health属性变化时直接Tween到新值——因为联机下Health可能在一帧内连续扣好几下一次Combo的多段伤害你同一帧内触发5次UI变化UE不会给你画5次UI只渲染最后一次的帧终值玩家看起来就像血条一下就空了。正确做法是UI暴露一个目标数值内部按固定速率追踪表现上就是平滑下滑手感好很多。这个优化在单机时不明显一旦上线联机测试玩家反馈打击感缺失时排查方向之一就会落在这里。6.5 什么时候别用GAS必须泼一盆冷水GAS不是银弹。如果你的项目是休闲类、超轻量解谜或者战斗只是点击敌人自动打这种弱交互强行上GAS反而增加无谓的心智负担。GAS对项目的帮助体现在技能多、状态复杂、数值体系完善、需要联网这四个条件下少一个都可以先考虑自研方案。我自己接手过一个已经跑了半年的放置类游戏战斗逻辑全靠定时器我评估后没有引入GAS——收益低于改造成本。但如果你确定要做ARPG那GAS值得长期投入。我的个人体会是GAS的骨架设计在最初就很扎实官方迭代多年补了很多边角功能比如Prediction、DataDriven能力配置、AttributeAggregator用的越久越能感受到这套框架在为复杂战斗建模上的深度。真正投入半年以上之后你会发现它已经不只是技能系统了——你团队的交流语言都会改变讨论玩法时大家直接说这个Buff走Duration GE还是Infinite GE这个连招在第几帧发GameplayEvent——框架内化成了项目的语言体系。最后分享一个我每次带新人都会说的技巧建议把GAS的四件套和它们之间的协作逻辑画成一张物理卡片贴在工位附近不是流程图是结构图。新功能开发前先对照卡片思考这次改动涉及哪些组件、数据从哪来、流向哪去、Tag怎么变化。想清楚了再动手你会发现实际编码只是把这些关系翻译成UE API调用而已。
返回列表