通过一个简单的多人示例项目分享我对UE4中GAS插件的理解。 由于这不是官方文档,示例项目和我都不是来自Epic Games。因此我并不能保证描述的准确性。(译注:本人才疏学浅,还请大家多多指教)
这个文档的主要目的是讲解GAS的主要概念和其中的一些类,同时分享一些我的使用经验。
当前文档和项目基于Unreal Engine 4.25。
GASShooter是此文档的姊妹篇,主要通过多人FPS/TPS项目演示GAS的一些高级应用。
当然,最好的文档是GAS源代码本身(Plugins\Runtime\GameplayAbilities)。
来自 官方文档:
Gameplay技能系统 是一个高度灵活的框架,可用于构建你可能会在RPG或MOBA游戏中看到的技能和属性类型。你可以构建可供游戏中的角色使用的动作或被动技能,使这些动作导致各种属性累积或损耗的状态效果,实现约束这些动作使用的“冷却”计时器或资源消耗,更改技能等级及每个技能等级的技能效果,激活粒子或音效,等等。简单来说,此系统可帮助你在任何现代RPG或MOBA游戏中设计、实现及高效关联各种游戏中的技能,既包括跳跃等简单技能,也包括你喜欢的角色的复杂技能集。
GAS是由虚幻官方(Epic Games)开发的虚幻4引擎自带的一个插件,已被应用于Paragon和Fortnite中。
GAS插件提供了对于无论是单人还是多人游戏来说拆箱即用的解决方案:
在多人游戏中,GAS同时提供了客户端预测的支持:
CharacterMovementComponent
的移动GAS必须在C++项目中才可使用,不过GameplayAbilities
和GameplayEffects
能够被设计师通过蓝图创建。
GAS当前的问题和使用的一些难处:
GameplayEffect
延迟和解 ( can’t predict ability cooldowns resulting in players with higher latencies having lower rate of fire for low cooldown abilities compared to players with lower latencies)。GameplayEffects
的删除。不过我们可以通过添加一个反效果的GameplayEffects
变相的实现此需求。当然这并不总是可行,这仍然是GAS的一个问题。通过一个多人的第三人称射击游戏演示文档中的内容,读者可以没有GAS基础,但需要有虚幻引擎的使用基础。比如C++,蓝图,UMG,网络同步等。这个示例项目通过创建两种类型的角色演示了如何使用GAS创建FPS多人游戏,其一:为PlayerState
添加AbilitySystemComponent
(后面简称为ASC
)用以实现玩家和AI控制的英雄角色,其二:直接将ASC
添加给Character
用以创建AI控制的小怪或杂兵。
本文档主要讲解GAS的基本概念和基础实践,并不会包括高级主题比如可预测的炮弹。
具体涉及:
ASC
应该属于PlayerState
还是Character
Attributes
GameplayTags
GameplayEffects
GameplayEffectExecutionCalculations
GameplayEffects
GameplayAbilities
GameplayAbilities
GameplayAbilities
GameplayAbilities
(Jump)GameplayCues
(FireGun projectile impact particle effect)GameplayCues
(Sprint and Stun particle effects)英雄角色有如下技能:
技能 | 绑定按键 | 可预测 | C++ / Blueprint | 描述 |
---|---|---|---|---|
Jump | Space Bar | Yes | C++ | 跳跃 |
Gun | Left Mouse Button | No | C++ | 射击, 动画支持预测炮弹不支持 |
Aim Down Sights | Right Mouse Button | Yes | Blueprint | 瞄准,角色将降低移动速度 |
Sprint | Left Shift | Yes | Blueprint | 冲刺,冲刺过程中会持续消耗耐力值 |
Forward Dash | Q | Yes | Blueprint | 闪冲,一次性消耗耐力值 |
Passive Armor Stacks | Passive | No | Blueprint | 每4秒可获取一个护甲的被动技能,最多4层,每次受伤掉一层护甲 |
Meteor | R | No | Blueprint | 流星技能,范围伤害,同时可以击晕目标。 目标选取是可预测的,砸下来的流星不是 |
C++或者蓝图创建GameplayAbilities
皆可,示例中会以这两种方式为例说明各自用法。
示例中的小怪没有任何技能,木桩而已。红色的小怪有回血BUFF,蓝色的小怪初始血量高。
关于GameplayAbility
的命名,带有_BP的GameplayAbility
是由蓝图创建,不带的是由C++创建。
蓝图资产命名前缀
Prefix | Asset Type |
---|---|
GA_ | GameplayAbility |
GC_ | GameplayCue |
GE_ | GameplayEffect |
使用GAS的基本步骤:
YourProjectName.Build.cs
添加"GameplayAbilities", "GameplayTags", "GameplayTasks"
到PrivateDependencyModuleNames
UAbilitySystemGlobals::InitGlobalData()
才能使用TargetData
。示例项目在UEngineSubsystem::Initialize()
中调用InitGlobalData
。详见InitGlobalData()
这就是启用GAS的全部步骤。下面将为Character
或者 PlayerState
添加ASC
和AttributeSet
并开始创建 GameplayAbilities
和GameplayEffects
!
AbilitySystemComponent
(ASC
)是整个技能系统的心脏。 ASC
本质上是一个UActorComponent
(UAbilitySystemComponent
) 用于处理技能系统中的所有交互。任何希望使用Abilities
或者想要包含Attributes
或者想要接收GameplayEffects
的Actor
必须拥有一个ASC
。 这些对象存在于、被管理于、被复制于ASC
(Attributes
的复制除外,其复制由 AttributeSet
完成)。开发者可以子类化ASC
,但这并不是必须的。
带有ASC
的Actor
也被称为ASC
的OwnerActor
。ASC
实际作用的Actor
叫作AvatarActor
。OwnerActor
和AvatarActor
可以是同一个Actor
,比如MOBA游戏中的野怪。它们也可以是不同的 Actors
,比如MOBA游戏中玩家和AI控制的英雄角色,OwnerActor
是PlayerState
、AvatarActor
是HeroCharacter
。大部分情况下OwnerActor
和AvatarActor
可以是角色Actor
。不过想像一下你控制的英雄角色死亡然后重生的过程,如果此时要保留死亡前的Attributes
或者GameplayEffects
,那么最理想的做法是将ASC
交给PlayerState
。
注意: 如果你将ASC
给了PlayerState
,那么你需要增加PlayerState
的网络更新频率NetUpdateFrequency
。 由于PlayerState
默认的更新频率非常低,会导致 Attributes
and GameplayTags
的同步延迟。确保启用 Adaptive Network Update Frequency
, Fortnite用了这个。
如果OwnerActor
和AvatarActor
是不同的Actors
,那么两者都需要实现IAbilitySystemInterface
。这个接口只有一个方法需要被重载UAbilitySystemComponent* GetAbilitySystemComponent() const
,此方法将返回ASC
。
ASC
持有当前活动的GameplayEffects
,详见FActiveGameplayEffectsContainer ActiveGameplayEffects
。
ASC
持有赋予的Gameplay Abilities
,详见 FGameplayAbilitySpecContainer ActivatableAbilities
。确保迭代ActivatableAbilities.Items
时一定要在迭代之前添加ABILITYLIST_SCOPE_LOCK();
。在ABILITYLIST_SCOPE_LOCK();
的过程中更不要删除Ability
。
ASC
提供了三种不同的复制模式,用以复制GameplayEffects
、GameplayTags
和 GameplayCues
,分别是Full
, Mixed
, 和 Minimal
。Attributes
是由 AttributeSet
复制。
复制模式 | 使用场景 | 描述 |
---|---|---|
Full |
单人 | GameplayEffect 会被复制到所有客户端。 |
Mixed |
多人,玩家控制的Actors |
GameplayEffects 仅被复制到拥有者的客户端. 仅 GameplayTags 和 GameplayCues 会被复制到所有客户端 |
Minimal |
多人, AI控制的Actors |
GameplayEffects 不会复制到任何客户端. 仅 GameplayTags 和 GameplayCues 会被复制到所有客户端 |
注意: Mixed
复制模式要求OwnerActor
的 Owner
必须是Controller
。 PlayerState
的 Owner
默认是Controller
,但是Character
不是。如果使用Mixed
复制模式的OwnerActor
不是PlayerState
那么你需要在OwnerActor
上调用SetOwner()
并传递一个有效的Controller
。(不过从4.24开始, PossessedBy()
会为Pawn
设置一个新的Controller
。)
ASCs
通常在OwnerActor
的构建方法中创建并且显示的标记复制(Replicated)。 这一步必须在C++中完成。
AGDPlayerState::AGDPlayerState()
{
// Create ability system component, and set it to be explicitly replicated
AbilitySystemComponent = CreateDefaultSubobject(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
//...
}
ASC
需要有OwnerActor
和AvatarActor
进行初始化,而且必须在服务器和客户端都要完成初始化。
对于玩家控制的角色,ASC
存在于Pawn
中,我通常在Pawn
的 PossessedBy()
方法中完成ASC
在服务器端的初始化,在PlayerController
的AcknowledgePawn()
方法中完成ASC
在客户端的初始化。
void APACharacterBase::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
// ASC MixedMode replication requires that the ASC Owner's Owner be the Controller.
SetOwner(NewController);
}
void APAPlayerControllerBase::AcknowledgePossession(APawn* P)
{
Super::AcknowledgePossession(P);
APACharacterBase* CharacterBase = Cast(P);
if (CharacterBase)
{
CharacterBase->GetAbilitySystemComponent()->InitAbilityActorInfo(CharacterBase, CharacterBase);
}
//...
}
对于玩家控制的角色,ASC
存在于PlayerState
中,我通常在Pawn
的PossessedBy()
方法中完成ASC
在服务器端的初始化(这一点与上述相同),在 Pawn
的 OnRep_PlayerState()
方法中完成ASC
在客户端的初始化(这将确保PlayerState
在客户端已存在)。
// Server only
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
AGDPlayerState* PS = GetPlayerState();
if (PS)
{
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
AbilitySystemComponent = Cast(PS->GetAbilitySystemComponent());
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
}
//...
}
// Client only
void AGDHeroCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
AGDPlayerState* PS = GetPlayerState();
if (PS)
{
// Set the ASC for clients. Server does this in PossessedBy.
AbilitySystemComponent = Cast(PS->GetAbilitySystemComponent());
// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
}
// ...
}
如果你看到如下日志LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local!
说明你没有在客户端初始化ASC
。
FGameplayTags
是一系列层次化的名字,如Parent.Child.Grandchild...
这种格式。这些名字通过GameplayTagManager
进行注册。 这些标签对于描述和归类一个对象的状态非常有用。例如,如果角色处于眩晕状态,我们可以给它一个State.Debuff.Stun
的 GameplayTag
在整个眩晕的过程中。
你会发现自己用GameplayTags
替换了以前用布尔值或枚举处理的东西,并对对象是否具有某些GameplayTags
进行了布尔逻辑。
为对象赋予标签,我们通常将标签添加到对象拥有的ASC
中,这样GAS就能与标签交互。UAbilitySystemComponent
实现了IGameplayTagAssetInterface
接口中的方法以便访问它拥有的GameplayTags
。
多个GameplayTags
可以被存储到FGameplayTagContainer
中。强烈建议使用GameplayTagContainer
而不是TArray
,因为GameplayTagContainers
添加了一些例其高效的魔法。 标签是标准的FNames
,在FGameplayTagContainers
中他们可以被高效的打包在一起以完成网络复制,当然需要先在项目设置中开启Fast Replication
。Fast Replication
要求服务器和客户端拥有相同的GameplayTags
列表。为了遍历GameplayTagContainers
也可以返回一个TArray
。
GameplayTags
存储在 FGameplayTagCountContainer
里有一个TagMap
,存储了GameplayTag
实例的数量。FGameplayTagCountContainer
可以有一个GameplayTag
但是TagMapCount
是0。任何HasTag()
或HasMatchingTag()
或其他类似的方法都会检查TagMapCount
,如果GameplayTag
不存在或者TagMapCount
等于0将返回false。
GameplayTags
需要在DefaultGameplayTags.ini
中提早定义。 虚幻4的编辑器在项目设置中提供了一个界面可以让开发者管理GameplayTags
而不需要手动编辑DefaultGameplayTags.ini
。GameplayTag
编辑器可以创建、重命名、删除GameplayTags
,也可以查找标签的引用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yJOhlMIH-1591700466338)(https://github.com/tranek/GASDocumentation/raw/master/Images/gameplaytageditor.png)]
查找GameplayTag
的引用将打开一个类似Reference Viewer
的界面,显示引用GameplayTag
的全部资源(不包括C++)。
重命名GameplayTags
将会创建一个重定向,相关资源仍然引用原始的GameplayTag
,GameplayTag
将会被定向到新的GameplayTag
。我推荐创建一个新的GameplayTag
, 然后手动更新所有引用到这个新GameplayTag
,然后删除旧的GameplayTag
,这样可以避免重定向。(译者注:前面多花精力想好结构和名字,这玩意尽可能的不要在后面改)
另外,当开启Fast Replication
后,GameplayTag
编辑器可配置进一步优化GameplayTags
的网络复制。
由GameplayEffect
添加的GameplayTags
会被复制。 ASC
也可以添加不会被复制并且需要手动管理的LooseGameplayTags
。示例项目使用LooseGameplayTag
处理State.Dead
标签,因此当HP为0的时候所属客户端可以立即响应。重生时需要手动将TagMapCount
设置为0。当使用LooseGameplayTags
时仅需设置TagMapCount
。建议使用UAbilitySystemComponent::AddLooseGameplayTag()
和 UAbilitySystemComponent::RemoveLooseGameplayTag()
方法设置TagMapCount
。
在C++中获取GameplayTag
的引用:
FGameplayTag::RequestGameplayTag(FName("Your.GameplayTag.Name"))
对于获取GameplayTag
的父或子标签这类处理,可以通过GameplayTagManager
中的一系列方法完成。要使用 GameplayTagManager
, 首先include GameplayTagManager.h
然后调用 UGameplayTagManager::Get().FunctionName
即可。 GameplayTagManager
实际上以关系节点的方式存储了 GameplayTags
速度上要远优于字符串的处理和比较。
GameplayTags
和GameplayTagContainers
有可选的 UPROPERTY
说明符Meta = (Categories = "GameplayCue")
,可以用来在蓝图中实现标签的过滤,仅展示出属于父标签为GameplayCue
的GameplayTags
。 要实现此功能也可以通过直接使用 FGameplayCueTag
其内部封装了一个带有Meta = (Categories = "GameplayCue")
的 FGameplayTag
。
当把 GameplayTag
当作方法的参数时,可以通过 UFUNCTION
specifier Meta = (GameplayTagFilter = "GameplayCue")
完成过滤。(译者注:GameplayTagContainer
也已经支持Filter,不再赘述)
示例项目广泛的使用了 GameplayTags
。
ASC
提供了 GameplayTags
添加和删除的委托。可以通过 EGameplayTagEventType
枚举指明要监听 GameplayTag
的添加和删除还是任何关于GameplayTag
的TagMapCount
变化。
AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);
委托的回调方法会带有相关的 GameplayTag
和新的 TagCount
。
virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);
Attributes
是由 FGameplayAttributeData
定义的浮点值。 Attributes
能够表达从角色的生命值到角色等级到药瓶的价格等任何数值。 如果Actor拥有游戏性相关的数值,那么可以考虑使用Attribute
。Attributes
通常只能被GameplayEffects
修改,因此ASC
可以 预测 这个修改。
Attributes
被定义并且存活在AttributeSet
中。 AttributeSet
也会负责处理 Attributes
的复制。如何定义Attributes
详见 AttributeSets
。
提示: 如果你不想要Attribute
显示在编辑器的属性详情中,可以使用 Meta = (HideInDetailsView)
属性说明符。
一个Attribute
由两个值构成 - 一个基值 BaseValue
和一个当前值CurrentValue
. 基值BaseValue
是属性 Attribute
的一个恒值, 而当前值 CurrentValue
是 BaseValue
加上GameplayEffects
的临时修改值。 例如,你的角色有个移动速度movespeed
的属性Attribute
其BaseValue
为600 单位/秒。由于没有任何GameplayEffects
修改movespeed
,所以其CurrentValue
也是600单位/秒。如果角色获取了一个50单位/秒的速度加成(BUFF),BaseValue
仍然保持在600单位/秒,而CurrentValue
将等于650单位/秒=600 + 50。当移动速度加成BUFF过期后,CurrentValue 将恢复成
BaseValue` 600单位/秒。
通常刚接触GAS
的新手会将BaseValue
理解为或当作是一个属性的最大值。这是不正确的, 能够被技能或者UI使用的Attribute
的最大值应该是另一个单独的Attribute
。 对于硬编码的最大值和最小值,可以通过FAttributeMetaData
的DataTable
定义,其可以设置最大值和最小值,但Epic注意这个结构体"work in progress"。详见AttributeSet.h
。 为了清除困惑,强烈建议用于技能或者UI上的最大值Attribute
是一个单独的Attribute
,并且FAttributeMetaData
中的最大值和最小值仅用于属性值的限定(Clamping)。CurrentValue
的属性值限定将会在PreAttributeChange()谈论,BaseValue
的属性值限定将会在PostGameplayEffectExecute()讨论,其执行由GameplayEffects
触发。
立即(Instant
) GameplayEffects
将永久改变BaseValue
,而持续(Duration
) 和永恒(Infinite
) GameplayEffects
将改变CurrentValue
。周期性(Periodic
)GameplayEffects
像立即(Instant
) GameplayEffects
一样将改变BaseValue
有一些Attributes
会被当作仅用于与其他Attributes
交互的临时值来使用,这种属性被称作元属性( Meta Attributes
)。例如,我们通常定义伤害值(damage
)为元属性,而不是直接在 GameplayEffect
中修改生命 Attribute
。这样damage
的值 可以在Buffs和Debuffs的 GameplayEffectExecutionCalculation
中修改,也可以在 AttributeSet
中被进一步处理,例如让damage
减掉当前的护甲 Attribute
, 最后让生命值 Attribute
减去damage
即可。 这个 damage
元属性并不持久在每一次的GameplayEffects
其值都会被覆盖,Meta Attributes
通常也不会被网络复制。
Meta Attributes
为伤害和治愈这种属性( “造成了多少伤害?”,“用这个伤害做什么?”)提供了一个好的逻辑分离方案。逻辑分离意味着我们的Gameplay Effects
和 Execution Calculations
不需要关心目标如何处理damage
。继续我们的damage
示例,Gameplay Effect
决定了damage
是多少,然后AttributeSet
决定了如何使用damage
。并不是所有角色都有相同的Attributes
,尤其是当子类化AttributeSets
时。基类 AttributeSet
可能只拥有一个生命值的Attribute
,但其子类AttributeSet
可能添加了一个护盾的Attribute
。那么这两个AttributeSets
对于damage
的处理肯定不同。
Meta Attributes
是一个好的设计模式,当然这并不代表一定要使用Meta Attributes
。如果你仅有一个Execution Calculation
用于处理所有damage
并且仅有一个 Attribute Set
类用于所有角色,那么你可以直接在Exeuction Calculation
中修改生命值,护盾等属性。这样做也是可行的,但会牺牲掉灵活性。
要监听 Attribute
的变化以更新UI或者做其他事情,可以使用UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)
。这个方法会返回一个委托,可以为特定属性绑定一个回调方法,当属性改变会自动执行这个方法。这个委托会提供一个FOnAttributeChangeData
参数,带有NewValue
, OldValue
和FGameplayEffectModCallbackData
。 Note: FGameplayEffectModCallbackData
仅在服务器端有效。
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
virtual void HealthChanged(const FOnAttributeChangeData& Data);
示例项目在GDPlayerState
中绑定了Attribute
值改变的委托用以更新HUD和当生命值为0时响应死亡。
在示例项目中还包含了一个使用异步任务(AsyncTask)处理Attribute
委托回调的自定义蓝图节点。它被用在UI_HUD
UMG Widget中用来更新生命值,法力值,体力值。异步任务AsyncTask
会永远存活直到调用EndTask()
, 我们会在 UMG Widget的 Destruct
事件中调用,详见AsyncTaskAttributeChanged.h/cpp
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-46I977RZ-1591700466340)(C:\Users\X\Desktop\attributechange.png)]
要使某个属性依据其他属性进行更新,可以使用永恒(Infinite)的GameplayEffect和基于属性或MMC(Custom Calculation Class
)的修改器。在其他属性变化时这个属性将会自动更新。这个属性叫作推导属性(Derived Attributes)
。
推导属性
上所有修改器
的最终公式与修改器聚合器(Modifier Aggregators)
的公式相同。如果需要按一定顺序进行计算,可以在MMC
内部完成所有操作。
((CurrentValue + Additive) * Multiplicitive) / Division
注意: 在PIE中运行多个客户端实例时,一定要在Editor Preferences
中禁用Run Under One Process
。否则除第一个客户端之外的客户端将不会更新`推导属性。
示例,我们有一个 Infinite
GameplayEffect
,其根据属性TestAttrB
和TestAttrC
推导(计算)并更新属性TestAttrA
的值, 计算公式如下:
TestAttrA = (TestAttrA + TestAttrB) * ( 2 * TestAttrC)
在TestAttrB
和TestAttrC
发生变化时TestAttrA
的属性值将根据上述公式重新计算
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5GTWDac1-1591700466341)(https://github.com/tranek/GASDocumentation/raw/master/Images/derivedattribute.png)]
AttributeSet
负责定义和持有属性
并且管理属性
的变化。开发者可以子类化UAttributeSet
。在OwnerActor
的构造方法中创建的AttributeSet
将会自动注册到ASC
。这一步必须在C++中完成。
一个ASC
可以拥有一个或多个AttributeSets
。属性集的内存开销是微不足道的,因此要使用多少属性集完全由开发者决定。
在游戏中所有Actor
共享一个巨大的AttributeSet
也是可行的,每个Actor
仅使用需要的属性即可。
或者,你也可以使用多个AttributeSet
对Attributes
进行分组,然后根据Actors
的需要进行有选择添加。例如,可以创建一个与生命值属性有关的AttributeSet
,再创建一个与法力值属性有关的AttributeSet
,等等。在MOBA游戏中,英雄可能需要法力值,但小怪可能不需要。
另外,AttributeSets
可以被子类化,这也作为Actor
选择拥有哪些属性的另一种方式。Attributes
在内部以AttributeSetClassName.AttributeName
的方式引用。当你子类化AttributeSet
后,所有父类的属性也必须通过父类作为前缀引用(ParentClassName.AttributeName
)。
在一个ASC
中可以有多个不同的AttributeSet
,谨记因为上述的属性引用方式,所以同一个AttributeSet
在一个ASC
中最多只能有一个。
考虑一个场景,当Pawn
上有多个可被破坏的组件时(比如可被破坏的护甲),假设你已经知道了一个Pawn
可拥有护甲的最大数量,那么Pawn
可以有一个包含众多像DamageableCompHealth0
、DamageableCompHealth1
等属性的AttributeSet
,然后通过一些方法(logical slots
)将这些属性与护甲关联起来。在护甲组件中有个槽位数字,护甲受到伤害时可以通过这个槽位数字索引到能够被GameplayAbilities
或者Executions
处理的Attribute
。即使Pawns
拥有的护甲少于AttributeSet
中定义的属性数量也没关系,因为AttributeSet
中定义的未被使用的Attribute
只占用极小的内存开销。
如果你的子组件需要很多Attributes
,或者子组件的数量是未知的,再或者子组件会被卸载然后被其他玩家使用(比如武器)。总之不管什么情况当上述方案并不能满足需求时,那么我建议你在组件上直接使用float
并且远离Attributes
。详见 物品属性。
可以通过ASC
在运行时动态添加和删除AttributeSets
,当然,删除AttributeSets
是非常危险的。例如,如果一个AttributeSet
的删除在客户端先完成于服务器端时,恰巧AttributeSet
中的一个Attribute's Value
的修改被同步到客户端,Attribute
将无法找到所属的AttributeSet
这将导致游戏崩溃。
however, removing AttributeSets
can be dangerous. For example, if an AttributeSet
is removed on a client before the server and an Attribute
value change is replicated to client, the Attribute
won’t find its AttributeSet
and crash the game.
装备武器时:
AbilitySystemComponent->SpawnedAttributes.AddUnique(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
卸载武器时:
AbilitySystemComponent->SpawnedAttributes.Remove(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();
有多种方式可以实现带有属性(武器弹药、装甲耐久等等)的可装备物品,所有这些方式都是将值直接存储在物品上,对于能够被其他玩家装备和使用的物品这是必须的。方法如下:
- 在物品上使用
floats
(推荐)- 在物品上使用
AttributeSet
- 在物品上使用
ASC
floats
代替Attributes
,直接在物品实例上存储浮点值。堡垒之夜和GASShooter使用这种方式处理枪的弹药。对于一把枪,需要存储弹夹大小,弹夹弹药数量,储备弹药等可直接使用支持复制的浮点数(COND_OwnerOnly
)。如果储备弹药是在武器间共享的(换句话说储备弹药属于角色而不是武器),那么你可以为Character
添加一个带有储备弹药Attribute
的AttributeSet
。由于弹夹弹药数量没有使用Attributes
,所以你需要重载几个UGameplayAbility
的方法以检查和应用消耗(枪上的浮点值)。在授予Ability
时,需要将枪作为GameplayAbilitySpec
的SourceObject
才能在Ability
中访问枪的数据(译者注:读一下示例中如何实现的射击就理解这个了)
为了防止枪在自动射击时弹药复制会搞乱本地弹药数量,所以需要在PreReplication
(此方法仅在服务器执行)中判断当玩家射击时(IsFiring
GameplayTag
)禁止replication
,也可以在这里实现你自己的本地预测。
void AGSWeapon::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
Super::PreReplication(ChangedPropertyTracker);
DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, PrimaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, SecondaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
}
优势:
AttributeSets
的限制 (继续往下看)缺陷:
GameplayEffect
(比如Cost GEs
)UGameplayAbility
的方法才能检查和应用弹药消耗AttributeSet
在物品上使用单独的AttributeSet
,当玩家装备物品时将物品上的AttributeSet
动态添加到玩家的ASC
上是可行的,但也会带来一些问题。我在GASShooter早期的版本中为武器弹药使用这种方式。武器通过一个AttributeSet
存储了一些Attributes
,例如弹夹大小,弹夹弹药数量,储备弹药等。如果储备弹药是在武器间共享的,那么可以将所需属性(Reserve Ammo
)转移到角色身上。当在服务器端玩家装备武器时,武器的AttributeSet
将会被添加到玩家的ASC::SpawnedAttributes
中,然后服务器将此复制到客户端。如果玩家卸载武器,过程同上,只是由添加变成删除。
当AttributeSet
存在于非OwnerActor
(比如武器),然后在构造方法中初始化AttributeSet
时将会编译错误。解决办法是将其放在BeginPlay()
进行初始化,还要在武器上实现IAbilitySystemInterface
接口,在装备武器时设置ASC
的指针。
void AGSWeapon::BeginPlay()
{
if (!AttributeSet)
{
AttributeSet = NewObject(this);
}
//...
}
上述示例详见older version of GASShooter.
优势:
GameplayAbility
和GameplayEffect
的工作流(Cost GEs
)缺陷:
AttributeSet
,因为ASC
仅能有一个AttributeSetClass
的实例。(如果你能够同时装备两把武器,这两把武器又有相同的AttributeSet
,这个方案就无解了)AttributeSet
是非常危险的。上面解释过,不再赘述。ASC
为每个物品添加一个ASC
是一种极端的方案。我没有亲自实践过这种方案,也没见过。要实现这个方案可能需要大量的工程工作。
多个
ASCs
拥有相同的Owner
不同的Avatars
是否可行(比如pawn
和weapon/items/projectiles
的Owner
全设置为PlayerState
)?第一个问题,在
Owing Actor
上实现IGameplayTagAssetInterface
和IAbilitySystemInterface
。实现IGameplayTagAssetInterface
或许可能:仅汇总所有ASCs
中的标签(但请注意,HasAlMatchingGameplayTags
只能通过交叉ASC
聚合来满足)。但要实现IAbilitySystemInterface
会更棘手:哪一个ASC
才是权威的?如果要应用一个GE,哪一个ASC
会接收它?也许你可以解决这些问题,但Owner
拥有多个ASCs
才是最难处理的。在
pawn
和weapon
上有单独的ASCs
这很好理解。例如,区分描述weapon
的标签和描述owing pawn
的标签,也许应用在武器上的标签应用在拥有者上也是有意义的(例如属性和GEs是独立的,但拥有者将会聚合拥有的标签像我上面描述的)。我相信这可以解决,但相同的owner
拥有多个ASCs
会有很大的风险。
Dave Ratti from Epic’s answer to community questions #6
优势:
GameplayAbility
和GameplayEffect
的工作流(Cost GEs
)AttributeSet
Classes (因为每个武器都有自己的ASC
)缺陷:
Attributes
只能在C++ 的AttributeSet
头文件中定义。强烈建议在每个AttributeSet
头文件中定义下述宏,它将会为属性自动生成Getter和Setter。
// Uses macros from AttributeSet.h
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
可被复制(replicated
)的生命值的定义:
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Health)
同样需要在头文件中定义 OnRep
方法:
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
在AttributeSet
的.cpp文件中实现OnRep
方法,调用GAMEPLAYATTRIBUTE_REPNOTIFY
宏才能使用预测系统
void UGDAttributeSetBase::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UGDAttributeSetBase, Health, OldHealth);
}
最后, Attribute
需要被添加到GetLifetimeReplicatedProps
中:
void UGDAttributeSetBase::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
}
REPTNOTIFY_Always
告诉 OnRep
方法在本地值和服务器下发的值即使已经相同也会触发(为了预测),默认情况下OnRep
不会触发。
如果Attribute
不需要复制(像Meta Attribute
),那么OnRep
和GetLifetimeReplicatedProps
可以被跳过。
有多种方法可以初始化Attributes
(设置BaseValue
并因此让其CurrentValue
为某个初始值),Epic建议使用一个Instant GameplayEffect
,这也是示例项目中使用的方法。详见示例项目中的GE_HeroAttributes
蓝图(在C++中应用的这个GameplayEffect
)。
当定义Attributes
属性时使用了ATTRIBUTE_ACCESSORS
宏,它将会为每个属性自动生成一个初始化方法
// InitHealth(float InitialValue) is an automatically generated function for an Attribute 'Health' defined with the `ATTRIBUTE_ACCESSORS` macro
AttributeSet->InitHealth(100.0f);
更多初始化属性的方法详见AttributeSet.h
。
注意: 在4.24之前,FAttributeSetInitterDiscreteLevels
将不能和FGameplayAttributeData
一起工作。它会在属性是原始浮点数时创建, 会抱怨FGameplayAttributeData
不是Plain Old Data
(POD
)。4.24已经解决了这个问题 https://issues.unrealengine.com/issue/UE-76557。
PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
是AttributeSet
中一个主要的方法,当Attribute
的CurrentValue
被改变之前调用。对于让CurrentValue
保持在正确的范围这是个理想的地方。
示例中让movespeed保持在150-1000 units/s之间:
if (Attribute == GetMoveSpeedAttribute())
{
// Cannot slow less than 150 units/s and cannot boost more than 1000 units/s
NewValue = FMath::Clamp(NewValue, 150, 1000);
}
GetMoveSpeedAttribute()
是由宏创建(Defining Attributes)。
任何Attributes
的改变都会调用此方法,无论是使用Attribute
setters 还是使用GameplayEffects
。
注意: 此处的clamping
并没有永久地修改ASC
的modifier
,它仅改变了查询modifier
返回的值。这意味着任何修改器GameplayEffectExecutionCalculations
和ModifierMagnitudeCalculations
对CurrentValue
的重计算都要重新clamping
。
注意: Epic注释,不要使用PreAttributeChange()
处理游戏性事件,仅用它处理clamping
(让CurrentValue
处在正确的范围内)。监听Attribute
的改变进行游戏性事件处理(比如角色上的血条)最好使用UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)
(Responding to Attribute Changes)。
PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
仅在instant GameplayEffect
使Attribute
的 BaseValue
改变时触发。GameplayEffect
执行后,在这里可以对Attribute
做进一步处理。
比如,在示例项目中当角色受到伤害后我们在这里让Health Attribute
减去最终伤害值(Final Damage Meta Attribute
)。如果有护盾属性(Shield Attribute
),我们可以先通过护盾抵消相对的伤害,然后让生命值减去剩余的伤害。示例项目也在这里处理击中反应动画,显示伤害跳字,给击杀者经验和金币奖励。在设计上,伤害值Meta Attribute
将始终通过instant GameplayEffect
进行设置,永远不会通过Attribute
setter设置。
由instant GameplayEffect
改变BaseValue
的其他属性,像法力值和耐力值也可以通过其最大值属性(MaxMana
或MaxStamina
)在此处进行clamping
。
注意 当PostGameplayEffectExecute()
被调用时,对属性的改变已经发生 ,但还没有复制回客户端,因此在此处进行clamping
不会执行两次复制,客户端只要收到clamping
后的结果。
在属性集中当为Attribute
创建聚合器(Aggregator
)后将会调用OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)
。在此方法中可以设置FAggregatorEvaluateMetaData
。AggregatorEvaluateMetaData
被Aggregator
用来基于所有应用到当前属性的Modifiers
求CurrentValue
的值。默认情况下,AggregatorEvaluateMetaData
仅被Aggregator
用来确定哪些Modifiers
符合MostNegativeMod_AllPositiveMods
,MostNegativeMod_AllPositiveMods
允许所有正的Modifiers
和最负的Modifiers
。Paragon中使用此方法仅允许将最负面的减速效果应用于玩家,无论正在应用多少减速效果。没有资格的Modifiers
仍然存在于ASC
上,只是不会被汇总到最终的CurrentValue
。当条件改变后这些Modifiers
仍有可能取得资格,比如最负的Modifier
已经过时,那么下一个最负的Modifier
()将会取得资格。
示例中使用AggregatorEvaluateMetaData
仅允许最负的Modifier
和所有正的`Modifiers:
virtual void OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const override;
void UGSAttributeSetBase::OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const
{
Super::OnAttributeAggregatorCreated(Attribute, NewAggregator);
if (!NewAggregator)
{
return;
}
if (Attribute == GetMoveSpeedAttribute())
{
NewAggregator->EvaluationMetaData = &FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods;
}
}
自定义的AggregatorEvaluateMetaData
限定符需要以静态变量的方式添加到FAggregatorEvaluateMetaDataLibrary
。(译者注:还要为其提供一个类似void QualifierFunc_MostNegativeMod_AllPositiveMods(const FAggregatorEvaluateParameters& EvalParameters, const FAggregator* Aggregator)
的方法)
GameplayEffects
(GE
) 是Abilities
改变自己或别人的Attributes
和GameplayTags
的途径。GEs
可以立即改变Attribute
(像伤害、治疗)或者立即应用持续长时间的Buff/Debuffs(移动加速或眩晕)。UGameplayEffect
是定义一个游戏效果的数据类,GameplayEffects
中不能添加任何其他逻辑。通常设计者只需要创建UGameplayEffect
的蓝图派生类。
GameplayEffects
通过Modifiers
和Executions
(GameplayEffectExecutionCalculation
) 改变Attributes
。
GameplayEffects
有三种持续类型:立即(Instant
),持续( Duration
),和无限(Infinite
)。
此外,GameplayEffects
也能够添加和执行GameplayCues
。 Instant
GameplayEffect
将调用GameplayCue
的Execute
,Duration
或Infinite
GameplayEffect
将在GameplayCue
GameplayTags
上执行添加和删除。
持续类型 | GameplayCue 事件 | 何时使用 |
---|---|---|
Instant |
Execute | 用于立即永久改变Attribute 的BaseValue 。GameplayTags 将不适用,即使一帧也不行。 |
Duration |
Add & Remove | 用于临时修改Attribute 的CurrentValue ,并且添加GameplayTags ( 在GameplayEffect 过期时将会被删除或者手动删除)。持续时间可以在UGameplayEffect 的类或蓝图中指定。 |
Infinite |
Add & Remove | 用于临时修改Attribute 的CurrentValue ,并且添加GameplayTags (在GameplayEffect 被移除时删除)。永不过时,必须通过Ability 或ASC 手动删除。 |
Duration
和Infinite
GameplayEffects
有周期效果(Periodic Effects
)配置项,可以通过配置Period
每隔x秒周期性的执行Modifiers
和Executions
。周期性效果可以看作是Instant GameplayEffects
,每次修改属性的BaseValue
并且执行GameplayCues
。这对实现持续伤害效果非常有用。 注意: Periodic Effects
不能被预测。
如果你需要手动重新计算Duration
或Infinite
GameplayEffect
的Modifiers
(比如有一个MMC
要使用的数据并不是来源于Attributes
),可以通过调用UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel)
传递一个从UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel()
获取的NewLevel
,Level本质并没有变化 ,只是为了调用SetActiveGameplayEffectLevel
以更新Modifiers
,其内部的主要实现如下:
MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
// Private function otherwise we'd call these three functions without needing to set the level to what it already is
UpdateAllAggregatorModMagnitudes(Effect);
GameplayEffects
的创建很特别。当Ability
或ASC
想要应用一个GameplayEffect
时,会从GameplayEffect
的ClassDefaultObject
创建一个GameplayEffectSpec
。然后当应用成功后将其添加到ASC
的ActiveGameplayEffects
(FActiveGameplayEffect
)中。
在GameplayAbilities
和ASC
中有多个方法可以应用GameplayEffects
,通常格式是ApplyGameplayEffectTo
。不同的方法其本质是相同的,都是在目标上调用UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()
。
在GameplayAbility
之外应用GameplayEffects
(比如炮弹),你需要获取目标的ASC
然后调用其ApplyGameplayEffectToSelf
方法。
你也可以通过下述方法监听在ASC
上应用任何Duration
或Infinite
的GameplayEffects
:
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);
回调方法:
virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
服务器总会调用此方法无论什么复制模式。当复制模式为Full
或Mixed
时,自主代理会调用此方法。只有当复制模式为Full
时,模拟代理才会调用此方法。
在GameplayAbilities
和ASC
中有多个方法可以删除GameplayEffects
,通常格式是RemoveActiveGameplayEffect
。不同的方法其本质是相同的,都是在目标上调用FActiveGameplayEffectsContainer::RemoveActiveEffects()
。
在GameplayAbility
之外删除GameplayEffects
,你需要获取目标的ASC
然后调用其RemoveActiveGameplayEffect
方法。
你也可以通过下述方法监听在ASC
上删除任何Duration
或Infinite
的GameplayEffects
:
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);
回调方法:
virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);
服务器总会调用此方法无论什么复制模式。当复制模式为Full
或Mixed
时,自主代理会调用此方法。只有当复制模式为Full
时,模拟代理才会调用此方法。
修改器(Modifiers
)用于修改属性并且是属性修改预测的仅有方式。一个GameplayEffect
可以有0个或多个Modifiers
。每一个修改器只能通过下述方式修改一个属性:
操作 | 描述 |
---|---|
Add |
加 |
Multiply |
乘 |
Divide |
除 |
Override |
覆盖 |
Attribute
的CurrentValue
是一系列Modifiers
添加到BaseValue
的聚合结果。聚合Modifiers
的公式如下所示(FAggregatorModChannel::EvaluateWithBase
):
((InlineBaseValue + Additive) * Multiplicitive) / Division
任何Override Modifiers
都会优先使用最后应用的Modifier
覆盖最终值。
注意: 百分比的修改要使用Multiply
(在Additive
之后执行)
注意: 百分比的修改Prediction会有问题。
一共有四种Modifiers
:Scalable Float, Attribute Based, Custom Calculation Class, and Set By Caller。这些全部通过浮点值和操作符改变Modifier
的Attribute
。
修改器类型 | 描述 |
---|---|
Scalable Float |
FScalableFloats 是一种能够指向Data Table (行表示变量,列表示等级)的结构。 Scalable Floats 将根据当前技能等级(或者是在GameplayEffectSpec 覆盖的等级)自动读取值。这个值可以根据系数进一步处理。如果没有指定数据表,值会被当作是1,需要硬编码系数作为实际的值(忽略等级)。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6d0oFbK-1591700466343)(https://github.com/tranek/GASDocumentation/raw/master/Images/scalablefloats.png)] |
Attribute Based |
Attribute Based Modifiers 基于源(GameplayEffectSpec 创建者)或目标(GameplayEffectSpec 接收者)的支持属性的CurrentValue 或BaseValue 并且可通过系数、Pre/Post Multiply Additive Value进一步处理。快照(Snapshot )意味着取GameplayEffectSpec 被创建时属性的值,否则取GameplayEffectSpec 被应用时属性的值。 |
Custom Calculation Class |
Custom Calculation Class 是最灵活和最复杂的Modifiers 。这个Modifier 需要创建一个ModifierMagnitudeCalculation 类并且可通过系数、Pre/Post Multiply Additive Value进一步处理。 |
Set By Caller |
SetByCaller Modifiers 是在GameplayEffect 之外由Ability 在运行时设置或者由GameplayEffectSpec 的创建者设置。 例如,当你想根据按钮按下的时间决定伤害大小时可以使用SetByCaller 。SetByCallers 本质是存在于GameplayEffectSpec 上的TMap ,Modifier 仅仅是告诉聚合器通过GameplayTag 去检索值。 SetByCallers 仅能使用GameplayTag 不能使用FName 。如果没有在GameplayEffectSpec 中找到GameplayTag 对应的值,游戏将会抛出一个运行时错误并且返回0。如果运算是除法你就悲剧了。 具体使用详见SetByCallers 。 |
GameplayEffects
默认会无视已存在的GameplayEffectSpec
实例,在应用GameplayEffectSpec
时会直接创建新的实例。GameplayEffects
也能够设置在新增效果时使用叠加替代创建新实例,这将只会改变当前已存在GameplayEffectSpec
的叠加数量。叠加仅能用于Duration
和Infinite
GameplayEffects
。
有两种类型的叠加:源聚合和目标聚合。
叠加类型 | 描述 |
---|---|
源聚合 | 目标上的每一个不同源的ASC 都有一个单独的栈实例。每一个源能够应用X个栈。 |
目标聚合 | 在目标上仅有一个栈实例无论源有多少。每一个源能够应用栈的上限不能超过共享栈限制。 |
叠加也有一些相应的策略:过期、持续时间刷新、定期刷新。在GameplayEffect
蓝图上有对应的悬停提示。
示例项目包含了一个自定义的蓝图节点用于监听GameplayEffect
栈的变化。UI界面使用这个监听更新玩家拥有的被动护甲叠加数量。我们将在UMG
的Destruct
中调用AsyncTask
的EndTask()
,否则AsyncTask
将调用存在。详见AsyncTaskEffectStackChanged.h/cpp
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gAeBqGDA-1591700466344)(https://github.com/tranek/GASDocumentation/raw/master/Images/gestackchange.png)]
GameplayEffects
能够赋予ASCs
新的GameplayAbilities
。仅有 Duration
和Infinite
GameplayEffects
才能赋予Abilities
。
一个常见的用例是当你想要强制其他玩家做一些事情(例如让他们击退或拉近)。你可以给他们应用一个GameplayEffect
然后自动激活能够完成上述事情的Ability
(详见当赋予Ability
时如何自动激活(被动技能) Passive Abilities)。
设计者可以选择GameplayEffect
将赋予哪些Abiltities
,设置Ability
的等级,绑定输入ID,设置Ability
的移除策略。
移除策略 | 描述 |
---|---|
Cancel Ability Immediately | 当GameplayEffect 从目标移除时立即取消并移除Ability |
Remove Ability on End | 当Ability 执行完成后移除 |
Do Nothing | 除非手动移除,否则永久存在 |
GameplayEffects
带有多个标签容器( GameplayTagContainers
)。对于每个类别设计者可以编辑GameplayTagContainers
的Added
和Removed
结果将会呈现在Combined Tags
中。Added
用于向父中添加标签。Removed
删除父中已有的标签。
Category | Description |
---|---|
Gameplay Effect Asset Tags | GameplayEffect 具有的标签。 它们本身不执行任何功能,仅用于描述GameplayEffect |
Granted Tags | 存在于GameplayEffect 的标签,但也会给到GameplayEffect 应用到的ASC 。当GameplayEffect 被移除时这些标签也会从ASC 移除。仅用于Duration 和Infinite GameplayEffects |
Ongoing Tag Requirements | 一旦应用,这些标签将决定GameplayEffect 是开启还是关闭。这也说明了GameplayEffect 在应用时可以被关闭。如果GameplayEffect 不满足Ongoing Tag Requirements 其将会被关闭,直到条件满足GameplayEffect 会被再次打开并重新应用Modifiers 。仅用于Duration 和Infinite GameplayEffects |
Application Tag Requirements | 目标上的标签决定GameplayEffect 是否能够被应用 |
Remove Gameplay Effects with Tags | 当前GameplayEffect 被成功应用时,如果目标上的GameplayEffects 的Asset Tags 或Granted Tags 中有这些标签,那么对应的GameplayEffect 将被移除 |
GameplayEffects
能够获得免疫,用于通过 GameplayTags
高效的阻止其他GameplayEffects
的应用。免疫也可以通过其他方式实现,比如Application Tag Requirements
,但此方法将提供一个委托(UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate
)可以监听GameplayEffects
的免疫阻止。
GrantedApplicationImmunityTags
将检查源ASC
(也包括源的Ability
中的AbilityTags
如果有的话)是否有指定的标签。这提供了一种根据某些角色或者源所拥有的标签决定是否免疫GameplayEffects
的方式。
Granted Application Immunity Query
将检查要应用的GameplayEffectSpec
如果满足标签匹配则阻止应用,否则允许。
通过GameplayEffect
蓝图的悬停提示了解更多关于Queries
的使用。
(GESpec
) 可以相像成是GameplayEffects
的实例化。GESpec
包括一个GameplayEffect
类的引用,创建时的等级,由谁创建。这些可以在运行时(被应用前)自由的创建和修改,不像GameplayEffects
是由设计师在运行前创建。在应用一个GameplayEffect
时,将会由GameplayEffect
创建出GameplayEffectSpec
,然后将其应用给目标。
GameplayEffectSpecs
是通过可被蓝图调用的UAbilitySystemComponent::MakeOutgoingSpec()
创建(需要以GameplayEffects
类作为参数),GameplayEffectSpecs
不会被立即应用。通常将其传递给由技能创建的炮弹,然后当炮弹击中目标时将其应用给目标。当GameplayEffectSpecs
被成功应用将返回一个新的结构体FActiveGameplayEffect
。
GameplayEffectSpec
主要内容:
GameplayEffec
类,由设计师在运行前创建GameplayEffectSpec
的等级,通常和创建GameplayEffectSpec
的技能等级相同,但也可以不同GameplayEffectSpec
的持续时间,默认是GameplayEffect
的持续时间,但也可以不同GameplayEffectSpec
的周期时间,默认是GameplayEffect
的周期时间,但也可以不同GameplayEffectSpec
的当前堆叠数量,堆叠数量限制在GameplayEffect
上GameplayEffectContextHandle
表示谁创建的这个GameplayEffectSpec
GameplayEffectSpec
创建时的属性快照DynamicGrantedTags
是对应GameplayEffect
中的GrantedTags
的额外标签DynamicAssetTags
是对应GameplayEffect
中的AssetTags
的额外标签SetByCaller
TMaps
SetByCallers
允许GameplayEffectSpec
以GameplayTag
或FName
关联浮点值,具体存储在GameplayEffectSpec
的TMap
和TMap
中。使用方式和GameplayEffect
的Modifiers
类似,也可以通过SetByCallers
将Ability
中生成的数据传递给GameplayEffectExecutionCalculations
或ModifierMagnitudeCalculations
。
SetByCaller 使用 |
描述 |
---|---|
Modifiers |
必须在GameplayEffect 类中提前定义。仅能使用GameplayTag 版本。如果在GameplayEffect 中被定义,在GameplayEffectSpec 找不到对应的值,游戏将会运行时错误并且返回0。小心除法,详见Modifiers |
Elsewhere | 不需要被提前定义。 如果在GameplayEffectSpec 找不到对应的值时将会返回一个开发者定义的默认值并且可选是否要给出警告 |
要在蓝图中设置SetByCaller
的值,可以使用对应的蓝图节点(GameplayTag
或 FName
)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ao6XdWY-1591700466345)(https://github.com/tranek/GASDocumentation/raw/master/Images/setbycaller.png)]
要在蓝图中读取SetByCaller
的值,需要在自己的Blueprint Library
中实现蓝图节点
要在C++中设置SetByCaller
的值,可以使用对应的方法(GameplayTag
或 FName
)
void FGameplayEffectSpec::SetSetByCallerMagnitude(FName DataName, float Magnitude);
void FGameplayEffectSpec::SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);
要在C++中读取SetByCaller
的值,可以使用对应的方法(GameplayTag
或 FName
)
float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
建议使用GameplayTag
而不是FName
版本的SetByCaller
。GameplayTags
可以阻止在蓝图中的拼写错误,而且在网络同步过程中会更高效。
包含了GameplayEffectSpec
的创建者(Instigator
)和应用的目标(TargetData
)。可以通过派生GameplayEffectSpec
用来在ModifierMagnitudeCalculations
/ GameplayEffectExecutionCalculations
,AttributeSets
和GameplayCues
之间传递任意数据。
派生GameplayEffectContext
的过程:
FGameplayEffectContext::GetScriptStruct()
FGameplayEffectContext::Duplicate()
FGameplayEffectContext::NetSerialize()
FGameplayEffectContext
一样实现派生类的TStructOpsTypeTraits
AbilitySystemGlobals
中重载AllocGameplayEffectContext()
返回FGameplayEffectContext
派生类的对象GASShooter 在GameplayEffectContext
的派生类中添加TargetData
用于在GameplayCues
中访问,比如霰弹枪可以击中多个目标。
ModifierMagnitudeCalculations
(简称ModMagcCalc
或MMC
)用于GameplayEffects
中的Modifiers
。 其作用和GameplayEffectExecutionCalculations
类似但不同的是MMC
可以被预测(Predicted)。MMC
唯一的作用是通过CalculateBaseMagnitude_Implementation()
返回一个浮点值,可以通过蓝图或C++进行MMC
的派生并重载此方法。
MMCs
可以被任何类型的GameplayEffects
(Instant
, Duration
, Infinite
, 或Periodic
)使用。
MMC
的优势在于可以获取GameplayEffect
的目标和源的任何属性并且能够读取GameplayEffectSpec
中的GameplayTags
和SetByCallers
。Attributes
可以是快照也可以不是,属性快照将在GameplayEffectSpec
创建时获取,属性非快照将在应用时自动获取。通过已存在于ASC
的Modes
捕获Attributes
重计算他们的CurrentValue
,重计算并不会执行AbilitySet
中的PreAttributeChange()
因此需要在此处完成Clamping
。
Snapshot | Source or Target | Captured on GameplayEffectSpec |
Automatically updates when Attribute changes for Infinite or Duration GE |
---|---|---|---|
Yes | Source | Creation | No |
Yes | Target | Application | No |
No | Source | Application | Yes |
No | Target | Application | Yes |
MMC
的结果浮点值可以被GameplayEffect's Modifier
的coefficient、pre coefficient和post coefficient进一步处理。
下面是一个MMC
的示例,Dota2中被敌法师打一下,计算法力损耗:
UPAMMC_PoisonMana::UPAMMC_PoisonMana()
{
//ManaDef defined in header FGameplayEffectAttributeCaptureDefinition ManaDef;
ManaDef.AttributeToCapture = UPAAttributeSetBase::GetManaAttribute();
ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
ManaDef.bSnapshot = false;
//MaxManaDef defined in header FGameplayEffectAttributeCaptureDefinition MaxManaDef;
MaxManaDef.AttributeToCapture = UPAAttributeSetBase::GetMaxManaAttribute();
MaxManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
MaxManaDef.bSnapshot = false;
RelevantAttributesToCapture.Add(ManaDef);
RelevantAttributesToCapture.Add(MaxManaDef);
}
float UPAMMC_PoisonMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
// Gather the tags from the source and target as that can affect which buffs should be used
const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();
FAggregatorEvaluateParameters EvaluationParameters;
EvaluationParameters.SourceTags = SourceTags;
EvaluationParameters.TargetTags = TargetTags;
float Mana = 0.f;
GetCapturedAttributeMagnitude(ManaDef, Spec, EvaluationParameters, Mana);
Mana = FMath::Max(Mana, 0.0f);
float MaxMana = 0.f;
GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvaluationParameters, MaxMana);
MaxMana = FMath::Max(MaxMana, 1.0f); // Avoid divide by zero
float Reduction = -20.0f;
if (Mana / MaxMana > 0.5f)
{
// Double the effect if the target has more than half their mana
Reduction *= 2;
}
if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
{
// Double the effect if the target is weak to PoisonMana
Reduction *= 2;
}
return Reduction;
}
如果你没有在MMC
的构建方法中将FGameplayEffectAttributeCaptureDefinition
添加到RelevantAttributesToCapture
中,在获取Attributes
时将会得到一个missing Spec
的错误。除非在计算过程中你不需要获取任何Attributes
。
GameplayEffectExecutionCalculations
(ExecutionCalculation
, Execution
(在插件代码中经常会看到这个术语), or ExecCalc
) 是GameplayEffects
改变ASC
的一种最有力的方式。与ModifierMagnitudeCalculations
类似,ExecCalc
可以获取Attributes
,可选的是否快照。 与MMCs
不同的是,ExecCalc
可以改变多个属性并且高效的处理任何事情。强大和灵活之下,ExecCalc
不支持Predicted,且必须在C++中实现(4.25已经可以在蓝图中实现)。
ExecutionCalculations
仅能用于Instant
和Periodic
GameplayEffects
。通常情况下带有Execute
的环境中只能使用这两种GameplayEffects
。
属性是否快照同4.5.11 修改器量计算 Modifier Magnitude Calculation,这里不再赘述
Snapshot | Source or Target | Captured on GameplayEffectSpec |
---|---|---|
Yes | Source | Creation |
Yes | Target | Application |
No | Source | Application |
No | Target | Application |
可以按照Epic的ActionRPG示例项目中的方式设置Attribute
的获取,通过一个自定义的结构体定义要获取的Attributes
。
对于Local Predicted
, Server Only
, 和Server Initiated
的 GameplayAbilities
,ExecCalc
仅在服务器端执行。
ExecCalc
最常用于伤害计算,从Source
和Target
读取一系列属性值,然后进行复杂的计算。示例项目中也使用ExecCalc
完成伤害计算,读取由GameplayEffectSpec's
SetByCaller
设置的伤害值,再通过Target
中的护甲属性缓解伤害。详见GDDamageExecCalculation.cpp/.h
。
CustomApplicationRequirement
(CAR
) 类给设计者提供了是否能够应用GameplayEffect
的高级控制手段(有别于简单的标签控制)。 可以通过在蓝图中重载CanApplyGameplayEffect()
或者在C++中重载CanApplyGameplayEffect_Implementation()
实现。
何时需要使用CARs
?比如:
Target
需要有一定数量的属性时Target
需要GameplayEffect
堆叠到一定数量时除此之外CARs
还能够做更多事情,比如检查Target
是否应用了一个GameplayEffect
的实例,在应用一个新实例时如果同类型的实例已存在则只改变其持续时间(CanApplyGameplayEffect()
要返回false)。
GameplayAbilities
可以指定一个处理技能消耗的GameplayEffect
。如果无法满足消耗,则技能不会被激活。Cost GE
必须是一个Instant GameplayEffect
其中可以有一个或多个Modifiers
用于减去技能所需的属性消耗。默认情况下,Cost GEs
是支持预测的,所以最好不要使用ExecutionCalculations
,建议只使用MMCs
完成消耗计算。
刚开始,你可能会为每一个带有消耗的GA
创建一个对应的Cost GE
。高阶方法是,对于多个GAs
复用一个Cost GE
,仅通过GA
的消耗数据(消耗值定义在GA
上)修改从Cost GE
创建出的GameplayEffectSpec
。仅能用于Instanced Abilities
。
两种使用Cost GE
的方法:
MMC
,这是最简单的方法。 创建一个MMC
从GameplayAbility
实例中读取消耗值:float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
const UPGGameplayAbility* Ability = Cast(Spec.GetContext().GetAbilityInstance_NotReplicated());
if (!Ability)
{
return 0.0f;
}
return Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel());
}
在这个示例中,通过在派生的GameplayAbility
中添加FScalableFloat
保存GA
的消耗:
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cost")
FScalableFloat Cost;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yC21FWgU-1591700466346)(https://github.com/tranek/GASDocumentation/raw/master/Images/costmmc.png)]
UGameplayAbility::GetCostGameplayEffect()
,在运行时创建GameplayEffect
,读取GameplayAbility
中的消耗值。GameplayAbilities
可以指定一个处理技能冷却的GameplayEffect
。冷却决定了技能多长时间能够被再次施放,处在冷却中的技能无法被施放。Cooldown GE
必须是一个Duration GameplayEffect
,不带Modifiers
,在GameplayEffect
的GrantedTags
(“Cooldown Tag
”)中配置代表每个GameplayAbility
或Ability Slot
(槽位装备技能)的唯一的GameplayTag
。实际上是由GA
检查Cooldown Tag
而不是Cooldown GE
。默认情况下,Cooldown GEs
是支持预测的,所以最好不要使用ExecutionCalculations
,建议只使用MMCs
完成冷却计算。
刚开始,你可能会为每一个带有冷却的GA
创建一个对应的Cooldown GE
。高阶方法是,对于多个GAs
复用一个Cooldown GE
,仅通过GA
的冷却数据(冷却时间和Cooldown Tag
定义在GA
上)修改从Cooldown GE
创建出的GameplayEffectSpec
。仅能用于Instanced Abilities
。
两种使用Cooldown GE
的方法:
SetByCaller
,这是最简单的方法,通过带有GameplayTag
的SetByCaller
设置Cooldown GE
的持续时间,在你的GameplayAbility
子类中定义一个FScalableFloat
的持续时间,一个FGameplayTagContainer
用于唯一的Cooldown Tag
,再有一个临时的FGameplayTagContainer
用于返回Cooldown Tag
和Cooldown GE's Tags
的合并。UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;
// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY()
FGameplayTagContainer TempCooldownTags;
接下来重载UGameplayAbility::GetCooldownTags()
并返回Cooldown Tag
和Cooldown GE's Tags
的合并:
const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
FGameplayTagContainer* MutableTags = const_cast(&TempCooldownTags);
const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
if (ParentTags)
{
MutableTags->AppendTags(*ParentTags);
}
MutableTags->AppendTags(CooldownTags);
return MutableTags;
}
最后,重载UGameplayAbility::ApplyCooldown()
注入上述的Cooldown Tags
并且通过GameplayEffectSpec
的SetByCaller
写入冷却持续时间:
void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
if (CooldownGE)
{
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName( OurSetByCallerTag )), CooldownDuration.GetValueAtLevel(GetAbilityLevel()));
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
}
}
在下图中,冷却持续时间的Modifier
是由SetByCaller
通过Data.Cooldown
设置。 Data.Cooldown
是上述代码中OurSetByCallerTag
的值:
MMC
,这种方法基本和上述相同,除了设置Cooldown GE
冷却持续时间的方式从SetByCaller
改变成了使用Custom Calculation Class
:UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;
// Temp container that we will return the pointer to in GetCooldownTags().
// This will be a union of our CooldownTags and the Cooldown GE's cooldown tags.
UPROPERTY()
FGameplayTagContainer TempCooldownTags;
const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
FGameplayTagContainer* MutableTags = const_cast(&TempCooldownTags);
const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
if (ParentTags)
{
MutableTags->AppendTags(*ParentTags);
}
MutableTags->AppendTags(CooldownTags);
return MutableTags;
}
void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
if (CooldownGE)
{
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
}
}
float UPGMMC_HeroAbilityCooldown::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
const UPGGameplayAbility* Ability = Cast(Spec.GetContext().GetAbilityInstance_NotReplicated());
if (!Ability)
{
return 0.0f;
}
return Ability->CooldownDuration.GetValueAtLevel(Ability->GetAbilityLevel());
}
bool APGPlayerState::GetCooldownRemainingForTag(FGameplayTagContainer CooldownTags, float & TimeRemaining, float & CooldownDuration)
{
if (AbilitySystemComponent && CooldownTags.Num() > 0)
{
TimeRemaining = 0.f;
CooldownDuration = 0.f;
FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
TArray< TPair > DurationAndTimeRemaining = AbilitySystemComponent->GetActiveEffectsTimeRemainingAndDuration(Query);
if (DurationAndTimeRemaining.Num() > 0)
{
int32 BestIdx = 0;
float LongestTime = DurationAndTimeRemaining[0].Key;
for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
{
if (DurationAndTimeRemaining[Idx].Key > LongestTime)
{
LongestTime = DurationAndTimeRemaining[Idx].Key;
BestIdx = Idx;
}
}
TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;
return true;
}
}
return false;
}
**注意:**要在客户端中查询剩余冷却时间,客户端必须能够收到GameplayEffects
的复制,这将依赖ASC
的Replication Mode
。
要监听冷却开始,可以通过绑定AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf
判断是否应用了Cooldown GE
或者通过绑定AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved)
判断是否新增了Cooldown Tag
。建议使用监听Cooldown GE
添加的方式,因为FOnGameplayEffectAppliedDelegate
可以访问GameplayEffectSpec
用以区分Cooldown GE
是本地预测还是服务器校正。
要监听冷却结束,可以通过绑定AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate
判断是否删除了Cooldown GE
或者通过绑定AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved)
判断是否删除了Cooldown Tag
。建议使用监听Cooldown Tag
删除的方式,因为当服务器校正的Cooldown GE
到达时,本地预测的Cooldown GE
将会被删除这将会触发OnAnyGameplayEffectRemovedDelegate()
即使我们仍处于冷却状态。而Cooldown Tag
在删除本地预测的Cooldown GE
,应用服务器校正的Cooldown GE
过程中将不会发生变化。
**注意:**监听GameplayEffect
在客户端的添加或删除,客户端必须能够收到GameplayEffects
的复制,这将依赖ASC
的Replication Mode
。
示例工程包含了一个自定义的蓝图节点用来监听冷却的开始和结束,用以在UI上显示和更新陨石技能的剩余冷却时间。需要在UMG Widget
的 Destruct
事件中调用EndTask()
以结束AsyncTask
。详见AsyncTaskEffectCooldownChanged.h/cpp
。
当前,冷却并不能正真的被预测。当本地预测的Cooldown GE
被应用时我们可以开始启动UI冷却的计数器,但GameplayAbility
的实际冷却束缚于服务器的冷却剩余时间。根据玩家的延迟,本地预测的冷却已经结束但在服务器端GameplayAbility
仍处于冷却中,这将阻止技能的施放直到服务器冷却结束。
示例工程解决上述问题的方式是,在本地预测冷却开始时将陨石技能UI图标置灰,然后当服务器校正的Cooldown GE
到达时启动UI冷却的计数器。
这样的游戏结果是,与较低延迟的玩家相比,具有较高延迟的玩家在短冷却时间的射击率较低。堡垒之夜解决此问题的方式是在武器中使用自定义统计而不是使用Cooldown GE
。
真正的可预测冷却(在GameplayAbility
本地冷却已结结束服务器仍在冷却中玩家依然可以激活GameplayAbility
)Epic会在后续GAS的迭代中实现。
要修改Cooldown GE
或任何Duration
GameplayEffect
的剩余持续时间,我们需要修改GameplayEffectSpec
的Duration
,更新StartServerWorldTime
、CachedStartServerWorldTime
、StartWorldTime
并且使用CheckDuration()
重新检查持续时间。在服务器完成上述步骤并将FActiveGameplayEffect
标记为Dirty
将会把修改复制到客户端。
注意: 这将需要一个const_cast
转换并且也不是Epic官方给出的方式,但目前为止工作正常:
bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
{
if (!Handle.IsValid())
{
return false;
}
const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
if (!ActiveGameplayEffect)
{
return false;
}
FActiveGameplayEffect* AGE = const_cast(ActiveGameplayEffect);
if (NewDuration > 0)
{
AGE->Spec.Duration = NewDuration;
}
else
{
AGE->Spec.Duration = 0.01f;
}
AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
ActiveGameplayEffects.MarkItemDirty(*AGE);
ActiveGameplayEffects.CheckDuration(Handle);
AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
OnGameplayEffectDurationChange(*AGE);
return true;
}
在运行时动态创建GameplayEffects
是一个高级主题。这种用法不能过于频繁。
仅有Instant
GameplayEffects
能够在C++中被运行时创建。示例工程动态创建了一个GameplayEffects
,用于当一个角色受到致命一击时(在其AttributeSet
中处理),将金币和经验奖励发送给他的击杀者。
// Create a dynamic instant Gameplay Effect to give the bounties
UGameplayEffect* GEBounty = NewObject(GetTransientPackage(), FName(TEXT("Bounty")));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;
int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);
FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
InfoXP.ModifierOp = EGameplayModOp::Additive;
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();
FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
InfoGold.ModifierOp = EGameplayModOp::Additive;
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();
Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());
Duration
和Infinite
GameplayEffects
不能在运行时动态创建,因为在他们复制时会查找GameplayEffect
类定义,结果没有。为了实现此功能,你应该像通常在编辑器中那样制作GameplayEffect
原型类,然后根据需要在运行时自定义GameplayEffectSpec
实例。
Epic官方的Action RPG 示例项目实现了一个叫FGameplayEffectContainer
的结构,对于包含GameplayEffects
和TargetData
极为方便。它自动化了一些工作,像根据GameplayEffects
创建GameplayEffectSpecs
,设置GameplayEffectContext
的默认值。在GameplayAbility
中创建一个GameplayEffectContainer
并且将它传递给生成的炮弹是非常简单和直接的。我并没有在示例项目中实现GameplayEffectContainers
,但还是强烈建议了解这个并考虑将其添加到你的项目中。
要访问GameplayEffectContainers
中的GESpecs
,需要展开FGameplayEffectContainer
然后通过索引GESpecs
可以得到具体的GESpec
。这需要在刚开始就知道你想访问的GESpec
索引是多少。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUkFtmi1-1591700466348)(https://github.com/tranek/GASDocumentation/raw/master/Images/gecontainersetbycaller.png)]
GameplayEffectContainers
还包括可选的目标选取方式。
GameplayAbilities
(GA
)是在游戏中Actor
能做的行为或技能。在同一时间可以激活多个GameplayAbility
,例如冲刺的同时射击。GA
可以通过Blueprint
或C++
制作。
GameplayAbilities
的示例:
不能通过GameplayAbilities
实现的示例:
GameplayAbility
处理从商店购买物品这些不是规则,只是我的建议。
GameplayAbilities
默认带有一个等级(技能等级)用于定义修改属性时的修改量,或是改变GameplayAbility
的功能。
GameplayAbilities
会在Owning Client
上执行,根据 Net Execution Policy
设置的策略决定是否在服务器端也要执行。Net Execution Policy
决定一个GameplayAbility
是否将会进行本地预测。对于可选的cost and cooldown GameplayEffects
它们包含了一些默认的行为。 在GameplayAbilities
持续过程中会使用AbilityTasks
处理一些行为比如等待一个事件、等待一个属性改变、等待玩家选择目标或者通过Root Motion Source
移动一个角色。模拟的客户端将不会执行GameplayAbilities
。取而代之的是,当服务器执行技能时,任何需要在simulated proxies
呈现的表现(像播放动画)都将通过Replicated
或者通过AbilityTasks
执行的RPC
或者是GameplayCues
(播放声音和特效)来实现。
所有的GameplayAbilities
都需要重载ActivateAbility()
方法以实现自己的游戏逻辑。在GameplayAbility
完成或取消时可以通过EndAbility
实现一些额外的逻辑。
复杂一些的GameplayAbility
流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3gLmT8ic-1591700466349)(https://github.com/tranek/GASDocumentation/raw/master/Images/abilityflowchartcomplex.png)]
复杂的Ability
也可以通过多个彼此交互的GameplayAbilities
实现。
不要使用这个选项。这个名字有误导性你并不需要这个。GameplayAbilitySpecs
默认会从服务器复制到Owning Client
。就像上面说过的,GameplayAbilities
不会在simulated proxies上执行。Epic的Dave Ratti说过他希望 在将来删除这个选项。
这个选项常常引发问题。这意味着如果客户端的GameplayAbility
由于取消或自然完成而终止,将强制服务器的版本结束(无论其是否完成)。 后一个问题很重要,尤其是对于高延迟玩家使用的本地预测的GameplayAbilities
。 通常要禁用此选项。
开启此项将总是将输入的按下和释放事件传递到服务器。
Epic官方建议不要使用此选项,取而代之的应该使用已存在的输入相关的AbilityTasks
内置的Generic Replicated Events
:
/** Direct Input state replication. These will be called if bReplicateInputDirectly is true on the ability and is generally not a good thing to use. (Instead, prefer to use Generic Replicated Events). */
UAbilitySystemComponent::ServerSetInputPressed()
ASC
允许直接绑定输入操作并且在获得技能时指定这些输入到GameplayAbilities
。当输入绑定后,触发事件时如果GameplayTag
满足则会自动施放这些GameplayAbilities
。要想使用内置的AbilityTasks
响应输入必须分配输入操作。
除了分配的输入操作可以激活GameplayAbilities
之外,ASC
也能够接受确认(Confirm
)和取消(Cancel
)输入。这些特别的输入被AbilityTasks
用于确定目标或取消目标。
要为ASC
绑定输入,必须先创建一个枚举将输入操作名转换为一个字节。枚举中的每一项必须与项目设置中输入操作匹配(DisplayName
无所谓)。
示例:
UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
// 0 None
None UMETA(DisplayName = "None"),
// 1 Confirm
Confirm UMETA(DisplayName = "Confirm"),
// 2 Cancel
Cancel UMETA(DisplayName = "Cancel"),
// 3 LMB
Ability1 UMETA(DisplayName = "Ability1"),
// 4 RMB
Ability2 UMETA(DisplayName = "Ability2"),
// 5 Q
Ability3 UMETA(DisplayName = "Ability3"),
// 6 E
Ability4 UMETA(DisplayName = "Ability4"),
// 7 R
Ability5 UMETA(DisplayName = "Ability5"),
// 8 Sprint
Sprint UMETA(DisplayName = "Sprint"),
// 9 Jump
Jump UMETA(DisplayName = "Jump")
};
如果你的ASC
在Character
上,那么可在以SetupPlayerInputComponent()
进行绑定:
// Bind to AbilitySystemComponent
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"), FString("CancelTarget"), FString("EGDAbilityInputID"), static_cast(EGDAbilityInputID::Confirm), static_cast(EGDAbilityInputID::Cancel)));
如果你的ASC
在PlayerState
上,SetupPlayerInputComponent()
内部可能存在潜在的竞态条件,PlayerState
可能还没有复制到客户端。因此,建议在SetupPlayerInputComponent()
和OnRep_PlayerState()
都尝试进行绑定。只在OnRep_PlayerState()
中进行绑定也不够充分,在PlayerState
被复制到客户端时 Actor
的 InputComponent
也可能是空的(PlayerController
通知客户端调用ClientRestart()
以创建InputComponent
,当这一步晚于OnRep_PlayerState()
)。示例项目尝试在这两个地方尝试进行绑定且通过一个布尔变量控制真正的绑定只会进行一次。
注意: 在示例项目中枚举中的Confirm
和Cancel
与项目设置中输入操作名并不匹配 (ConfirmTarget
和CancelTarget
)。但是我们可以通过BindAbilityActivationToInputComponent
完成它们的映射。枚举中的其他输入都与项目设置中的输入操作名匹配。
对于只会通过一个输入激活的GameplayAbilities
(比如MOBA中有些技能可以使用相同的槽),建议为UGameplayAbility
子类添加一个变量用于定义输入,然后在赋予技能时从ClassDefaultObject
中读取这个变量。
为ASC
赋予一个GameplayAbility
会将其加入到ASC
的ActivatableAbilities
列表中,并允许GameplayAbility
在满足GameplayTag
requirements时可以被激活,只能在C++。
我们在服务器端赋予GameplayAbilities
,对应的GameplayAbilitySpec
会被自动复制到Owning Client
。其他客户端(Simulated Proxies
)将不会收到GameplayAbilitySpec
。
示例项目在Character
类中存储了一个TArray
,这些技能在游戏开始时将被自动赋予角色:
void AGDCharacterBase::AddCharacterAbilities()
{
// Grant abilities, but only on the server
if (Role != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->CharacterAbilitiesGiven)
{
return;
}
for (TSubclassOf& StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast(StartupAbility.GetDefaultObject()->AbilityInputID), this));
}
AbilitySystemComponent->CharacterAbilitiesGiven = true;
}
当赋予这些GameplayAbilities
时,我们会创建带有UGameplayAbility
类、技能等级、绑定的输入、SourceObject
(谁将GameplayAbility
给了ASC
)的GameplayAbilitySpecs
。
如果为一个GameplayAbility
分配了输入操作,当输入按下并且GameplayTag
满足技能就会被释放。这并不总是期望的GameplayAbility
激活方式,ASC
还提供了其他四种激活GameplayAbilities
的方法:通过GameplayTag
,GameplayAbility
类,GameplayAbilitySpec
句柄(Handle)和事件。通过事件激活一个GameplayAbility
允许你传递一些事件数据(Payload)。
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation = true);
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilityByClass(TSubclassOf InAbilityToActivate, bool bAllowRemoteActivation = true);
bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);
bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);
FGameplayAbilitySpecHandle GiveAbilityAndActivateOnce(const FGameplayAbilitySpec& AbilitySpec);
要通过事件激活一个GameplayAbility
,需要在GameplayAbility
中添加一个Triggers
,分配一个GameplayTag
,选择GameplayEvent
。然后通过UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload)
发送事件。通过事件激活一个GameplayAbility
允许你传递一些事件数据(Payload)。
GameplayAbility
Trigger
也可以在GameplayTag
添加或删除时激活GameplayAbility
。
注意: 当通过事件激活一个GameplayAbility
,你必须在技能蓝图中使用ActivateAbilityFromEvent
节点,并且ActivateAbility
节点不能存在于你的蓝图中。如果技能蓝图中ActivateAbility
节点存在,它将始终被调用,ActivateAbilityFromEvent
则不会被调用。
注意: 不要忘记在GameplayAbility
结束时调用EndAbility()
,除非你想要一个一直运行的被动技能。
本地预测GameplayAbilities
的激活序列过程:
TryActivateAbility()
InternalTryActivateAbility()
CanActivateAbility()
检查GameplayTag
、消耗、冷却等决定是否能够释放技能CallServerTryActivateAbility()
并且传递生成好的Prediction Key
CallActivateAbility()
PreActivate()
ActivateAbility()
最终施放技能Server receives CallServerTryActivateAbility()
ServerTryActivateAbility()
InternalServerTryActivateAbility()
InternalTryActivateAbility()
CanActivateAbility()
ClientActivateAbilitySucceed()
在服务器确定激活成功时更新ActivationInfo
并且广播OnConfirmDelegate
委托(这不同于输入确认)CallActivateAbility()
PreActivate()
ActivateAbility()
最终施放技能如果服务器激活技能失败,将会调用ClientActivateAbilityFailed()
并立即终止客户端的GameplayAbility
并回退任何可预测的修改。
要自动激活执行运行的被动GameplayAbilities
,可以重写UGameplayAbility::OnAvatarSet()
(它将在赋予GameplayAbility
并且设置AvatarActor
时自动执行)调用TryActivateAbility()
。
建议为自定义的UGameplayAbility
类添加一个bool变量用以控制GameplayAbility
在被赋予时是否自动激活。示例工程这样实现的被动护甲技能。
通常被动GameplayAbilitites
的Net Execution Policy
设置为Server Only
。
void UGDGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec)
{
Super::OnAvatarSet(ActorInfo, Spec);
if (ActivateAbilityOnGranted)
{
bool ActivatedAbility = ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
}
}
要从内部取消一个GameplayAbility
,可以调用CancelAbility()
,它将调用EndAbility()
并且设置WasCancelled
为true。
要从外部取消一个GameplayAbility
,ASC
提供了几个方法:
/** Cancels the specified ability CDO. */
void CancelAbility(UGameplayAbility* Ability);
/** Cancels the ability indicated by passed in spec handle. If handle is not found among reactivated abilities nothing happens. */
void CancelAbilityHandle(const FGameplayAbilitySpecHandle& AbilityHandle);
/** Cancel all abilities with the specified tags. Will not cancel the Ignore instance */
void CancelAbilities(const FGameplayTagContainer* WithTags=nullptr, const FGameplayTagContainer* WithoutTags=nullptr, UGameplayAbility* Ignore=nullptr);
/** Cancels all abilities regardless of tags. Will not cancel the ignore instance */
void CancelAllAbilities(UGameplayAbility* Ignore=nullptr);
/** Cancels all abilities and kills any remaining instanced abilities */
virtual void DestroyActiveState();
注意: 我发现CancelAllAbilities
可能无法正确工作,当存在Non-Instanced
GameplayAbilities
的时候。它似乎在遇到Non-Instanced
GameplayAbility
时会放弃取消。CancelAbilities
会更好的处理Non-Instanced
GameplayAbilities
,示例项目就是用的这个 (Jump 是一个Non-Instanced
GameplayAbility
)。
新手经常会问“怎样获得激活的技能?”。由于可以同时激活多个技能,因此需要在ASC
的ActivatableAbilities
(可激活技能)列表中查找匹配Asset
或Granted
GameplayTag
的技能。
UAbilitySystemComponent::GetActivatableAbilities()
返回一个可被迭代的TArray
。
ASC
提供了另一个帮助方法,可以带有一个GameplayTagContainer
参数(比上述方法方便很多),还有一个bOnlyAbilitiesThatSatisfyTagRequirements
仅返回当前能够被激活的GameplayAbilitySpecs
。
例如,你可能有两种基本的攻击技能,一个是带武器,一个是赤手空拳。可以根据装备武器的GameplayTag
区分两者以激活正确的一个。详见Epic对此方法的注释。
UAbilitySystemComponent::GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true)
获取FGameplayAbilitySpec
后可以通过IsActive()
方法判断技能当前是否激活中。
GameplayAbility
的实例化策略决定当激活GameplayAbility
时是否以及如何实例化GameplayAbility
。
Instancing Policy |
描述 | 何时使用 |
---|---|---|
Instanced Per Actor | 每一个ASC 仅有一个GameplayAbility 的实例,激活时复用 |
通常这是使用最多的实例化策略,设计师需要在激活时手动重置所有变量 |
Instanced Per Execution | 每一次GameplayAbility 的激活都会创建一个新的实例 |
优势是每一次GameplayAbilitites 激活变量都是已经重置的。问题是性能开销较大。示例工程并没有使用过这个 |
Non-Instanced | 使用GameplayAbility 的 ClassDefaultObject ,没有实例被创建 |
三者之中性能最好的,但使用也是最苛刻的。Non-Instanced GameplayAbilities 不能存储任何状态,不能有动态变量,不能绑定AbilityTask 的委托。最佳用途是频繁使用的简单技能,比如MOBA或RTS中小怪的普攻。示例项目中的Jump Ability 是Non-Instanced |
GameplayAbility
的网络执行策略决定谁以什么顺序运行GameplayAbility
Net Execution Policy |
描述 |
---|---|
Local Only |
GameplayAbility 仅运行于Owning Client ,当技能仅用于本地修饰时有用。单人游戏应使用Server Only |
Local Predicted |
Local Predicted GameplayAbilities 将在Owning Client 先执行,然后在Server 执行。服务器版本将修正客户端所有的预测错误 |
Server Only |
GameplayAbility 仅运行于Server ,被动GameplayAbilities 通常使用Server Only 。单人游戏应使用Server Only |
Server Initiated |
Server Initiated GameplayAbilities 先在Server 执行,然后在Owning Client 执行。个人没怎么用过 |
GameplayAbilities
有一系列的GameplayTagContainers
用以处理内部逻辑。所有`GameplayTags均未复制。
GameplayTag Container |
Description |
---|---|
Ability Tags |
用以描述GameplayAbility |
Cancel Abilities with Tag |
当此技能激活时会用此取消其他GameplayAbilities |
Block Abilities with Tag |
当此技能激活时会用此阻止其他GameplayAbilities 的激活 |
Activation Owned Tags |
当GameplayAbility 激活时将GameplayTags 给GameplayAbility 的Owner 。记住不会被复制 |
Activation Required Tags |
仅当Owner 拥有所有这些GameplayTags ,GameplayAbility 才能被激活 |
Activation Blocked Tags |
Owner 拥有任一个这里的GameplayTags ,GameplayAbility 都不能被激活 |
Source Required Tags |
仅当Source 拥有所有这些GameplayTags ,GameplayAbility 才能被激活。仅在由事件触发的GameplayAbility 设置 |
Source Blocked Tags |
Source 拥有任一个这里的GameplayTags ,GameplayAbility 都不能被激活。仅在由事件触发的GameplayAbility 设置 |
Target Required Tags |
仅当Target 拥有所有这些GameplayTags ,GameplayAbility 才能被激活。仅在由事件触发的GameplayAbility 设置 |
Target Blocked Tags |
Target 拥有任一个这里的GameplayTags ,GameplayAbility 都不能被激活。仅在由事件触发的GameplayAbility 设置 |
在赋予GameplayAbility
后,ASC
上会存在一个GameplayAbilitySpec
,其定义了可被激活的GameplayAbility
(其中包括GameplayAbility
类,等级,输入绑定,运行时状态)。
当在Server
赋予了一个GameplayAbility
,Server
将会把GameplayAbilitySpec
复制给Owning Client
(才可被激活)。
激活一个GameplayAbilitySpec
是否会创建一个GameplayAbility
的实例由Instancing Policy
决定。
GameplayAbilities
的通常范例是激活->生成数据->应用->结束
。有时需要将外部的数据传递给GameplayAbilities
,为此GAS
提供了下述方式:
方法 | 描述 |
---|---|
Activate GameplayAbility by Event |
通过事件激活一个GameplayAbility 带有一个Payload 。对于本地预测的GameplayAbilities ,事件的Payload 将会从Client 传递至Server 。Payload 除了包含一些变量外,还可以使用两个可选的Objects 或者一个TargetData 。问题是不能使用输入绑定激活技能。要使用此项必须在GameplayAbility 中设置Triggers ,上面介绍过这里不再赘述 |
Use WaitGameplayEvent AbilityTask |
在技能激活后,可以使用WaitGameplayEvent AbilityTask 告诉GameplayAbility 监听带有Payload (格式同上)的事件。WaitGameplayEvent 的问题是将不会被网络复制仅能用于Local Only 或Server Only 的GameplayAbilities 。你可以自己编写支持Replicated 复制Payload 的AbilityTask |
Use TargetData |
使用一个自定义的TargetData 结构体是在客户端和服务器端之间传递数据的好方式,详见FGameplayAbilityTargetData |
Store Data on the OwnerActor or AvatarActor |
使用OwnerActor 或AvatarActor 存储可被复制的变量,或者任何能获得引用的其他对象。这种方法是最灵活的并且也可以与事件绑定激活的GameplayAbilities 一起工作。但并不能保证在需要时,同步的数据一定到达。要使用这种方法必须要能够确保数据复制已提前完成,这意味着如果设置Replicated Variable 后马上激活一个GameplayAbility 将不能保证由于潜在的数据包丢失而在接收器上发生的顺序。 |
GameplayAbilities
带有可选的消耗和冷却功能。Cost GEs
和Cooldown GEs
上面已经介绍过这里不再赘述。
在一个GameplayAbility
调用UGameplayAbility::Activate()
之前,首先会调用UGameplayAbility::CanActivateAbility()
,此方法中会检查ASC
是否能够负担技能开销(UGameplayAbility::CheckCost()
) 并且确保技能没有处在冷却中(UGameplayAbility::CheckCooldown()
)。
在GameplayAbility
调用Activate()
之后,技能激活的任何时间内都可以通过UGameplayAbility::CommitAbility()
提交Cost
和Cooldown
。设计者也可以根据需要使用UGameplayAbility::CommitCost()
和UGameplayAbility::CommitCooldown()
单独提交Cost
或Cooldown
。 提交Cost
和Cooldown
时会再次调用CheckCost()
和CheckCooldown
,因为在激活GameplayAbility
之后Owning ASC
的Attributes
可能被修改,导致提交消耗时可能无法满足。如果提交时prediction key是有效的则消耗和冷却是可以被本地预测的(locally predicted)。
对于实现细节详见CostGE
和CooldownGE
。
升级技能有两种常用的方法:
技能升级方法 | 描述 |
---|---|
基于新的等级重新赋予技能 | 先从ASC 中删除GameplayAbility 然后在服务器端基于新的等级重新赋予GameplayAbility 。如果技能此时处于激活状态会被终止 |
增加GameplayAbilitySpec 的Level |
在服务器端,找到GameplayAbilitySpec ,增加它的Level ,标记它为Dirty 以复制到Owning Client 。如果技能此时处于激活状态不会被终止 |
两种方法的主要区别是在技能升级时当前激活的技能是否会被终止。使用哪种方法依赖于你的 GameplayAbilities
,建议为你的UGameplayAbility
子类添加一个bool
变量控制使用哪种方式。
GameplayAbilitySets
是一个便利的UDataAsset
类,用于将其持有的带有输入绑定的GameplayAbilities
赋予给Characters
。GameplayAbilitySets
子类化可以添加额外的逻辑和属性。Paragon对于每一个英雄都会有一个与之对应的GameplayAbilitySet
。
这个类并不是必须要使用。示例工程在GDCharacterBase
和它的子类中完成了和GameplayAbilitySets
类似的功能。
传统的Gameplay Ability
生命周期涉及到最小两到三次的客户端到服务器端的RPC调用。
CallServerTryActivateAbility()
ServerSetReplicatedTargetData()
(可选)ServerEndAbility()
如果一个GameplayAbility
可以在一帧内原子性的执行完上述步骤,那么我们可以通过Batch(Combine)
优化这个工作流,将两到三个RPCs
合并到一个RPC
。GAS
提供了这种优化叫作Ability Batching
。使用Ability Batching
通常的示例是基于命中检测的枪(hitscan guns),射击、射线检测、将命中结果(TargetData
)发送给服务器、结束一气呵成。GASShooter提供了一个这样的示例。
半自动的枪(按一下射一发)是最佳示例,将CallServerTryActivateAbility()
,ServerSetReplicatedTargetData()
(the bullet hit result)和ServerEndAbility()
从三个RPCs
合并到一个RPC
。
全自动或连发的枪对第一发子弹将CallServerTryActivateAbility()
和ServerSetReplicatedTargetData()
这两个RPCs
合并到一个RPC
。随后的每一发子弹只有一个ServerSetReplicatedTargetData()
RPC
。最终,当停止射击时发送一个单独的RPC
ServerEndAbility()
。这是糟糕示例,我们仅将第一发子弹从两个RPCs
优化成了一个RPC
。这种情况也可以通过Gameplay Event
来触发技能,使用EventPayload
将子弹的TargetData
从客户端发送到服务器,这种方法的缺点是必须在Ability
的外部生成TargetData
,而Ability Batching
方式是在Ability
的内部生成。
Ability Batching
在ASC
中默认是被禁用的。要启用Ability Batching
需要重载ShouldDoServerAbilityRPCBatch()
并且返回true:
virtual bool ShouldDoServerAbilityRPCBatch() const override { return true; }
现在Ability Batching
已经启用,接下来在激活要批处理的技能之前,必须预先创建一个FScopedServerAbilityRPCBatcher
结构体。在其后的且在其作用域内的任何Abilities
都将尝试Ability Batching
。FScopedServerAbilityRPCBatcher
的工作原理是在每个可批处理的函数中都有特殊的代码,这些特殊代码可拦截发送RPC的调用,并将消息打包为批处理结构。当FScopedServerAbilityRPCBatcher
超出作用域时,将在UAbilitySystemComponent::EndServerAbilityRPCBatch()
中自动把这个批处理结构发送到服务器,服务器会在UAbilitySystemComponent::ServerAbilityRPCBatch_Internal(FServerAbilityRPCBatch& BatchInfo)
中接收到这个Batch RPC
,BatchInfo
参数包含了一些标记:技能是否结束,激活技能时是否有输入按下,是否有TargetData
。调试Ability Batching
是否工作正确时可以在这里设置断点。也可以使用AbilitySystem.ServerRPCBatching.Log 1
开启Ability Batching
的日志。
这个机制只能在C++中使用并且仅能通过FGameplayAbilitySpecHandle
激活技能。
bool UGSAbilitySystemComponent::BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately)
{
bool AbilityActivated = false;
if (InAbilityHandle.IsValid())
{
FScopedServerAbilityRPCBatcher GSAbilityRPCBatcher(this, InAbilityHandle);
AbilityActivated = TryActivateAbility(InAbilityHandle, true);
if (EndAbilityImmediately)
{
FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(InAbilityHandle);
if (AbilitySpec)
{
UGSGameplayAbility* GSAbility = Cast(AbilitySpec->GetPrimaryInstance());
GSAbility->ExternalEndAbility();
}
}
return AbilityActivated;
}
return AbilityActivated;
}
GASShooter为半自动和全自动的射击复用了相同的支持批处理的GameplayAbility
并且永远不会直接调用EndAbility()
(整个射击一共由两个技能实现:一个是仅本地执行用于根据玩家输入和当前射击模式激活批处理射击的一个技能,其也会负责调用批处理射击的EndAbility()
。第二个是真正完成射击逻辑的技能,也就是这里的支持批处理GameplayAbility
)。由于所有的RPCs
必须发生在FScopedServerAbilityRPCBatcher
作用域之内,我提供了一个参数EndAbilityImmediately
用以控制是否将EndAbility()
纳入批处理,用以区分半自动射击和全自动射击。
GASShooter暴露了一个蓝图节点用以在仅本地执行的技能中激活批处理技能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eLK1rupZ-1591700466350)(https://github.com/tranek/GASDocumentation/raw/master/Images/batchabilityactivate.png)]
一个GameplayAbility
的网络安全策略决定技能在网络上何处执行,这可以防止客户端尝试执行受限功能。
网络安全策略 | 描述 |
---|---|
ClientOrServer |
没有安全要求,客户端和服务器可以自由的执行和终止技能 |
ServerOnlyExecution |
客户端发送的执行请求会被服务器端忽略。客户端仅能请求取消或结束技能 |
ServerOnlyTermination |
客户端发送的取消或结束技能的请求会被服务器端忽略。客户端仅能请求技能的执行 |
ServerOnly |
服务器控制技能的执行和终止。客户端的任何请求都会被忽略 |
GameplayAbilities
仅能执行一帧,其本身并没有太大的灵活性。要在技能的持续过程中做一些事情或者在稍后响应委托回调,我们可以使用AbilityTasks
。
GAS
带了很多拆箱即用的AbilityTasks
:
RootMotionSource
的角色移动任务同时最多只能运行1000个并行的AbilityTasks
(见UAbilityTask
的构建方法)。在设计GameplayAbilities
时需要谨记这一点,一个RTS
游戏在同一时间可能有几百个角色。
通常你将在C++中创建自定义的AbilityTasks
。示例项目有两个自定义的AbilityTasks
:
PlayMontageAndWaitForEvent
组合了默认的PlayMontageAndWait
和WaitGameplayEvent
AbilityTasks
。这将允许动画蒙太奇通过AnimNotifies
给播放它的技能发送事件,使用这个在指定的动画播放节点触发行为。WaitReceiveDamage
监听OwnerActor
受到伤害,被动护甲技能在英雄受到伤害时删除一层护甲。AbilityTasks
的构成:
AbilityTask
的静态方法AbilityTask
完成任务时广播的委托Activate()
方法,开始主要任务,绑定外部委托等OnDestroy()
方法,执行清理工作,包括清理已经绑定的委托注意: AbilityTasks
仅能定义一种类型的输出委托,所有的输出委托都必须是这种类型,无论它们是否使用参数,对于未使用参数的委托传递默认值。
AbilityTasks
仅在拥有GameplayAbility
的客户端或服务器上执行。不过,AbilityTasks
能够通过在构造方法中设置bSimulatedTask = true
,重载virtual void InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent);
使其运行在Simulated Clients
上。也可以复制任意数量的成员变量。这仅用于极少的情况,比如模拟整个移动的AbilityTasks
,你肯定不想复制每一次的移动变化。所有的RootMotionSource
AbilityTasks
都是这么做的。 详见AbilityTask_MoveToLocation.h/.cpp
。
AbilityTasks
也可以通过在AbilityTask
的构造方法设置bTickingTask = true
开启Tick
,还需要重载virtual void TickTask(float DeltaTime)
。这在需要流畅的插值时将非常有用。 详见AbilityTask_MoveToLocation.h/.cpp
。
在C++中创建和激活一个AbilityTask
(来自GDGA_FireGun.cpp
):
UGDAT_PlayMontageAndWaitForEvent* Task = UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(this, NAME_None, MontageToPlay, FGameplayTagContainer(), 1.0f, NAME_None, false, 1.0f);
Task->OnBlendOut.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnCompleted.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnInterrupted.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->OnCancelled.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->EventReceived.AddDynamic(this, &UGDGA_FireGun::EventReceived);
Task->ReadyForActivation();
在蓝图中, 仅用一个创建AbilityTask
的蓝图节点即可,并不需要调用ReadyForActivate()
,它将被Engine/Source/Editor/GameplayTasksEditor/Private/K2Node_LatentGameplayTaskCall.cpp
自动调用。 K2Node_LatentGameplayTaskCall
也会自动调用BeginSpawningActor()
和FinishSpawningActor()
如果你的AbilityTask
类中存在的话(详见AbilityTask_WaitTargetData
)。再次重审,K2Node_LatentGameplayTaskCall
仅对蓝图完成这些。在C++
中,需要手动调用ReadyForActivation()
, BeginSpawningActor()
, 和 FinishSpawningActor()
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hX04e0NF-1591700466351)(C:\Users\X\Desktop\abilitytask.png)]
要取消AbilityTask
,可以在蓝图或C++通过AbilityTaskProxy
对象调用EndTask()
。
有些AbilityTasks
不会在GameplayAbility
结束时自动结束比如WaitTargetData
,这些需要在GameplayAbility
的OnEndAbility
中手动结束(WaitTargetData
在用户输入Confirm
或Cancel
时将自然结束)。
GAS
带有一些能够处理角色移动的AbilityTasks
,比如对于角色击退、复杂的跳跃、拉、冲撞。其本质是使用连接到CharacterMovementComponent
的Root Motion Sources
。
注意: 可预测的RootMotionSource
AbilityTasks
在4.19和4.25+版本能够正常工作,在4.20-4.24有BUG(如果要使用这些版本,可以自行修正详见prediction fix)。
GameplayCues
(GC
) 执行非游戏性相关的事情,比如音效,粒子特效,震屏等。GameplayCues
通常会被复制和预测(除非设置Executed
, Added
或Removed
是本地的)。
我们通过发送必须以GameplayCue
开始的GameplayTag
触发GameplayCues
并且通过ASC
向GameplayCueManager
指定事件类型(Executed
, Added
或Removed
)。
GameplayCueNotify
对象和其他实现了IGameplayCueInterface
接口的Actors
能够订阅基于 GameplayCueTag
的事件。
注意: 再次重审,GameplayCue
GameplayTags
必须要以GameplayCue
开始。一个正确的示例:GameplayCue.A.B.C
。
有两种类型的GameplayCueNotifies
,Static
和Actor
。他们响应不同的事件,并且需要不同类型的GameplayEffects
来触发。使用时根据你的需要重载对应的事件即可。
GameplayCue Class |
事件 | GameplayEffect 类型 |
描述 |
---|---|---|---|
GameplayCueNotify_Static |
Execute |
Instant or Periodic |
静态GameplayCueNotifies 将使用ClassDefaultObject (意味着没有实例),主要用于一次性的效果比如击中效果 |
GameplayCueNotify_Actor |
Add or Remove |
Duration or Infinite |
Actor GameplayCueNotifies 当被Added 时将创建一个新的实例。这些实例能在持续时间内一直工作直到他们被Removed 。主要用于在Duration 或Infinite GameplayEffect 的持续期间循环播放音效或粒子特效,当然也可以手动删除他们。他们也带有一个选项用来管理能Added 多少个相同的GameplayCueNotify_Actor ,因此对同一个目标多次应用相同的效果比如音效和粒子可以使其只执行一次 |
从技术上讲,GameplayCueNotify
可以响应任何事件,但上述内容通常是我们使用它们的方式。
注意: 当使用GameplayCueNotify_Actor
时要勾选Auto Destroy on Remove
否则后续通过GameplayCueTag
调用Add
将不能工作。
当ASC
的Replication Mode不是Full
时,在服务器玩家(Listen Server
)上Add
and Remove
GC
的事件将会触发两次,一次是应用GE,另一次是通过NetMultiCast
广播给客户端,然而,WhileActive
事件仅会触发一次。所有事件在客户端仅触发一次。
示例项目使用GameplayCueNotify_Actor
实现了一个眩晕和一个冲刺效果。还使用了一个GameplayCueNotify_Static
实现子弹击中效果。这些GC
能够通过本地触发进一步优化,而不是通过GE复制它们,详见示例项目。
当GameplayEffect
被成功应用时,配置在GameplayTags
的所有GameplayCues
都会被触发。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0irxy9Io-1591700466352)(C:\Users\X\Desktop\gcfromge.png)]
UGameplayAbility
提供了相应的蓝图节点用于Execute
, Add
或Remove
GameplayCues
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADZ0WB9O-1591700466352)(C:\Users\X\Desktop\gcfromga.png)]
在C++中,可以直接调用ASC
中的这些方法 (或者在你ASC
的子类中将这些方法暴露给蓝图):
/** GameplayCues can also come on their own. These take an optional effect context to pass through hit result, etc */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Add a persistent gameplay cue */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Remove a persistent gameplay cue */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
/** Removes any GameplayCue added on its own, i.e. not as part of a GameplayEffect. */
void RemoveAllGameplayCues();
从GameplayAbilities
和ASC
暴露的用于触发GameplayCues
的方法默认会复制。每一个GameplayCue
事件都是一个多播RPC
,这将导致大量的RPCs
。GAS
也强制每次网络更新相同的GameplayCue
RPCs
最大只有两个,我们可以使用本地GameplayCues
解决此问题,Local GameplayCues
仅能在每个客户端独立的执行Execute
, Add
或Remove
。
什么情况下会使用Local GameplayCues
:
GameplayCues
Local GameplayCue
相关方法可以添加到ASC
的子类中:
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
void UPAAbilitySystemComponent::ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Executed, GameplayCueParameters);
}
void UPAAbilitySystemComponent::AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::OnActive, GameplayCueParameters);
}
void UPAAbilitySystemComponent::RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters & GameplayCueParameters)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Removed, GameplayCueParameters);
}
如果一个GameplayCue
是本地添加的,它也会被本地删除。如果它是由Replication
添加的,也会被Replication
删除。
GameplayCues
接收一个FGameplayCueParameters
结构体作为其参数其中包含了一些额外的信息。如果你通过GameplayAbility
或者 ASC
中的方法手动激活一个GameplayCue
,那么你也必须手动填充一个GameplayCueParameters
用以传递给GameplayCue
。如果GameplayCue
是被GameplayEffect
触发,那么GameplayCueParameters
将会被自动填充。
GameplayEffect
has an Attribute
for magnitude selected in the dropdown above the GameplayCue
tag container and a corresponding Modifier
that affects that Attribute
)在GameplayCueParameters
结构体中的SourceObject
变量可能是当手动激活GameplayCue
时向其传递任意数据的好地方。
**注意:**在参数结构体中的一些变量像Instigator
可能已经存在于EffectContext
之中。EffectContext
也包含一个FHitResult
用于指定GameplayCue
在世界中生成的位置。通过子类化EffectContext
向GameplayCues
传递更多数据可能是一种好的方式,尤其是当GameplayCues
是由GameplayEffect
触发时。
下面UAbilitySystemGlobals
中的三个方法用于自动填充GameplayCueParameters
结构,这些是可以被重载的virtual
方法。
/** Initialize GameplayCue Parameters */
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectSpecForRPC &Spec);
virtual void InitGameplayCueParameters_GESpec(FGameplayCueParameters& CueParameters, const FGameplayEffectSpec &Spec);
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectContextHandle& EffectContext);
默认情况下,GameplayCueManager
将会搜索整个游戏目录寻找GameplayCueNotifies
并且在游戏时将它们加载到内存。我们可以通过DefaultGame.ini
改变GameplayCueManager
搜索的目录:
[/Script/GameplayAbilities.AbilitySystemGlobals]
GameplayCueNotifyPaths="/Game/GASDocumentation/Characters"
我们想要GameplayCueManager
检索到所有的GameplayCueNotifies
,但并不希望它在游戏开始的时候异步加载所有的GameplayCueNotifies
。因为这将导致其引用的所有声音和粒子特效等资源也会被加载到内存中,无论当前关卡是否需要。在大体量的游戏中比如Paragaon,这将会导致在内存中存在几百兆不需要的资源并且可能还会导致在游戏开始时的冻结。对于在开始游戏时就加载每一个GameplayCue
,另一个方案是在游戏中真正使用GameplayCue
时才进行异步加载,用哪个加载哪个。这将会缓解不必要的内存开销,加载GameplayCue
时的游戏冻结会变成小的游戏卡顿,在游戏过程中GameplayCue
第一次被触发时其效果也可能会延迟。到目前为止,这是我推荐的解决方案,直到我们发现更好的方法。
首先必须要子类化UGameplayCueManager
,然后在DefaultGame.ini
中配置我们的UGameplayCueManager
子类:
[/Script/GameplayAbilities.AbilitySystemGlobals]
GlobalGameplayCueManagerClass="/Script/ParagonAssets.PBGameplayCueManager"
在 UGameplayCueManager
子类中重载ShouldAsyncLoadRuntimeObjectLibraries()
:
virtual bool ShouldAsyncLoadRuntimeObjectLibraries() const override
{
return false;
}
有些时候我们不希望GameplayCues
被触发,比如当我们格档了一次攻击时不希望播放Damage GameplayEffect
上的受击效果或者希望换一个效果。这时我们可以在GameplayEffectExecutionCalculations
中调用OutExecutionOutput.MarkGameplayCuesHandledManually()
并且手动发送我们想要的GameplayCue
事件给Target
或 Source
的 ASC
即可。
如果在特定的ASC
上不想触发任何GameplayCues
,可以设置AbilitySystemComponent->bSuppressGameplayCues = true
。
每一个GameplayCue
的触发都是一次不可靠的广播RPC
,当我们同时触发多个GCs
时,有几种优化的方法将他们压缩到一个RPC
中或是发送少量的数据以节省带宽。
假设你有一把霰弹枪能够同时发射8发弹丸,有8个射线检测和8个GameplayCues
。GASShooter使用了一种懒方法,通过将射线检测信息以TargetData
的形式保存到EffectContext
中将其组合到一个RPC
中。当然这种作法只是将RPCs
从8个减少到1个,但还是会在这个RPC
中通过网络发送大量的数据(大概是500字节)。一种更好的方法是,发送一个自定义结构体的RPC
,将击中的位置信息或者一个能在接收端重建位置信息的随机种子编码到这个结构体中,在客户端通过这个自定义的结构体触发 locally executed GameplayCues
即可。
具体步骤:
FScopedGameplayCueSendContext
,它将阻止UGameplayCueManager::FlushPendingCues()
直到超出其作用域。这意味着所有在其作用域内的 GameplayCues
都将进行排队。UGameplayCueManager::FlushPendingCues()
,根据GameplayTag
将能够合并到一个批次的GameplayCues
保存到自定义结构体中,调用RPC
将其发送到客户端。GameplayCues
中。这种方法还能将一些特定的参数传递给GameplayCues
,这些参数并不在GameplayCueParameters
中并且你也不想将它们添加到EffectContext
中,比如伤害数字,是否暴击,是否破盾,是否致使一击等。
https://forums.unrealengine.com/development-discussion/c-gameplay-programming/1711546-fscopedgameplaycuesendcontext-gameplaycuemanager
在一个GameplayEffect
上的所有GameplayCues
已经通过一个RPC
发送。默认情况下,UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec()
将会通过不可靠的多播发送整个GameplayEffectSpec
(但会转换成FGameplayEffectSpecForRPC
)而无论ASC
的Replication Mode
是什么。根据GameplayEffectSpec
中包含的内容这有可能需要大量的带宽,不过可以通过设置cvar AbilitySystem.AlwaysConvertGESpecToGCParams 1
尝试进行优化。开启这个选项后将会在RPC
的过程中把GameplayEffectSpecs
转换成FGameplayCueParameter
这样就不用发送整个FGameplayEffectSpecForRPC
了。这将可能会节省带宽但也会少了一些信息,这依赖于GESpec
是如何转换成GameplayCueParameters
的,且必须知晓你的GCs
需要哪些信息。
AbilitySystemGlobals
类持有GAS
的全局信息。其中的大多数变量可以通过DefaultGame.ini
设置。通常并不需要与这个类进行交互,但需要知道它的存在。如果需要子类化GameplayCueManager
或GameplayEffectContext
,则需要通过派生AbilitySystemGlobals
完成。
派生AbilitySystemGlobals
后需要在DefaultGame.ini
中进行设置:
[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"
从UE4.24开始,要使用TargetData
必须要调用UAbilitySystemGlobals::InitGlobalData()
,否则你会遇到ScriptStructCache
相关的错误且客户端将从服务器断开连接。在项目中这个方法只需要调用一次。堡垒之夜是在AssetManager
类的开始初始加载方法调用,帕拉贡是在UEngine::Init()
中调用。示例项目在UEngineSubsystem::Initialize()
中调用,建议将这个直接拷贝到你的项目中以解决TargetData
相关的问题。
如果在使用AbilitySystemGlobals
GlobalAttributeSetDefaultsTableNames
时崩溃,则需要将UAbilitySystemGlobals::InitGlobalData()
的调用置后一些,将其放到AssetManager
或GameInstance
中而不是UEngineSubsystem::Initialize()
中。崩溃的原因是因为子系统的初始化顺序导致,GlobalAttributeDefaultsTables
需要EditorSubsystem
已被加载以绑定UAbilitySystemGlobals::InitGlobalData()
里的一个委托。
GAS
支持拆箱即用的客户端预测(预判)功能,然而它并不能预测所有事情。GAS
中的客户端预测意味着客户端不必等待服务器的许可就可以激活一个GameplayAbility
并应用GameplayEffects
。它也能预测做这件事需要的服务器许可和应用GameplayEffects
的目标。在客户端激活后,服务器将在网络延迟时间后执行GameplayAbility
并且告诉客户端它的预测执行是否正确。如果客户端的预测是错误的,它将从错误的预测中回滚它的修改匹配到服务器。
GAS
预测相关代码在插件的GameplayPrediction.h
文件中。
Epic的倾向是仅预测可以回滚的,例如Paragon和Fortnite都不预测伤害,这两个游戏很可能使用ExecutionCalculations
(不支持预测)做伤害计算。这并不是说你一定不能预测这些事情,只要符合需求并且能够工作。
我们也不是“无缝地,自动地预测一切”的解决方案。 我们仍然认为,将玩家的预测最好保持在最低限度。
摘自Epic的Dave Ratti新的网络预测插件
Network Prediction Plugin
什么是可以预测的:
- 技能激活
- 触发事件
GameplayEffect
的应用:
Attribute
修改 (注意:Executions
当前不能被预测,只有只有属性Modifiers
才可以被预测)GameplayTag
修改Gameplay Cue
事件- 动画蒙太奇
- 移动 (
UCharacterMovement
)
什么是不可预测的:
GameplayEffect
移除GameplayEffect
周期效果
来自GameplayPrediction.h
(译者注:一个注释比代码多的头文件,如果要使用客户端预测强烈建议读此文件)
我们可以预测GameplayEffect
的应用,却不能预测GameplayEffect
的移除。解决这个问题的一种方式是预测我们想要移除的GameplayEffect
的反效果。比如说我们预测一个移动速度减缓40%的效果,我们可以通过应用一个移动速度加速40%的效果来当作删除它,最后同时删除这两个GameplayEffects
。当然这并不能满足所有GameplayEffect
移除的情况。Epic的Dave Ratti表示希望在GAS
后续的迭代中支持这个。
因为我们不能预测GameplayEffects
的移除,我们不能完整的预测GameplayAbility
的冷却,并没有与之对应的反效果。服务器复制的Cooldown GE
将存在于客户上,并且任何绕过此操作的尝试将被服务器拒绝。 这意味着高延迟的客户端需要花费更长的时间告诉服务器进行冷却并接受服务器的Cooldown GE
的移除。这将导致高延迟的玩家射击频率会低于低延迟的玩家,低延迟的玩家比高延迟的玩家有优势。Fortnight使用自定义的统计替代了Cooldown GEs
以解决此问题。
就预测伤害而论,我个人并不推荐,尽管它是大多数人开始使用GAS时首先尝试的事情之一。我也强烈建议不要预测死亡。虽然你可以预测伤害,但这很棘手。如果你错误的预测了应用伤害,玩家会看到敌人的生命值跳回。如果要预测死亡,这可能会更加奇葩。假设你误预测了角色的死亡刚开始了布娃娃模拟,当服务器校正后这个角色突然停止了布娃娃模拟并且开始朝你射击,这种体验太奇怪了。
注意: Instant
GameplayEffects
(像Cost GEs
) 预测修改你自己的属性是无缝的, 预测Instant
Attribute
对其他角色的修改将会在他们的属性上表现短暂的异常。可预测的Instant
GameplayEffects
预测失败时,可以想像成是Infinite
GameplayEffects
的回滚。当服务器的GameplayEffect
被应用时,可能存在两个相同的GameplayEffect
,这将导致短时间内Modifier
被应用两次或根本不应用。最终它会自行修正,但有时玩家会注意到此问题。
GAS
的预测实现尝试解决的问题:
- “我可以这样做吗?” 预测的基本协议
- “Undo”当预测失败时如何撤消副作用
- “Redo”如何避免重播我们在本地预测但也会从服务器复制的副作用
- “完整性”如何确定我们预测了所有副作用
- “依赖性”如何管理依赖性预测和预测事件链
- “覆盖”如何预测性地覆盖服务器原本已复制/拥有的状态。
来自GameplayPrediction.h
GAS
的预测机制基于一个叫作预测键(Prediction Key
)的概念,它是一个当客户端激活一个GameplayAbility
时在客户端生成的整型标识符。
GameplayAbility
时生成一个预测键( Activation Prediction Key
)CallServerTryActivateAbility()
将这个预测键发送给服务器GameplayEffects
GameplayAbility
中进一步预测效果需要一个新的 Scoped Prediction WindowGameplayEffects
GameplayEffects
,如果复制回的GameplayEffects
与客户端应用的GameplayEffects
有相同的预测键,则预测正确。此时在目标上将有两个GameplayEffect
直到客户端删除它预测的那一个Replicated Prediction Key
),这个预测键现在被标记为阵旧的GameplayEffects
,服务器复制回的GameplayEffects
将被保持。任何客户端添加的且没有接收到匹配的服务器返回版本的GameplayEffects
都是预测失败的预测键在通过Activation Prediction Key
激活的GameplayAbilities
的原子指令组(也叫作Window
)中保证是有效的。你也可以想像成仅在一帧有效。任何AbilityTasks
中的回调将不再有一个有效的预测键除非AbilityTask
中有内建的同步点生成一个新的Scoped Prediction Window。
要在AbilityTasks
的回调中预测更多行为,我们需要使用一个新的Scoped Prediction Key
创建一个新的Scoped Prediction Window
。这有时也被称作在客户端和服务器间的一个同步点(Synch Point
)。一些AbilityTasks
,比如所有输入相关的AbilityTasks
内建了创建新的Scoped Prediction Window
的功能,意味着在AbilityTasks
的回调方法中的原子性代码可以使用一个有效的Scoped Prediction Key
。其他任务,像WaitDelay
则没有内置的代码为它的回调方法创建新的Scoped Prediction Window
。如果你需要为一个没有内置代码创建新的Scoped Prediction Window
的AbilityTask
预测行为(像上述的WaitDelay
),我们可以通过手动调用OnlyServerWait
选择的WaitNetSync
完成。当客户端遇到带有OnlyServerWait
的WaitNetSync
它将基于GameplayAbility
的Activation Prediction Key
生成一个新的Scoped Prediction Key
,通过RPC
将其传递给服务器,然后将其添加给它应用的新的GameplayEffects
。当服务器端遇到带有OnlyServerWait
的WaitNetSync
,它将等待直到它从客户端收到新的Scoped Prediction Window
才会继续。接下来Scoped Prediction Key
要做的和Activation Prediction Key
一样。Scoped Prediction Key
超出作用域时失效,意味着Scoped Prediction Windows
已经关闭。再讲一次,仅不能延迟的原子操作才可以使用Scoped Prediction Key
。
你可以根据需要创建Scoped Prediction Windows
。
如果你想在自定义的AbilityTasks
中加入同步点功能,可以参考WaitNetSync
。
**注意:**当使用 WaitNetSync
时,其会阻塞服务器上GameplayAbility
的执行直到收到客户端的消息。这可能会被恶意的玩家滥用,他们会攻击游戏故意延迟发送新的Scoped Prediction Key
。Epic很少使用WaitNetSync
,如果你有这种困扰,建议你创建一个新版本的可以延迟自动继续的AbilityTask
(指定时间内收不到客户端的消息就跳过等待)。
示例项目在冲刺技能中使用了WaitNetSync
创建了一个新的Scoped Prediction Windows
,这使得每次应用耐力消耗都能够进行预测。理想情况下,在应用消耗和冷却时我们想要一个有效的Prediction Key
。
如果你有一个预测的 GameplayEffect
在其所属客户端上播放了两次,你的预测密钥已过期并且遇到了"Redo"问题。这通常可以在应用GameplayEffect
之前放一个带有OnlyServerWait
的WaitNetSync
以创建一个新的Scoped Prediction Key
解决。
在客户端可预测的生产Actors
是一个高级主题,GAS
没有提供拆箱即用的功能(SpawnActor
AbilityTask
仅在服务器端生产Actor
)。核心点是在客户端和服务器端都生产一个复制的Actor
。
如果Actor
仅用于视觉表现或者不是任何游戏性相关的目的,有一个简单的方案可以满足此需求。重载Actor
的 IsNetRelevantFor()
方法阻止从服务器将其复制到所属客户端。所属客户端仅需要本地生产的版本,服务器和其他客户端使用服务器的已复制版本。
bool APAReplicatedActorExceptOwner::IsNetRelevantFor(const AActor * RealViewer, const AActor * ViewTarget, const FVector & SrcLocation) const
{
return !IsOwnedBy(ViewTarget);
}
如果生产的Actor
会影响到游戏性(比如子弹,需要预测伤害),那么你需要更高级的方案这超出了此文档的范围。可以在Epic Games的GitHub中找到UnrealTournament项目参考其中如何实现的可预测生产子弹。它们仅在所属客户端上生成一个虚拟子弹,该虚拟子弹与服务器的复制子弹同步。
将来官方可能会在GameplayPrediction.h
中加入GameplayEffect
的移除预测和周期性GameplayEffects
的预测。
来自Epic的Dave Ratti表达了有兴趣解决冷却预测导致的低延迟玩家比高延迟玩家有优势的问题。
预计由Epic开发的新插件Network Prediction
将能与GAS
完全互用就像CharacterMovementComponent
一样。
Epic最近发起了一项计划,用新的Network Prediction
插件替换CharacterMovementComponent
。 该插件仍处于早期阶段,但仍可以在Unreal Engine GitHub上提早访问。现在还不确定此插件在未来哪个版本的引擎亮相。
FGameplayAbilityTargetData
是一个能在网络上传递用于描述目标数据的通用结构体。TargetData
通常将持有AActor
或UObject
的引用以及FHitResults
和位置/朝向/原点信息。不过,你也可以通过子类化在其中加入任何你需要的东西,这是一种通过GameplayAbilities
在客户端和服务器端之间传递数据的简单方法。不要直接使用FGameplayAbilityTargetData
结构体而应使用它的子类。在GAS
的GameplayAbilityTargetTypes.h
中包含了几个能够被直接使用的FGameplayAbilityTargetData
派生类。
TargetData
通常是由Target Actors
产生或者是手动创建,它会被AbilityTasks
和GameplayEffects
(通过EffectContext
)消耗。TargetData
作为EffectContext
的结果时,Executions
, MMCs
, GameplayCues
和AttributeSet
的[Pre|Post]GameplayEffectExecute
方法都可以访问它。
通常我们不会直接传递FGameplayAbilityTargetData
,而是使用一个FGameplayAbilityTargetDataHandle
,其内部保存了一个FGameplayAbilityTargetData
指针的TArray。
GameplayAbilities
使用WaitTargetData
AbilityTask
生产TargetActors
,其作用是呈现和捕获世界中的目标信息。TargetActors
可以使用可选的GameplayAbilityWorldReticles
显示当前的目标。在目标选择确认之后,目标信息将会以TargetData
的形式返回,然后将其传递给GameplayEffects
。TargetActors
本质是AActor
因此他们可以有任何的显示组件(static meshes
或者 decals
)用以呈现在哪以及如何选择目标。Static Meshes
被用来显示你的角色将要构建的一个对象(堡垒之夜的建造模式)。Decals
用来显示地面上的作用区域。示例项目使用带有一个Decal
的AGameplayAbilityTargetActor_GroundTrace
呈现陨石技能的伤害区域。TargetActors
也可以不显示任何东西,比如GASShooter
中的霰弹枪会直接使用射线检测目标而不需要显示任何东西。
TargetActors
使用基本的射线检测或者碰撞检测获得目标信息并根据TargetActor
的实现方式将结果转换成FHitResults
或AActor
的数组保存到TargetData
。WaitTargetData
AbilityTask
通过TEnumAsByte
参数决定目标何时被确定。当参数不是TEnumAsByte
TargetActor
通常会在Tick()
中执行trace/overlap并且根据它的实现更新FHitResult
的位置。要注意它使用了Tick()
并且复杂的TargetActors
可能会做很多的事情,比如GASShooter
中的火箭筒的第二技能。当在Tick()
上追踪时对客户端是非常敏感的,如果消耗了太多性能你可能要考虑降低TargetActor
的Tick频率。当参数是TEnumAsByte
TargetActor
会立即产生,并生产TargetData
,然后销毁(Tick()
将不会被调用)。
EGameplayTargetingConfirmation::Type |
何时确定目标 |
---|---|
Instant |
触发目标选取时立即确定,不需要额外逻辑或者用户输入决定 |
UserConfirmed |
当技能绑定了Confirm 目标选取由用户确定或者是通过调用UAbilitySystemComponent::TargetConfirm() 确定。取消同理可以绑定Cancel 或者调用UAbilitySystemComponent::TargetCancel() |
Custom |
通过技能的UGameplayAbility::ConfirmTaskByInstanceName() 确定选取,通过技能的UGameplayAbility::CancelTaskByInstanceName() 取消选取 |
CustomMulti |
要使用的方法同上,只是在数据产生时不结束AbilityTask ,可用于选取多个目标 |
并不是每个TargetActor
都会支持上述所有EGameplayTargetingConfirmation::Type
。例如AGameplayAbilityTargetActor_GroundTrace
不支持Instant
。
WaitTargetData
AbilityTask
带有一个AGameplayAbilityTargetActor
的参数,在每一次AbilityTask
激活时创建TargetActor
的实例,AbilityTask
结束时销毁这个实例。WaitTargetDataUsingActor
AbilityTask
则可以利用一个已存在的TargetActor
,且在AbilityTask
结束时并不会销毁它。这两种AbilityTasks
效率都很低,它们都会生产或请求生产一个新的TargetActor
。对于制作游戏原型它们很方便,但在生产中当需要不断的产生TargetData
时(比如自动步枪)则需要对此进行优化。GASShooter
中有一个自定义AGameplayAbilityTargetActor
子类和一个新的WaitTargetDataWithReusableActor
AbilityTask
,它们可以复用一个TargetActor
而不会进行销毁。
TargetActors
默认情况下不会被复制,但是当在你的游戏中需要将本地玩家的目标选择过程显示给其他玩家时也可以将TargetActors
改为被复制。WaitTargetData
AbilityTask
中包含了通过RPCs
与服务器通讯的默认功能。如果TargetActor
的ShouldProduceTargetDataOnServer
为false
,在确定选择目标时会通过在UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()
方法中调用CallServerSetReplicatedTargetData()
使客户端把TargetData
通过RPC
传递给服务器。当ShouldProduceTargetDataOnServer
为true
,客户端将发送一个确定事件(EAbilityGenericReplicatedEvent::GenericConfirm
),通过在UAbilityTask_WaitTargetData::OnTargetDataReadyCallback
中调用RPC
方法ServerSetReplicatedEvent()
传递给服务器,然后服务器基于接收到的RPC
将会执行射线或碰撞检测并产生TargetData
。如果客户端取消了选取目标,将会发送一个取消事件(EAbilityGenericReplicatedEvent::GenericCancel
),在UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback
中执行上述类似过程,这里不再赘述。就像你所见,TargetActor
和 WaitTargetData
AbilityTask
包含了大量的委托。TargetActor
响应输入产生并且广播 TargetData
就绪,确定,取消的委托。WaitTargetData
则监听TargetActor
的TargetData
就绪,确定和取消的委托并且将结果返回给GameplayAbility
和服务器。如果是客户端将TargetData
发送给服务器,还需要进行反作弊处理。如果直接在服务器产生TargetData
会解决上述问题,但可能会导致所属客户端的误判。
根据使用的AGameplayAbilityTargetActor
的派生类,在WaitTargetData
AbilityTask
节点将会暴露不同的ExposeOnSpawn
参数。包含的一些公共参数:
Common TargetActor Parameters |
Definition |
---|---|
Debug | 为true 时,在非Shipping 构建中将会绘制TargetActor 的射线或碰撞检测的信息 |
Filter | [可选]用于过滤检测到的结果,常用于过滤玩家角色,只检测特定类型的目标,或是通过子类化FGameplayTargetDataFilter 做更复杂的检测(比如团队) |
Reticle Class | [可选] TargetActor 会创建的AGameplayAbilityWorldReticle 的子类 |
Reticle Parameters | [可选] Reticles 的配置 |
Start Location | 追踪的起始位置,通常是玩家的视点,武器的枪口或者是玩家的位置 |
对于默认的TargetActor
,当Actors
处于追踪或碰撞时它们才是有效目标,一旦离开追踪或碰撞(它们移开了或者你转移了目光)它们将不再有效。如果你想让TargetActor
记住最近的有效目标,可以将这个功能添加到自定义的TargetActor
类中。我将这些称为永久目标,它们将一直有效直到收到目标的确定和取消事件,或者是TargetActor
找到了新目标,再或者是目标本身已经无效(比如Actor
已销毁)。GASShooter为火箭筒的第二技能使用了永久目标。
(Reticles
)显示了TargetActors
(必须是非Instant
)已确定的目标。TargetActors
负责所有Reticles
的创建和销毁。 Reticles
是AActors
因此它们可以使用任何可视化组件。GASShooter中常见的实现是使用WidgetComponent
在屏幕空间显示了一个UMG Widget
(总是朝向玩家摄相机)。Reticles
并不知道哪一个AActor
是他们的目标(但是你可以在自定义的TargetActor
中实现此功能),通常由TargetActors
在Tick()
中根据目标位置更新Reticle
的位置。
GASShooter使用Reticles
显示被火箭筒二技能锁定的目标,在下图中敌人身上的红色标识就是Reticle
。白色的是火箭筒的准星。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bl0yzoX1-1591700466353)(C:\Users\X\Desktop\gameplayabilityworldreticle.png)]
Reticles
提供给设计师一些有用的蓝图事件:
/** Called whenever bIsTargetValid changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnValidTargetChanged(bool bNewValue);
/** Called whenever bIsTargetAnActor changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnTargetingAnActor(bool bNewValue);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnParametersInitialized();
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamFloat(FName ParamName, float value);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamVector(FName ParamName, FVector value);
Reticles
可以使用由TargetActor
提供的FWorldReticleParameters
进行配置。默认的结构体仅提供了一个变量FVector AOEScale
,当需要更多的变量时可以派生FWorldReticleParameters
,当然也需要同时派生对应的TargetActor
(原本的TargetActor
仅能接受基类)。
Reticles
默认不会被复制,但当你的游戏需要将本地玩家的目标显示给其他玩家时可以打开复制。
默认的TargetActors
仅当存在有效目标时Reticles
才会显示,例如,你使用AGameplayAbilityTargetActor_SingleLineTrace
追踪一个目标,当敌人在追踪路径上时Reticle
才会显示出来如果你转移目光,当你转移目光敌人不再有效时Reticle
将会消失。如果想要Reticle
保持在最后一个有效目标上,你需要定制自己的TargetActor
用来记住最后的有效目标并且将Reticle
保持在目标上。
GameplayEffectContainers
带有TargetType
和GameplayEffects
,当EffectContainer
在客户端和服务器端被应用时立即获得目标并应用GameplayEffects
。这样比TargetActors
高效,因为它是运行在目标选取对象的CDO
上(不需要创建和销毁Actors
),但它会缺失玩家输入,只能被立即触发不能确认和取消选取,并且不能从客户端向服务器发送数据(因为它将同时在两端执行)。对于立即触发的射线或碰撞检测这将非常有效。Epic的ActionRPG
示例工程中在Containers
里包含了两种不同的目标选择类型,一个是选择技能施放者,一个是从事件中取得的TargetData
。它还在蓝图中实现了一个功能,可以在玩家特定的偏移(可以在子蓝图中设置)位置立即触发球体追踪。你也可以通过在C++或蓝图中子类化URPGTargetType
实现自己的目标选取类型。
眩晕可以打断一个角色正在施放的技能,阻止他施放新的技能,在整个眩晕的过程中阻止其移动。示例项目的陨石技能在击中的目标上应用了眩晕。
取消目标正在施放的技能,可以在stun GameplayTag
添加时调用AbilitySystemComponent->CancelAbilities()
。
在眩晕时阻止施放技能,可以在GameplayAbilities
的 Activation Blocked Tags
GameplayTagContainer
中添加stun GameplayTag
。
在眩晕时阻止角色移动,可以重载CharacterMovementComponent
的GetMaxSpeed()
方法,在其拥有者有stun GameplayTag
时返回0。
示例项目提供了冲刺技能-按住Left Shift
键角色会加速跑。
加速跑是可预测的,由CharacterMovementComponent
向服务器发送一个标记实现。详见GDCharacterMovementComponent.h/cpp
GA
处理Left Shift
的输入事件,通知CharacterMovementComponent
开始和停止加速,当Left Shift
按下后同时预测耐力。详见GA_Sprint_BP
示例项目处理瞄准和冲刺相似,但瞄准会降低移动速度。
可预测的降低移动速度,详见GDCharacterMovementComponent.h/cpp
。
处理输入详见GA_AimDownSight_BP
,瞄准时不会消耗耐力值。
我在伤害计算的ExecutionCalculation
中处理生命偷取。GameplayEffect
将有一个GameplayTag
比如Effect.CanLifesteal
。ExecutionCalculation
检查如果GameplayEffectSpec
有Effect.CanLifesteal
这个标签则动态创建一个动态的Instant
GameplayEffect
,并且给它一个增加生命值的Modifer
将其应用给Source
的ASC
。
if (SpecAssetTags.HasTag(FGameplayTag::RequestGameplayTag(FName("Effect.Damage.CanLifesteal"))))
{
float Lifesteal = Damage * LifestealPercent;
UGameplayEffect* GELifesteal = NewObject(GetTransientPackage(), FName(TEXT("Lifesteal")));
GELifesteal->DurationPolicy = EGameplayEffectDurationType::Instant;
int32 Idx = GELifesteal->Modifiers.Num();
GELifesteal->Modifiers.SetNum(Idx + 1);
FGameplayModifierInfo& Info = GELifesteal->Modifiers[Idx];
Info.ModifierMagnitude = FScalableFloat(Lifesteal);
Info.ModifierOp = EGameplayModOp::Additive;
Info.Attribute = UPAAttributeSetBase::GetHealthAttribute();
SourceAbilitySystemComponent->ApplyGameplayEffectToSelf(GELifesteal, 1.0f, SourceAbilitySystemComponent->MakeEffectContext());
}
有时你需要在GameplayAbility
中生成随机数比如用作射击的后座力或子弹扩散。为了要在客户端和服务器端要生成相同的随机数,我们需要在激活GameplayAbility
时设置相同的Random Seed
。每次激活GameplayAbility
时,都需要设置Random Seed
以防止客户端错误地预测了激活并且其随机数序列与服务器的序列不同步。
Seed Setting Method | Description |
---|---|
Use the activation prediction key | GameplayAbility 的activation prediction key 是一个int16且保证在客户端和服务器的Activation() 中是同步的和可用的。可以把它当作客户端和服务器的Random Seed 。问题是在每次游戏开始时prediction key 从0开始,在生成keys 的过程中持续增加。这意味着每次都是相同的随机数序列,这可能不足以满足你的需求 |
Send a seed through an event payload when you activate the GameplayAbility |
使用事件激活GameplayAbility 并且通过可复制的Event Payload 将随机生成种子从客户端发送到服务器。此方法会有更大的随机性,但客户端容易被破解每次都发送相同的种子值,而且通过事件激活的GameplayAbilities 也将无法再使用输入绑定激活 |
如果你的随机偏差很小,大部分玩家不会注意到每次游戏时序列是相同的,此时使用Activation Prediction Key
作为Random Seed
是可行的。如果你需要做一些复杂的事情,可能使用Server Initiated
GameplayAbility
在服务器创建Prediction Key
或生成随机种子然后通过事件Payload
发送是更好的选择。
我在伤害ExecutionCalculation
中处理致命一击。在ExecutionCalculation
检查GameplayEffect
是否带有Effect.CanCrit
标签,如果有会根据暴击率(Source
中的属性)生成一个随机数并将其加到暴击伤害中(同样来源于Source
)。因此我并没有预测伤害,也不需要担心随机数在客户端和服务器的同步问题因为ExecutionCalculation
仅在服务器上运行。如果你要使用支持预测的MMC
完成这个伤害计算,可以通过GameplayEffectSpec->GameplayEffectContext->GameplayAbilityInstance
获取Random Seed
。
查看GASShooter如何实现的爆头,本质和上述相同,只不过没有使用暴击率而是检查FHitResult
中的骨骼名字。
Paragon中的减速效果不会叠加,但每一个应用的减速效果都会像平常一样追踪自己的生命周期,不过只会应用最大的减速效果给角色。GAS
提供了AggregatorEvaluateMetaData
用于解决此问题。详见AggregatorEvaluateMetaData()
如果玩家在WaitTargetData
AbilityTask
中等待生成TargetData
时需要暂停游戏,建议使用slomo 0
替代pause
。
通常在调试GAS
相关问题时,需要了解如下事情:
- “我的属性值是什么?”
- “我有哪些游戏标签?”
- “我当前有哪些游戏效果?”
- “我有哪些技能, 哪些正在施放, 哪些不允许施放?”.
GAS
有两种方式在运行时回答上述问题: showdebug abilitysystem
和挂钩(hooks in)到GameplayDebugger
.
提示: UE4会优化C++代码,这将会调试一些方法变得困难。可以设置VS Solution配置为DebugGame Editor
阻止代码优化。也可以通过PRAGMA_DISABLE_OPTIMIZATION_ACTUAL
和PRAGMA_ENABLE_OPTIMIZATION_ACTUAL
两个宏阻止特定方法的优化。
PRAGMA_DISABLE_OPTIMIZATION_ACTUAL
void MyClass::MyFunction(int32 MyIntParameter)
{
// My code
}
PRAGMA_ENABLE_OPTIMIZATION_ACTUAL
在游戏控制台输入showdebug abilitysystem
。一共三页,每一页都会显示你当前拥有的 GameplayTags
。输入AbilitySystem.Debug.NextCategory
切换下一页。
第一页显示你所有属性的当前值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqGn1dTd-1591700466354)(https://github.com/tranek/GASDocumentation/raw/master/Images/showdebugpage1.png)]
第二页显示所有的BUFF(Duration
和Infinite
GameplayEffects
),它们的叠加数,给了哪些GameplayTags
,给了哪些Modifiers
:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IblFZ9w0-1591700466354)(https://github.com/tranek/GASDocumentation/raw/master/Images/showdebugpage2.png)]
第三页显示所有的拥有的 GameplayAbilities
,无论它们当前是否运行,无论它们是否被阻止施放。还有当前执行的AbilityTasks
的状态:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aBVviosN-1591700466355)(https://github.com/tranek/GASDocumentation/raw/master/Images/showdebugpage3.png)]
当使用PageUp
和PageDown
在目标间切换时,当前的页面只显示本地控制角色的ASC
。使用AbilitySystem.Debug.NextTarget
和 AbilitySystem.Debug.PrevTarget
切换目标将显示正确的ASCs
的数据,但表示当前选中目标的绿色框并没有随之更新。BUG已报告 https://issues.unrealengine.com/issue/UE-90437。
GAS
添加了一些功能给Gameplay Debugger
。可以通过单引号(')开启Gameplay Debugger
。按小键盘上的数字3开启Abilities Category
。
当你想查看其他角色的GameplayTags
, GameplayEffects
和GameplayAbilities
时可以使用Gameplay Debugger
。可惜的是它不能显示目标属性的当前值。它将会选取屏幕中间的角色,要切换角色可以再次按下单引号(')。
当前目标角色会有一个大红圈标识:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N2X7C3HD-1591700466355)(https://github.com/tranek/GASDocumentation/raw/master/Images/gameplaydebugger.png)]
GAS
源代码中针对不同的日志级别包含了大量的日志语句,这些日志通过ABILITY_LOG()
进行打印,默认日志级别是Display
,然后高于这个级别的日志不会打印。
通过下述方法改变要打印的日志级别:
log [category] [verbosity]
例如要打开 ABILITY_LOG()
:
log LogAbilitySystem VeryVerbose
重置回默认状态:
log LogAbilitySystem Display
显示全部日志类别:
log list
GAS
相关的日志类别:
Logging Category | Default Verbosity Level |
---|---|
LogAbilitySystem | Display |
LogAbilitySystemComponent | Log |
LogGameplayCueDetails | Log |
LogGameplayCueTranslator | Display |
LogGameplayEffectDetails | Log |
LogGameplayEffects | Display |
LogGameplayTags | Log |
LogGameplayTasks | Log |
VLogAbilitySystem | Display |
详见Wiki on Logging。
GameplayAbilities
的激活,发送TargetData
到服务器(可选),结束所有这些事情如果在一帧完成可以使用Ability Batching
将两到三个RPCs
优化到一个RPC
。这种类型的Abilities
常用于霰弹枪。
如果你在同时发送了多个GameplayCues
,也可以考虑将他们合并到一个RPC
中。这可以减少RPCs
的数量并且使发送的数据量尽可能的小。
默认情况下,ASC
处于Full Replication Mode
模式。这将会把所有的GameplayEffects
复制到每一个客户端(这对于单人游戏没问题)。在多人游戏中,将玩家拥有的 ASCs
设置为 Mixed Replication Mode
,将AI控制的角色设置为Minimal Replication Mode
。这将会把玩家角色上应用的GEs
只复制给其角色的拥有者,AI控制的角色上的GEs
将不会复制到客户端。GameplayTags
将会被复制,GameplayCues
将会被不可靠的广播发送给所有客户端,不管Replication Mode
是什么。当所有客户端不需要看到它们时,这将减少复制GEs
的网络数据。
在有大量玩家的游戏中比如Fortnite Battle Royale (FNBR),将有大量的ASCs
存在于对应的PlayerStates
中且会复制大量属性。要优化这个瓶颈,Fortnite禁用了ASC
和它的AttributeSets
在PlayerState::ReplicateSubobjects()
中一起同步给Simulated Player-controlled Proxies
。Autonomous Proxies
和AI Controlled
角色仍然根据其Replication Mode
进行全同步。取而代之的当要同步PlayerStates
中的ASC
中的Attributes
时,FNBR使用一个玩家角色上的复制代理结构体。当服务器端的ASC
的属性改变时,上述代理结构体也与之改变,客户端接收到改变的代理结构体并将其包含的属性修改同步至本地的ASC
中。这将允许属性复制使用Pawn
的相关性机制(Relevancy
)和其网络更新频率(NetUpdateFrequency
)。这个代理结构体也可以使用位掩码同步白名单的GameplayTags
。这个优化大大降低了网络带宽,体现了Relevancy
的优势。AI控制的Pawns
的ASC
在Pawn
上,其原本就会使用Relevancy
因为不需要为它额外优化。
详见community questions #3*
Fortnite Battle Royale (FNBR)的世界中有大量可被破坏的AActors
(树,建筑等),每一个都带有一个ASC
。这将增加内存的消耗。FNBR使用延迟加载ASCs
的方案解决此问题,仅当需要时才加载ASCs
(当这些AActors
第一次被玩家伤害时)。一场游戏中很多AActors
可能从未被伤害,这将减少整体内存的开销。
GameplayEffectContainers combine GameplayEffectSpecs
, TargetData
, simple targeting, and related functionality into easy to use structures. These are great for transfering GameplayEffectSpecs
to projectiles spawned from an ability that will then apply them on collision at a later time.
To increase designer-friendly iteration times, especially when designing UMG Widgets for UI, create Blueprint AsyncTasks (in C++) to bind to the common change delegates on the ASC
directly from your UMG Blueprint graphs. The only caveat is that they must be manually destroyed (like when the widget is destroyed) otherwise they will live in memory forever. The Sample Project includes three Blueprint AsyncTasks.
Listen for Attribute
changes:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7LC1UTa-1591700466356)(https://github.com/tranek/GASDocumentation/raw/master/Images/attributeschange.png)]
Listen for cooldown changes:
Listen for GE
stack changes:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jywAbnNS-1591700466357)(https://github.com/tranek/GASDocumentation/raw/master/Images/gestackchange.png)]
LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local!
You need to initialize the ASC
on the client.
ScriptStructCache
errorsYou need to call UAbilitySystemGlobals::InitGlobalData()
.
Name | Acronyms |
---|---|
AbilitySystemComponent | ASC |
AbilityTask | AT |
Action RPG Sample Project by Epic | ARPG, ARPG Sample |
CharacterMovementComponent | CMC |
GameplayAbility | GA |
GameplayAbilitySystem | GAS |
GameplayCue | GC |
GameplayEffect | GE |
GameplayEffectExecutionCalculation | ExecCalc, Execution |
GameplayTag | Tag, GT |
ModiferMagnitudeCalculation | ModMagCalc, MMC |
GameplayPrediction.h
#gameplay-abilities-plugin
This is a list of notable changes (fixes, changes, and new features) to GAS compiled from the official Unreal Engine upgrade changelog and from undocumented changes that I’ve encountered. If you’ve found something that isn’t listed here, please make an issue or pull request.
RootMotionSource
AbilityTasks
GAMEPLAYATTRIBUTE_REPNOTIFY()
now additionally takes in the old Attribute
value. We must supply that as the optional parameter to our OnRep
functions. Previously, it was reading the attribute value to try to get the old value. However, if called from a replication function, the old value had already been discarded before reaching SetBaseAttributeValueFromReplication so we’d get the new value instead.NetSecurityPolicy
to UGameplayAbility
.Attribute
variables resetting to None
on compile.UAbilitySystemGlobals::InitGlobalData()
to use TargetData
otherwise you will get ScriptStructCache
errors and clients will be disconnected from the server. My advice is to always call this in every project now whereas before 4.24 it was optional.GameplayTag
setter to a blueprint that didn’t have the variable previously defined.UGameplayAbility::MontageStop()
function now properly uses the OverrideBlendOutTime
parameter.GameplayTag
query variables on components not being modified when edited.GameplayEffectExecutionCalculations
to support scoped modifiers against “temporary variables” that aren’t required to be backed by an attribute capture.
GameplayTag
-identified aggregators to be created as a means for an execution to expose a temporary value to be manipulated with scoped modifiers; you can now build formulas that want manipulatable values that don’t need to be captured from a source or target.ValidTransientAggregatorIdentifiers
; those tags will show up in the calculation modifier array of scoped mods at the bottom, marked as temporary variables—with updated details customizations accordingly to support featureGameplayTag
source. We no longer reset the source when adding restricted tags to make it easier to add several in a row.APawn::PossessedBy()
now sets the owner of the Pawn
to the new Controller
. Useful because Mixed Replication Mode expects the owner of the Pawn
to be the Controller
if the ASC
lives on the Pawn
.FAttributeSetInittterDiscreteLevels
.