前言
最近在做自己项目的时候,遇到技能系统设计的一些技术选型问题。 考虑到自己这方面经验很少,因此我打算先学习一下业界的成熟解决方案。 Unity在这方面没有特别好的轮子,简单研究之后发现UE的Game Ability System在设计上和自己的原始想法比较接近,因此我决定深度学习一下UE5的GAS,查漏补缺地改进自己的设计,最终应用在自己的Unity项目中。
Note:这些只是我自己的主观总结,用于打开设计思路的,很多和引擎、网络同步相关的部分我都略过了。 我的主要参考资料是:GASDocumentation和Official Ducument
1. 总体介绍
- 适用场景:适用于Moba游戏或者RPG游戏,支持联网,开箱即用
- 成功案例:Paragon, Fortnite
- 框架模块:
- GameplayAbilities(基于等级的角色能力系统,支持资源消耗和冷却时间)
- Attributes(操纵Actor身上的属性数值)
- GameplayEffects(在Actor身上添加状态效果)
- GameplayTags(Actor身上的标签)
- GameplayCues(生成特效或音效)
- 上述所有内容的实例复制
- 多人游戏支持下列客户端预测:
- 技能激活
- 播放timeline
- 属性改变
- 添加GameTags
- 生成GameplayCues
- 角色的移动
2. GAS中的概念
2.1 Ability System Component
ASC
是一种Actor组件,它是GAS的核心。挂载了ASC
的Actor才可以使用GameplayAbilities
、拥有Attribute
或者接收GameplayEffects
。ASC
也管理了上述对象的生命周期、复制等等。
挂载ASC
的OwnerActor
不一定和它的表现类AvatarActor
是同一个Actor。对于MOBA游戏,OwnerActor
一般是PlayerState
类,而AvatarActor
一般是Character
类。OwnerActor
和AvatarActor
如果不是同一个Actor,则需要实现GetAbilitySystemComponent()
接口来实现相互通信。
ASC
保存了Actor获得的所有Gameplay Abilities
,遍历这些能力的时候,需要加锁ABILITYLIST_SCOPE_LOCK()
,以确保不会删除列表中的能力(自己实现技能系统的话,可能技能列表的删除需要延迟处理?)。
ASC
管理了GameplayEffects
、GameplayTags
、GameplayCues
的网络同步(Attributes
由AttributesSet
同步)。有三种同步模式Full
, Mixed
和Minimal
。在单机、多人,以及是玩家操控还是AI操控Actor,使用的同步模式不同。
2.1 GameplayTags
GameplayTags
是一些带有层级结构的名称的集合,以FName
储存。例如:State.Debuff.Stun
表示一个被晕眩的debuff。通过布尔逻辑就能知道物体是否带有某些tag。出于效率原因,tag的集合一般用GameplayTagContainers
而不是TArray<FGameplayTag>
,因为FName
的复制有冗余,前者有做优化。UE5需要通过ini文件预先定义所有的tags,可以在UE5编辑器里配置与编辑tags。
GameplayTags
如果是由GameplayEffect
添加的话,会是自动复制的。ASC
可以添加和删除另一种LooseGameplayTags
,它不会被复制,因此这种情况下,需要手动管理LooseGameplayTags
。例如角色的死亡,需要客户端本地立即处理,可以手动添加State.Dead
的LooseGameplayTags
;角色重生之后再手动删除这个LooseGameplayTags
。
当ASC
添加或者移除GameplayTag
时,会有回调事件(可以同时添加多个同名tag到一个ASC
,返回的变化后的TagCount):
AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewTagCount);
2.2 Attributes
-
Attributes
的定义:Attributes
储存float类型的数据,用来表示游戏里与某个Actor相关的各种数值(等级、生命值、法力值etc)。一般来说Attributes
只能由GameplayEffects
来修改,这样ASC
才能预测其改变(网络同步相关)。Attributes
存在于AttributeSet
之中,由其定义、复制(只有标记了可复制的才会复制)。 -
Attributes
由BaseValue
和CurrentValue
两个值构成。BaseValue
储存的是基础值,CurrentValue
储存的是基础值经过GameplayEffects
的临时修改之后的值。例如角色基础移速600,BaseValue
和CurrentValue
在没有buff的情况下都是600;这时候如果有个加速buff,那BaseValue
还是600,CurrentValue
就是650了。BaseValue
的永久改变来源于Instant GameplayEffects
,而Duration GameplayEffects
和Infinite GameplayEffects
则只会改变CurrentValue
。 -
Meta Attributes
:一类需要与其他属性交互的临时Attributes
。例如伤害值一般是Meta Attributes
,GameplayEffect
不会直接改变我们的生命值,而是改变这个临时的伤害值属性;伤害值属性也能被buff和debuff影响,也能被AttributeSet
中的其他属性影响(例如护甲值)。Meta Attributes
一般不会被标记为需要复制。Meta Attributes
的引入可以实现逻辑分离,Gameplay Effects
不需要知道目标如何处理伤害,不同目标可能有不同的AttributeSet
,例如有的没护甲属性,有的有护甲属性,它们可以有不同的处理伤害的方式。 - 属性变化也有回调事件:
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged); virtual void HealthChanged(const FOnAttributeChangeData& Data);
FOnAttributeChangeData
包含OldValue
、NewValue
Derived Attributes
(派生属性):派生属性一般是由一个或多个Attributes
,通过一个Infinite GameplayEffect
(拥有多个Attribute Based
或者MMC Modifiers
)派生出来的。当Attributes
的值改变时,相应的Derived Attributes
的值会自动更新。
2.3 Attribute Set
ASC
通过AttributeSet
定义、管理Attributes
。一个ASC
可以有一个或多个AttributeSet
。AttributeSet
的开销是微不足道的,所以开发者可以比较自由地组织Attributes
,既可以使用一个全局通用的AttributeSet
,所有Actor都使用其中的某些属性;也可以不同类型Actor使用不同的AttributeSet
。
AttributeSet
可以通过一个instant GameplayEffect
来初始化属性(推荐做法)。
PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
是Attribute
的CurrentValue
改变之前的回调,这里适合对最终值做一些clamp。
PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
只会在BaseValue
被Instant GameEffect
改变之后被触发。例如,伤害属性在设计上是一个Meta Attribute
,可以监听它的这个事件,在这里去做扣除生命值、护甲的逻辑,以及受击动画、伤害飘字等等逻辑。
OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)
会在新的Aggregator
创建时触发。Aggregator
会基于所有的Modifier
和属性的BaseValue
去计算属性的CurrentValue
。例如,减速buff会创建负面的Modifier
,在所有减速Modifier
中,只想减速程度最大的Modifier
生效,这个时候就需要在Aggregator
创建这个回调里,设置Aggregator
的参数。
2.4 Gameplay Effects
GameplayEffects(GE)
是Ability改变Attributes
和GameTags
的一种载体。GE
既可以造成立即的属性值改变(例如受伤、治疗),也可以添加长期的状态(例如眩晕、加速buff)。一个GE
类只包含数据,不包含逻辑,一个游戏里一般有非常多的GE
派生类。
GE
通过Modifiers
和Executions
改变属性;GE
有三种持续类型:Instant
, Duration
和Infinite
。另外,GE
也可以添加或执行GameplayCues
。Instant GE
会调用GameplayCues
的Execute
事件,用于BaseValue
的立即修改,不能添加GameTag
;而Duration GE
和Infinite GE
会调用GameplayCues
的Add/Remove
事件,用于CurrentValue
的临时修改,也可以添加GameTag
,二者区别在于是否会expire。
Duration
或者Infinite
类型的GE
还可以添加Periodic Effects
,周期性地应用Modifiers
和Executions
。Periodic Effects
被视为Instant GE
,可以改变BaseValue
、执行GameplayCues
,它对于制作DOT(Damage Over Time)类型的伤害效果很有用。
GE
临时的开和关可以由GameplayTags
控制,而不需要删掉这个GE
的实例。关闭GE
时可以添加一个关闭的GameplayTag
,移除Modifiers
;打开GE
可以重新激活Modifiers
,移除相应GameplayTag
。
GE
是一般是不会被实例化的。当一个ASC
想要添加一个GE
时,会从GE
的ClassDefaultObject
创造一个GameplayEffectSpec
(可以看作GE
的实例)并添加到ASC
的ActiveGameplayEffects
中。
添加或移除GE
有很多的接口,但最终都是走到Target
的ASC
的ApplyGameplayEffectToSelf
和RemoveActiveEffects
函数。
可以如下添加Duration GE
和Infinite GE
的Add/Remove事件监听:
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);
virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);
Modifiers
可以改变Attribute
。一个GE
可以有零个或多个Modifiers
。每个Modifier
负责通过特定的操作去改变它对应的那个Attribute
。这些操作包含Add
、Multiply
、Divide
、Override
。CurrentValue
就是将BaseValue
通过所有的Modifiers
聚合之后的结果。默认情况下,同一类的Modifier
先累加,再通过公式聚合,聚合公式为((InlineBaseValue + Additive) * Multiplicitive) / Division
。Override
会直接覆盖上一个Modifier
的结果。
有四种Modifiers
,它们都能生成一个float类型的值,用于改变Attribute
。Scalable Float
可以通过读表,将技能等级映射到一个float值;Attribute Based
可以取GE
的Source
(创建者)或者Target
(目标)的某个属性的CurrentValue
或BaseValue
;Custom Calculation Class
是通过自定义的办法生成float,是最自由的一种方法;SetByCaller
,由GE
的创建者提供值的一种方法。
GE
会捕获source ASC
(创建时)和target ASC
(应用时)的GameplayTags
。Modifiers
也可以根据这些tag决定要不要apply。
GE
默认情况下是创建新的GameplayEffectSpec
实例。但是也可以选择堆叠的形式,改变现有的GameplayEffectSpec
的堆叠层数,而不创造新实例。堆叠只对Duration GE
和Infinite GE
生效。有两种堆叠类型,Aggregate by Source
会为每一个Source
创造一个堆叠实例,各自最多叠加X层;Aggregate by Target
则是所有Source
共享一个堆叠实例,一起最多叠X层。
Duration GE
和Infinite GE
可以赋予新的GameplayAbilities
到ASC
上。常见的用法是,例如当你想强制另一个玩家受到击退或者拉近效果,可以添加一个GE
,赋予他们一个自动激活的GameplayAbilities
,达到强制位移效果。一个GE
可以控制赋予何种能力、赋予能力的等级,以及GE
从Target
移除时赋予的能力的处理。有三种处理方式:立即移除能力;允许能力结束后再移除;不再控制能力,交予Target
控制。
GE
有几种tag分类,每一类都有对应的一个tag容器,用于不同的目的。Gameplay Effect Asset Tags
不做任何事,只是用来描述GE
的;Granted Tags
会在GE
添加到ASC
时,给它添加容器里的所有tag,在GE
移除时移除这些tag;Ongoing Tag Requirements
只有当里面所有tag都满足时,GE
才会激活,否则会失效,待到条件满足会重新激活;Remove Gameplay Effects with Tags
当GE
被赋予到ASC
时,会移除ASC
上所有Granted Tags
或Asset Tags
里包含这个目录里tag的GE
。
GESpec
是GE
的实例化,其中包括了指向对应GE
的指针、创建时的等级、创建它的ASC
等等。GESpec
不一定是在创建之后立马被赋予到Target
上,例如一个投射物可以被赋予一个GESpec
,当击中物体时再向目标赋予这个GESpec
。一个GESpec
包含的内容:
- 创建它的
GE
的引用 - 它的等级,通常与
GE
相同,但也可以不同 - 它的持续时间,通常与
GE
相同,但也可以不同 - 它的
periodic effects
的发生周期,通常与GE
相同,但也可以不同 - 当前堆叠层数。最大堆叠层数在
GE
类上 GameplayEffectContextHandle
,告诉我们谁创建的GESpec
GESpec
创建时的属性的快照SetByCaller
TMaps
,保存由外部传入的Map(Tag映射到float),再用于Modifiers
或者别的用途
ModifierMagnitudeCalculations
(MMC
) 是GE
中一种功能强大的Modifier
。它的能力比Execution
弱,但是好处在于可以预测。它可以捕获Source
或Target
的属性(可以是快照版本也可以是实时版本),可以获取SetByCallers
的数据,并用这些数据计算属性的CurrentValue
。
Gameplay Effect Execution Calculation
(Execution
)是GE
改变ASC
最强大的一种方式。它也能像MMC
一样捕获属性,但它可以改变ASC
的任意属性,不仅仅只是某个属性。因此程序员几乎能用它完成任意想要的功能。权衡处就在于灵活性高,但是不可预测了。
CustomApplicationRequirement
(CAR
)是可以用来自定义GE
应用规则的类。默认的GE
应用限制只有GameTag的限制。例如需要新增一种当目标身上某些GE
的堆叠层数达到N层才激活的GE
,可以从这里扩展。
GameplayAbilities
(GA
)可以通过特定的GE
来确定激活时的资源消耗。这个GE
需要是Instant GE
,有一个或多个Modifiers
来计算资源Attribute
的消耗值。
GA
也可以通过特定的GE
来实现能力的冷却。这个Cooldown GE
需要是一个没有Modifiers
,并且对于每一种能力(如果游戏支持能力切换,并且能力槽共享cd,则是能力槽)在GrantedTags
中有一个特殊的GameTag
(Cooldown Tag
)的Duration GE
。GA
正是通过检查Cooldown Tag
的存在而不是Cooldown GE
的存在来确定是否能力正在冷却的。
2.5 Gameplay Abilities
GameplayAbilities
(GA) 是游戏里Actor
可以使用的技能。同一时间可以同时激活多个GA
,例如冲刺的同时可以射击。常见的GA
:跳跃、冲刺、设计、每X秒免疫一次伤害、使用药水、打开一扇门、收集资源、建造建筑。GA
具有等级,可以通过改变GA
等级改变GA
的数值或功能。GA
使用AbilityTasks
处理持续一段时间的动作,例如监听某个事件、等待属性改变、等待玩家选择目标等等。
一个简单点的GA
流程如下:
一个复杂一些的GA
流程如下:
更复杂的GA
可以由使用不同的GA
进行组合、交互来实现
GA
的升级有两种,一种是移除原来的GA
的实例,再重新赋予新的等级的GA
;一种是直接改原来GA
实例的等级。二者流程上有点区别,取决于需求。
2.6 Ability Tasks
GA
只在一帧里执行,当我们想要响应一段时间内的事件时,我们需要AbilityTasks
。
例如:
- 移动
Character
- 播放timeline
- 响应
Attribute
- 响应用户输入
- and more
例如一种多重护甲技能,当受到伤害减少一层护甲层数,就需要新增AbilityTasks
去监听Actor
受伤的事件。像击退、冲刺、拉取这样的是用一些影响移动组件的AbilityTasks
来实现的
2.7 Gameplay Cues
GameplayCues
(GC) 执行的是与gameplay无关的逻辑,例如声效、粒子特效、相机震动等。 GameplayCues一般是从远程同步的(除非是特意由本地管理的)。
3. 常见能力的实现
3.1 眩晕
眩晕时,我们一般取消Character
的所有主动类型的GameplayAbilities
,阻止新的主动GameplayAbility
的激活,并且在眩晕的持续过程中,阻止角色移动。
对于取消GameplayAbilities
,我们可以在眩晕时调用ASC
的AbilitySystemComponent->CancelAbilities()
,再添加眩晕的GameTag
。为了阻止能力在眩晕时激活,我们可以在阻止这些能力激活的GameTag
类别里添加眩晕的GameTag
。为了阻止移动,我们可以在角色移动组件的GetMaxSpeed()
里当存在眩晕标签时返回0。
3.2 吸血
可以在伤害的Execute
流程里计算吸血。如果GE
的GameTag
存在Effect.CanLifesteal
,那么Execution
会计算吸血量,构造一个Instant GE
,将这个值赋予到Source
的ASC
上。