GameplayAbility框架在《幻塔》手游中的应用及优化
Scylla
2022年6月30日
完美世界
幻塔
技术博客
游戏
一、概要
《幻塔》手游作为一款使用虚幻引擎4实现多人在线开放世界动作游戏,为实现多样化的连招系统以及多人同时战斗,虚幻引擎4的技能系统GAS(GameplayAbilitySystem)正好能满足我们整体技能框架的需求,所以假如你的游戏中需要比较复杂的技能系统,GAS插件是一种非常好的选择。
Epic官方的ActionRPG工程,是虚幻提供的一个示例项目,该项目展示了GAS的详细用法,可以在虚幻商城中免费下载。当你想去了解和学习GAS的详细用法,先学习使用ActionRPG能起到事半功倍的效果。
二、技能系统GAS的简单介绍
GAS主要包含以下几个关键的类
AbilitySystemComponent(ASC)
技能系统组件AbilitySystemComponent是整个GAS的核心,用于管理Abilities和GamePlayEffects,以及处理技能系统的所有交互。该组件一般附加在需要拥有技能的Character上,当然也可以选择附加在PlayerState上。我们可以根据游戏类型选择适合自己的模式。
GameplayAbility(GA)
GameplayAbility是GAS中对于单个技能的实现,我们可以非常简单的实现一个技能效果。其中主要包激活技能,执行消耗、冷却,取消技能和结束技能。GA中还能触发异步技能任务(AbilityTask),他们承担实现各种技能功能,例如:播放Montage,监听碰撞实现伤害,实现技能移动效果。多种多样的AbilityTask组合,可以很方便的实现设计人员想要的技能效果。
GameplayEffect(GE)
GameplayEffect是GAS中的Buff类,主要用于修改自己或者目标的属性,根据DurationType可以分为:一次性的GE、永久性的GE和持续时间性的GE。一次性的GE一般用于一次性的伤害的计算,永久性和持续性的GE,就是我们一般意义上的Buff效果,可以实现例如光环效果,回血效果,减伤效果,减速效果。GA中的CoolDown和Costs 就是使用GE来实现。
GameplayCue(GC)
GameplayCue是GAS中的用于实现表现效果的部分,主要用于GE执行后,可以通过GameplayTag来关联创建一个GameCue,其中主要包含OnActive、WhileActive、Removed和Executed函数,我们可以重写他们,来实现功能,例如:实现一个中毒后身上冒绿光的效果。其实就是执行一个扣血DeBuff(GE),我们在GE上关联一个GameCueTag,当执行GC时候在OnActive里面可以创建一个特效,来展现扣血Buff的表现效果。
技能的释放流程
这里我们举例一个《幻塔》手游中最简单的技能实现流程。其中包含两部分,其中一部分是C++代码的流程,另外一部分是蓝图上组装技能流程和实现技能效果。
C++代码部分
首先我们想释放一个技能需要调用UAbilitySystemComponent::GiveAbility,返回一个技能的Handle,然后才能执行技能。执行流程如图:
蓝图上组装技能流程
这就是《幻塔》手游中最简单的一个点击攻击按钮执行一次刀砍技能的流程。要是大家看过《ActionRPG》工程中的技能蓝图,肯定发现也是类似。其中最主要的PlayMontageAndWaitForEvent是执行播放Montage并监听EventTag来执行技能效果的Task。这样我们就组装好了一个最简单的技能释放流程。
三、技能系统在《幻塔》手游中的实战应用
GA上的Advanced配置选择
对于《幻塔》手游这样一款开放世界多人在线动作游戏,网络延迟引起的攻击手感延迟是非常不好的体验。为了增强游戏按键反馈的及时性,我们在执行技能的时候就需要能够及时的释放表现出来。GAS充分考虑到了我们的问题,在Ability对象的 NetExecutionPolicy属性中提供了四种策略。
LocalPredicted(本地预测)
LocalOnly(本地执行)
ServerInitiated(服务器先执行)
ServerOnly(仅服务器执行)
我们游戏中选择的就是LocalPredicted模式,通过本地预先释放技能,这样能很好的解决网络延迟所带来的动作手感不好的问题。
图中左上角网络显示400ms的延迟,但是我们能正常的释放技能并且进入cd,然后还能释放连招,虽然网络延迟非常高但玩家在主角释放技能过程中体验都非常流畅。
Ability中InstancingPolicy实例化策略
此参数决定激活GA的时候是否以及如何实例化GA。在《幻塔》手游中我们使用InstancedPerActor策略,可以有效的满足策划对技能的定制化设计,并且也能避免每次释放技能去实例化技能对象所带来的开销。
《幻塔》手游中如何使用GAS系统实现连招
在UE4中Montage可以配置多个Section,这样我们可以实现一种同一个Montage多个Section之间的互相跳转实现连招的方案。另外一种方案就是将每一个连招实现单独的Ability,然后通过逻辑控制实现不同技能之间的切换。
第一种方案能有效的减少连招技能的配置,只需要将连招统一配置在一个Montage中,方便管理,但是灵活性不够,假如技能的连招比较复杂,有多种技能逻辑,这样一个技能中就需要写复杂的逻辑来实现。第二种方案,对于单个技能的逻辑可以比较简单的封装,只需要支持单个Section的逻辑需求,不需要将所有的逻辑都写在一个GA上,但是美中不足就是要实现一个连招,需要配置的GA很多,而且比较难管理。在《幻塔》手游中使用的是两种方案结合的方式。
图一
图一是Ability上实现连招的配置参数,是一个KeyValue的实现,Key对应的是当前播放的片段,Value是指实现跳转到下一个Combo的条件包。
图二
图二是跳转到下一个Combo的条件包,此条件是延迟多久后主动跳转到Montage的片段,如果配置了Ability,就跳转到另外一个GA的Montage对应的片段。我们可以实现多种多样的条件包,来满足多样化的连招规则。
AbilityTask在《幻塔》手游中的运用
AbilityTask是GameplayTask的子类,只在GameplayAbilities中使用。AbilityTask默认是异步执行。Ability的执行如果期间没有运行Task的话,实际上按顺序执行过程,在同一帧就结束了技能流程,加入AbilityTask将一个技能的执行变成了持续时间的一个过程,这样就能完成随着时间推移而发生的行为,或者监听某些行为而做出反应。为了实现技能持续时间的过程,我们继承AbilityTask实现了PlayMontageAndWaitForEventTask、WaitInputPress、WaitInputRelease、ApplyRootMotionTask等。
PlayMontageAndWaitForEventTask
在ActionRPG示例中实现的技能流程中触发播放Montage和监听GameEvent的两大部分逻辑。这就允许在AnimNotify动画通知GA执行技能效果。这对于动作游戏中需要播放动作和触发伤害做出精确的匹配提供了一种完美的解决方案。
WaitInputPress和WaitInputRelease
在技能蓝图上添加上按键状态监听,《幻塔》手游中使用这类Task来实现长按、抬起等行为,实现长按进入瞄准状态、长按进入蓄力攻击等需求。
我们在WaitInputRelease中添加了一个 AutoTriggerTime参数,当长按0.3s后,会自动触发Release事件以此认为是触发了长按行为,进而触发后续逻辑。例如《幻塔》手游中的长按进入瞄准逻辑就是依靠此Task来实现的。
ApplyRootMotionTask
ApplyRootMotionTask实现的是非Montage自身的RootMotion,通过程序逻辑控制实现按指定方式的移动。其中核心逻辑使用RootMotionSource,是CharacterMovementComponent中相比于动画驱动的RootMotion优先级更高的移动模式,他可以更灵活的实现动态控制。比如实现一种非匀速的冲刺到目标面前的技能。我们可以有多种方案,比如美术做动作带上根骨骼位移,这种方案能有效的实现动作和速率变化的匹配,但不够灵活,当位移距离不是固定的情况下,这种方案就受到了限制。这个时候,我们使用ApplyRootMotionMoveToForce就能很好的解决这类问题,而且是支持同步的。
GE实现的Buff系统
GameplayEffects(GE)是改变自身和他人属性和GameplayTag的容器。它们可以造成属性变化,比如伤害或治疗,或长期状态增益/减益。GE有三种持续时间:瞬间、持续时间和永久。
《幻塔》手游中实现的Buff保存系统
我们使用GE来实现Buff系统。原生的GAS系统中不支持保存,我们在这基础上实现了一套自定义的保存机制,用于玩家上下线时处理Buff保存功能。通过监听ASC中的OnGameplayEffectAppliedDelegateToSelf和OnAnyGameplayEffectRemovedDelegate代理回调,记录下当前GE的ID,开始时间,当前层数,持续时间等属性,在Remove事件中来删除Buff,并且在再次上线后,重新计算剩余时间,来实现Buff下线保存功能。
GE衍生Buff事件
一种类似回调事件,配置定义于UGameplayEffect派生类上,例如,玩家进入战斗,触发事件:效果是GE添加,第几层开始效果生效(默认0直接生效),施加者选择(BySelf,TargetTrigger,UseSourceObject),触发对象名称(是否指定对象ID),释放GA列表,添加GE列表(GE添加成功后执行效果),GE时间修正,GE奖励列表。如图一所示:
图一
图二
例如此图二,玩家通过监听攻击事件来叠加玩家身上的Buff,每攻击一次就能叠加一层Buff。在《幻塔》手游中使用的很普遍,设计人员能很方便的来通过监听事件实现想要的效果。
四、GAS在《幻塔》手游的优化设置
RepAnimMontageInfo的同步策略选择
ASC组件中成员变量RepAnimMontageInfo是用于同步模拟端动画播放,通过调用SetMontageRepAnimPositionMethod来设置位置同步方式,并且提供两种同步方案。第一种是直接同步Montage播放的具体时间,第二种是同步Montage播放到的Section。 我们可以使用第二种方案,仅同步当前播放的Montage片段,而不是时刻同步当前Montage播放时刻,能减少模拟端的同步频率,减少网络消耗。
图一:第一种方案的NetworkProfiler
图二:第二种方案的NetworkProfile
在一次攻击的过程中,图二同步Montage的开始和结束,能有效的优化网络同步次数和消耗
GamePlayEffects的同步策略选择
ASC组件中成员变量ReplicationMode是用于设置成员变量ActiveGameplayEffects的同步策略,在实际项目中使用发现ActiveGameplayEffects的同步压力比较大,通过选择合适的同步策略,能有效的减少同步压力,GAS中提供了三种同步策略。
在多人游戏中,玩家操作的角色使用Mixed模式,AI建议使用Minimal模式,这样能有效的减少 ActiveGameplayEffects的同步开销。
当AI设置成Minimal的模式,模拟端中是获取不到ActiveGameplayEffects的信息,当不满足游戏设定的时候,我们可以通过简化数据,将其通过Pawn来同步到模拟端,举个例子:有一个Boss,玩家需要看到Boss身上的Buff信息,所以就有必要将GE同步到模拟端,方便获取Buff的信息,这个时候可以通过在Pawn身上创建一个同步的数据结构,将有需要的数据同步到模拟端;当然还有其他的解决方案,这只是我们可以使用的一种解决方案。
GameplayCueManager的配置来优化游戏内占用的内存。
默认情况下,GampeplayCue(GC)是默认全部加载的,这将导致我们配置在GC上的资源引用(音效,粒子等)全部加载到内存中,无论我们现在是否需要。在我们游戏中就发现内存多了好几百兆的GameCue资源。优化方案就是关闭启动时异步加载GameCueActor,这样游戏中就会在真正使用GC的时候才异步加载对应的对象,能有效的减少不必要的内存开销。这里有个小问题,第一次异步加载的时候如果触发的Buff存在时间很小会导致 OnRemove先于OnActive执行,例如我们在OnActive中创建一个特效,在OnRemove中删除这个特效,由于上述问题,就会导致特效残留。我们的解决方案是 在GameplayCueActor上重载HandleGameplayCue函数,添加判定是否第一次执行,如果是第一次执行并且先收到了OnRemove事件,就先不执行,等待OnActive之后,再次执行一次OnRemove,保证执行循序正确。
五、总结
GameplayAbilitySystem是一款非常成熟的技能系统插件,并且非常好入门。GAS中涵盖了技能系统中最主要的几大部分,能轻松的帮助大家实现一套技能效果,满足大型多人在线的网络游戏的设计要求。
立即获取虚幻引擎!
获取全球最开放、最先进的创作工具。
虚幻引擎包罗万象,并提供完整的源代码访问权限,开箱即用,诚意十足。
立即开始