虚幻引擎游戏技能系统文档
虚幻引擎游戏技能系统文档
GASDocumentation
通过一个简单的多人示例项目分享我对UE4中GAS插件的理解。 由于这不是官方文档,示例项目和我都不是来自Epic Games。因此我并不能保证描述的准确性。(译注:本人才疏学浅,还请大家多多指教)
这个文档的主要目的是讲解GAS的主要概念和其中的一些类,同时分享一些我的使用经验。
当前文档和项目基于 Unreal Engine 4.25 。
是此文档的姊妹篇,主要通过多人FPS/TPS项目演示GAS的一些高级应用。
当然,最好的文档是GAS源代码本身(Plugins\Runtime\GameplayAbilities)。
文章目录
1. 初识游戏技能系统GAS
来自 :
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的一个问题。 - 官方缺少多人游戏的示例和相关文档。希望此文档能够有所帮助!
2. 示例项目
通过一个多人的第三人称射击游戏演示文档中的内容,读者可以没有GAS基础,但需要有虚幻引擎的使用基础。比如C++,蓝图,UMG,网络同步等。这个示例项目通过创建两种类型的角色演示了如何使用GAS创建FPS多人游戏,其一:为
PlayerState
添加
AbilitySystemComponent
(后面简称为
ASC
)用以实现玩家和AI控制的英雄角色,其二:直接将
ASC
添加给
Character
用以创建AI控制的小怪或杂兵。
本文档主要讲解GAS的基本概念和基础实践,并不会包括高级主题比如可预测的炮弹。
具体涉及:
ASC
应该属于PlayerState
还是Character
- 复制
Attributes
- 复制动画蒙太奇
GameplayTags
- 应用和删除
GameplayEffects
- 应用被护甲降低的伤害以修改角色HP
GameplayEffectExecutionCalculations
- 眩晕效果
- 死亡和重生
- 在服务器端通过Ability创建Actor(炮弹)
- 在瞄准和冲刺时可预测地改变本地玩家的速度
- 在冲刺时持续消耗耐力
- 消耗法力值的技能
- 被动技能
- 可叠加的
GameplayEffects
- 目标选择
- 在蓝图中创建
GameplayAbilities
- 在C++中创建
GameplayAbilities
- 实例化的
GameplayAbilities
- 非实例化的
GameplayAbilities
(Jump) - 静态
GameplayCues
(FireGun projectile impact particle effect) - Actor
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 |
3. 启用GAS
使用GAS的基本步骤:
- 在虚幻引擎编辑器中启用GameplayAbilitySystem插件
- 编辑
YourProjectName.Build.cs
添加"GameplayAbilities", "GameplayTags", "GameplayTasks"
到PrivateDependencyModuleNames
- 刷新Visual Studio工程
- 从4.24开始,必须要调用
UAbilitySystemGlobals::InitGlobalData()
才能使用 。示例项目在UEngineSubsystem::Initialize()
中调用InitGlobalData
。详见
这就是启用GAS的全部步骤。下面将为
Character
或者
PlayerState
添加
和
并开始创建
和
!
4. GAS概念
4.1 技能系统组件 Ability System Component
AbilitySystemComponent
(
ASC
)是整个技能系统的心脏。
ASC
本质上是一个
UActorComponent
(
) 用于处理技能系统中的所有交互。任何希望使用
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
的同步延迟。确保启用
, 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
。
4.1.1 ASC复制模式
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
。)
4.1.2 设置和初始化
ASCs
通常在
OwnerActor
的构建方法中创建并且显示的标记复制(Replicated)。
这一步必须在C++中完成
。
AGDPlayerState::AGDPlayerState()
{
// Create ability system component, and set it to be explicitly replicated
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(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<APACharacterBase>(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<AGDPlayerState>();
if (PS)
{
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(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<AGDPlayerState>();
if (PS)
{
// Set the ASC for clients. Server does this in PossessedBy.
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(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
。
4.2 游戏标签 Gameplay Tags
是一系列层次化的名字,如
Parent.Child.Grandchild...
这种格式。这些名字通过
GameplayTagManager
进行注册。 这些标签对于描述和归类一个对象的状态非常有用。例如,如果角色处于眩晕状态,我们可以给它一个
State.Debuff.Stun
的
GameplayTag
在整个眩晕的过程中。
你会发现自己用
GameplayTags
替换了以前用布尔值或枚举处理的东西,并对对象是否具有某些
GameplayTags
进行了布尔逻辑。
为对象赋予标签,我们通常将标签添加到对象拥有的
ASC
中,这样GAS就能与标签交互。
UAbilitySystemComponent
实现了
IGameplayTagAssetInterface
接口中的方法以便访问它拥有的
GameplayTags
。
多个
GameplayTags
可以被存储到
FGameplayTagContainer
中。强烈建议使用
GameplayTagContainer
而不是
TArray<FGameplayTag>
,因为
GameplayTagContainers
添加了一些例其高效的魔法。 标签是标准的
FNames
,在
FGameplayTagContainers
中他们可以被高效的打包在一起以完成网络复制,当然需要先在项目设置中开启
Fast Replication
。
Fast Replication
要求服务器和客户端拥有相同的
GameplayTags
列表。为了遍历
GameplayTagContainers
也可以返回一个
TArray<FGameplayTag>
。
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
。
4.2.1 响应Gameplay Tags的改变
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);
4.3 属性 Attributes
4.3.1 Attribute 定义
Attributes
是由
定义的浮点值。
Attributes
能够表达从角色的生命值到角色等级到药瓶的价格等任何数值。 如果Actor拥有游戏性相关的数值,那么可以考虑使用
Attribute
。
Attributes
通常只能被
修改,因此
ASC
可以
这个修改。
Attributes
被定义并且存活在
中。
AttributeSet
也会负责处理
Attributes
的复制。如何定义
Attributes
详见
。
提示:
如果你不想要
Attribute
显示在编辑器的属性详情中,可以使用
Meta = (HideInDetailsView)
属性说明符。
4.3.2 BaseValue vs CurrentValue
一个
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
的属性值限定将会在
谈论,
BaseValue
的属性值限定将会在
讨论,其执行由
GameplayEffects
触发。
立即(
Instant
)
GameplayEffects
将永久改变
BaseValue
,而持续(
Duration
) 和永恒(
Infinite
)
GameplayEffects
将改变
CurrentValue
。周期性(
Periodic
)
GameplayEffects
像立即(
Instant
)
GameplayEffects
一样将改变
BaseValue
4.3.3 Meta Attributes
有一些
Attributes
会被当作仅用于与其他
Attributes
交互的临时值来使用,这种属性被称作元属性(
Meta Attributes
)。例如,我们通常定义伤害值(
damage
)为元属性,而不是直接在
GameplayEffect
中修改生命
Attribute
。这样
damage
的值 可以在Buffs和Debuffs的
中修改,也可以在
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
中修改生命值,护盾等属性。这样做也是可行的,但会牺牲掉灵活性。
4.3.4 响应Attribute的改变
要监听
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)]
4.3.5 推导属性(Derived Attributes)
要使某个属性依据其他属性进行更新,可以使用永恒(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)]
4.4 属性集 Attribute Set
4.4.1 Attribute Set 定义
AttributeSet
负责定义和持有
属性
并且管理
属性
的变化。开发者可以子类化
。在
OwnerActor
的构造方法中创建的
AttributeSet
将会自动注册到
ASC
。
这一步必须在C++中完成
。
4.4.2 Attribute Set 设计
一个
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
或者
处理的
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
,直接在物品实例上存储浮点值。堡垒之夜和
使用这种方式处理枪的弹药。对于一把枪,需要存储弹夹大小,弹夹弹药数量,储备弹药等可直接使用支持复制的浮点数(
COND_OwnerOnly
)。如果储备弹药是在武器间共享的(换句话说储备弹药属于角色而不是武器),那么你可以为
Character
添加一个带有储备弹药
Attribute
的
AttributeSet
。由于弹夹弹药数量没有使用
Attributes
,所以你需要重载几个
UGameplayAbility
的方法以检查和应用消耗(枪上的浮点值)。在授予
Ability
时,需要将枪作为
的
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
上是可行的,但也会带来一些问题。我在
早期的版本中为武器弹药使用这种方式。武器通过一个
AttributeSet
存储了一些
Attributes
,例如弹夹大小,弹夹弹药数量,储备弹药等。如果储备弹药是在武器间共享的,那么可以将所需属性(
Reserve Ammo
)转移到角色身上。当在服务器端玩家装备武器时,武器的
AttributeSet
将会被添加到玩家的
ASC::SpawnedAttributes
中,然后服务器将此复制到客户端。如果玩家卸载武器,过程同上,只是由添加变成删除。
当
AttributeSet
存在于非
OwnerActor
(比如武器),然后在构造方法中初始化
AttributeSet
时将会编译错误。解决办法是将其放在
BeginPlay()
进行初始化,还要在武器上实现
IAbilitySystemInterface
接口,在装备武器时设置
ASC
的指针。
void AGSWeapon::BeginPlay()
{
if (!AttributeSet)
{
AttributeSet = NewObject<UGSWeaponAttributeSet>(this);
}
//...
}
上述示例详见 .
优势:
- 可以使用
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
优势:
- 可以使用
GameplayAbility
和GameplayEffect
的工作流(Cost GEs
) - 可以重用
AttributeSet
Classes (因为每个武器都有自己的ASC
)
缺陷:
- 未知的工作量
- 甚至于此方案的可行性?
4.4.3 定义Attributes
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<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
}
REPTNOTIFY_Always
告诉
OnRep
方法在本地值和服务器下发的值即使已经相同也会触发(为了预测),默认情况下
OnRep
不会触发。
如果
Attribute
不需要复制(像
Meta Attribute
),那么
OnRep
和
GetLifetimeReplicatedProps
可以被跳过。
4.4.4 初始化Attributes
有多种方法可以初始化
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已经解决了这个问题
。
4.4.5 PreAttributeChange()
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<float>(NewValue, 150, 1000);
}
GetMoveSpeedAttribute()
是由宏创建(
)。
任何
Attributes
的改变都会调用此方法,无论是使用
Attribute
setters 还是使用
。
注意:
此处的
clamping
并没有永久地修改
ASC
的
modifier
,它仅改变了查询
modifier
返回的值。这意味着任何修改器
和
对
CurrentValue
的重计算都要重新
clamping
。
注意:
Epic注释,不要使用
PreAttributeChange()
处理游戏性事件,仅用它处理
clamping
(让
CurrentValue
处在正确的范围内)。监听
Attribute
的改变进行游戏性事件处理(比如角色上的血条)最好使用
UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)
(
)。
4.4.6 PostGameplayEffectExecute()
PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)
仅在instant
使
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
后的结果。
4.4.7 OnAttributeAggregatorCreated()
在属性集中当为
Attribute
创建聚合器(
Aggregator
)后将会调用
OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)
。在此方法中可以设置
。
AggregatorEvaluateMetaData
被
Aggregator
用来基于所有应用到当前属性的
求
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)
的方法)
4.5 游戏效果 Gameplay Effects
4.5.1 Gameplay Effect定义
(
GE
) 是
Abilities
改变自己或别人的
和
的途径。
GEs
可以立即改变
Attribute
(像伤害、治疗)或者立即应用持续长时间的Buff/Debuffs(移动加速或眩晕)。
UGameplayEffect
是定义一个游戏效果的数据类,
GameplayEffects
中不能添加任何其他逻辑。通常设计者只需要创建
UGameplayEffect
的蓝图派生类。
GameplayEffects
通过
和
改变
Attributes
。
GameplayEffects
有三种持续类型:立即(
Instant
),持续(
Duration
),和无限(
Infinite
)。
此外,
GameplayEffects
也能够添加和执行
。
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
创建一个
。然后当应用成功后将其添加到
ASC
的
ActiveGameplayEffects
(
FActiveGameplayEffect
)中。
4.5.2 应用Gameplay Effects
在
和
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
时,模拟代理才会调用此方法。
4.5.3 删除Gameplay Effects
在
和
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
时,模拟代理才会调用此方法。
4.5.4 Gameplay Effect修改器
修改器(
Modifiers
)用于修改属性并且是属性修改预测的仅有方式。一个
GameplayEffect
可以有0个或多个
Modifiers
。每一个修改器只能通过下述方式修改一个属性:
操作 | 描述 |
---|---|
Add | 加 |
Multiply | 乘 |
Divide | 除 |
Override | 覆盖 |
Attribute
的
CurrentValue
是一系列
Modifiers
添加到
BaseValue
的聚合结果。聚合
Modifiers
的公式如下所示(
FAggregatorModChannel::EvaluateWithBase
):
((InlineBaseValue + Additive) * Multiplicitive) / Division
任何
Override Modifiers
都会优先使用最后应用的
Modifier
覆盖最终值。
注意:
百分比的修改要使用
Multiply
(在
Additive
之后执行)
注意: 百分比的修改 会有问题。
一共有四种
Modifiers
:Scalable Float, Attribute Based, Custom Calculation Class, and Set By Caller。这些全部通过浮点值和操作符改变
Modifier
的
Attribute
。
修改器类型 | 描述 |
---|---|
Scalable Float | FScalableFloats 是一种能够指向 Data Table (行表示变量,列表示等级)的结构。 Scalable Floats 将根据当前技能等级(或者是在
覆盖的等级)自动读取值。这个值可以根据系数进一步处理。如果没有指定数据表,值会被当作是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 需要创建一个
类并且可通过系数、Pre/Post Multiply Additive Value进一步处理。 |
Set By Caller | SetByCaller Modifiers 是在 GameplayEffect 之外由 Ability 在运行时设置或者由 GameplayEffectSpec 的创建者设置。 例如,当你想根据按钮按下的时间决定伤害大小时可以使用 SetByCaller 。 SetByCallers 本质是存在于 GameplayEffectSpec 上的 TMap<FGameplayTag, float> , Modifier 仅仅是告诉聚合器通过 GameplayTag 去检索值。 SetByCallers 仅能使用 GameplayTag 不能使用 FName 。如果没有在 GameplayEffectSpec 中找到 GameplayTag 对应的值,游戏将会抛出一个运行时错误并且返回0。如果运算是除法你就悲剧了。 具体使用详见
。 |
4.5.5 叠加Gameplay Effects
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)]
4.5.6 赋予Abilities
GameplayEffects
能够赋予
ASCs
新的
。仅有
Duration
和
Infinite
GameplayEffects
才能赋予
Abilities
。
一个常见的用例是当你想要强制其他玩家做一些事情(例如让他们击退或拉近)。你可以给他们应用一个
GameplayEffect
然后自动激活能够完成上述事情的
Ability
(详见当赋予
Ability
时如何自动激活(被动技能)
)。
设计者可以选择
GameplayEffect
将赋予哪些
Abiltities
,设置
Ability
的等级,绑定输入ID,设置
Ability
的移除策略。
移除策略 | 描述 |
---|---|
Cancel Ability Immediately | 当 GameplayEffect 从目标移除时立即取消并移除 Ability |
Remove Ability on End | 当 Ability 执行完成后移除 |
Do Nothing | 除非手动移除,否则永久存在 |
4.5.7 游戏效果标签 Gameplay Effect Tags
GameplayEffects
带有多个标签容器(
)。对于每个类别设计者可以编辑
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 将被移除 |
4.5.8 免疫游戏效果
GameplayEffects
能够获得免疫,用于通过
高效的阻止其他
GameplayEffects
的应用。免疫也可以通过其他方式实现,比如
Application Tag Requirements
,但此方法将提供一个委托(
UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate
)可以监听
GameplayEffects
的免疫阻止。
GrantedApplicationImmunityTags
将检查源
ASC
(也包括源的
Ability
中的
AbilityTags
如果有的话)是否有指定的标签。这提供了一种根据某些角色或者源所拥有的标签决定是否免疫
GameplayEffects
的方式。
Granted Application Immunity Query
将检查要应用的
GameplayEffectSpec
如果满足标签匹配则阻止应用,否则允许。
通过
GameplayEffect
蓝图的悬停提示了解更多关于
Queries
的使用。
4.5.9 游戏效果细则 Gameplay Effect Spec
(
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
上- 表示谁创建的这个
GameplayEffectSpec
GameplayEffectSpec
创建时的属性快照DynamicGrantedTags
是对应GameplayEffect
中的GrantedTags
的额外标签DynamicAssetTags
是对应GameplayEffect
中的AssetTags
的额外标签SetByCaller
TMaps
SetByCallers
SetByCallers
允许
GameplayEffectSpec
以
GameplayTag
或
FName
关联浮点值,具体存储在
GameplayEffectSpec
的
TMap<FGameplayTag, float>
和
TMap<FName, float>
中。使用方式和
GameplayEffect
的
Modifiers
类似,也可以通过
SetByCallers
将
Ability
中生成的数据传递给
或
。
SetByCaller 使用 | 描述 |
---|---|
Modifiers | 必须在 GameplayEffect 类中提前定义。仅能使用 GameplayTag 版本。如果在 GameplayEffect 中被定义,在 GameplayEffectSpec 找不到对应的值,游戏将会运行时错误并且返回0。小心除法,详见 |
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
可以阻止在蓝图中的拼写错误,而且在网络同步过程中会更高效。
4.5.10 游戏效果上下文 Gameplay Effect Context
包含了
GameplayEffectSpec
的创建者(
Instigator
)和应用的目标(
)。可以通过派生
GameplayEffectSpec
用来在
/
,
和
之间传递任意数据。
派生
GameplayEffectContext
的过程:
- 重载
FGameplayEffectContext::GetScriptStruct()
- 重载
FGameplayEffectContext::Duplicate()
- 需要复制新数据时需要重载
FGameplayEffectContext::NetSerialize()
- 像
FGameplayEffectContext
一样实现派生类的TStructOpsTypeTraits
- 在你的
中重载
AllocGameplayEffectContext()
返回FGameplayEffectContext
派生类的对象
在
GameplayEffectContext
的派生类中添加
TargetData
用于在
GameplayCues
中访问,比如霰弹枪可以击中多个目标。
4.5.11 修改器量计算 Modifier Magnitude Calculation
(简称
ModMagcCalc
或
MMC
)用于
GameplayEffects
中的
。 其作用和
类似但不同的是
MMC
可以被预测(
)。
MMC
唯一的作用是通过
CalculateBaseMagnitude_Implementation()
返回一个浮点值,可以通过蓝图或C++进行
MMC
的派生并重载此方法。
MMCs
可以被任何类型的
GameplayEffects
(
Instant
,
Duration
,
Infinite
, 或
Periodic
)使用。
MMC
的优势在于可以获取
GameplayEffect
的目标和源的任何属性并且能够读取
GameplayEffectSpec
中的
GameplayTags
和
SetByCallers
。
Attributes
可以是快照也可以不是,属性快照将在
GameplayEffectSpec
创建时获取,属性非快照将在应用时自动获取。通过已存在于
ASC
的
Modes
捕获
Attributes
重计算他们的
CurrentValue
,重计算并不会执行
AbilitySet
中的
因此需要在此处完成
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<float>(Mana, 0.0f);
float MaxMana = 0.f;
GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvaluationParameters, MaxMana);
MaxMana = FMath::Max<float>(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
。
4.5.12 游戏效果执行计算 Gameplay Effect Execution Calculation
(
ExecutionCalculation
,
Execution
(在插件代码中经常会看到这个术语), or
ExecCalc
) 是
GameplayEffects
改变
ASC
的一种最有力的方式。与
类似,
ExecCalc
可以获取
Attributes
,可选的是否快照。 与
MMCs
不同的是,
ExecCalc
可以改变多个属性并且高效的处理任何事情。强大和灵活之下,
ExecCalc
不支持
,且必须在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
的
,
ExecCalc
仅在服务器端执行。
ExecCalc
最常用于伤害计算,从
Source
和
Target
读取一系列属性值,然后进行复杂的计算。示例项目中也使用
ExecCalc
完成伤害计算,读取由
GameplayEffectSpec's
设置的伤害值,再通过
Target
中的护甲属性缓解伤害。详见
GDDamageExecCalculation.cpp/.h
。
4.5.13 自定义应用条件 Custom Application Requirement
(
CAR
) 类给设计者提供了是否能够应用
GameplayEffect
的高级控制手段(有别于简单的标签控制)。 可以通过在蓝图中重载
CanApplyGameplayEffect()
或者在C++中重载
CanApplyGameplayEffect_Implementation()
实现。
何时需要使用
CARs
?比如:
Target
需要有一定数量的属性时Target
需要GameplayEffect
堆叠到一定数量时
除此之外
CARs
还能够做更多事情,比如检查
Target
是否应用了一个
GameplayEffect
的实例,在应用一个新实例时如果同类型的实例已存在则只改变其持续时间(
CanApplyGameplayEffect()
要返回false)。
4.5.14 技能消耗 Cost Gameplay Effect
可以指定一个处理技能消耗的
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
的方法:
- Use an
MMC
,这是最简单的方法。 创建一个 从GameplayAbility
实例中读取消耗值:
float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(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
中的消耗值。
4.5.15 技能冷却 Cooldown Gameplay Effect
可以指定一个处理技能冷却的
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
的方法:
- Use a
,这是最简单的方法,通过带有
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<FGameplayTagContainer*>(&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
的值:
- Use an
,这种方法基本和上述相同,除了设置
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<FGameplayTagContainer*>(&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<UPGGameplayAbility>(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<float, float> > 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的迭代中实现。
4.5.16 修改活动游戏效果的持续时间
要修改
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<FActiveGameplayEffect*>(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;
}
4.5.17 在运行时创建动态游戏效果
在运行时动态创建
GameplayEffects
是一个高级主题。这种用法不能过于频繁。
仅有
Instant
GameplayEffects
能够在C++中被运行时创建。示例工程动态创建了一个
GameplayEffects
,用于当一个角色受到致命一击时(在其
AttributeSet
中处理),将金币和经验奖励发送给他的击杀者。
// Create a dynamic instant Gameplay Effect to give the bounties
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(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
实例。
4.5.18 游戏效果容器 Gameplay Effect Containers
Epic官方的
实现了一个叫
FGameplayEffectContainer
的结构,对于包含
GameplayEffects
和
极为方便。它自动化了一些工作,像根据
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
还包括可选的目标选取方式。
4.6 游戏技能 Gameplay Abilities
4.6.1 Gameplay Ability定义
(
GA
)是在游戏中
Actor
能做的行为或技能。在同一时间可以激活多个
GameplayAbility
,例如冲刺的同时射击。
GA
可以通过
Blueprint
或
C++
制作。
GameplayAbilities
的示例:
- 跳跃
- 冲刺
- 射击
- 每隔几秒被动的阻挡一次攻击
- 使用一个药瓶
- 开门
- 收集资源
- 建造建筑
不能通过
GameplayAbilities
实现的示例:
- 基本的移动输入
- 一些UI交互 - 不要使用
GameplayAbility
处理从商店购买物品
这些不是规则,只是我的建议。
GameplayAbilities
默认带有一个等级(技能等级)用于定义修改属性时的修改量,或是改变
GameplayAbility
的功能。
GameplayAbilities
会在
Owning Client
上执行,根据
设置的策略决定是否在服务器端也要执行。
Net Execution Policy
决定一个
GameplayAbility
是否将会进行本地预测。对于可选的
它们包含了一些默认的行为。 在
GameplayAbilities
持续过程中会使用
处理一些行为比如等待一个事件、等待一个属性改变、等待玩家选择目标或者通过
Root Motion Source
移动一个角色。
模拟的客户端将不会执行
GameplayAbilities
。取而代之的是,当服务器执行技能时,任何需要在
simulated proxies
呈现的表现(像播放动画)都将通过
Replicated
或者通过
AbilityTasks
执行的
RPC
或者是
(播放声音和特效)来实现。
所有的
GameplayAbilities
都需要重载
ActivateAbility()
方法以实现自己的游戏逻辑。在
GameplayAbility
完成或取消时可以通过
EndAbility
实现一些额外的逻辑。
简单的
GameplayAbility
流程图:
复杂一些的
GameplayAbility
流程图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3gLmT8ic-1591700466349)(https://github.com/tranek/GASDocumentation/raw/master/Images/abilityflowchartcomplex.png)]
复杂的
Ability
也可以通过多个彼此交互的
GameplayAbilities
实现。
复制策略 Replication Policy
不要使用这个选项。这个名字有误导性你并不需要这个。
默认会从服务器复制到
Owning Client
。就像上面说过的,
GameplayAbilities
不会在simulated proxies上执行
。Epic的Dave Ratti说过他希望
。
服务器端远程技能取消 Server Respects Remote Ability Cancellation
这个选项常常引发问题。这意味着如果客户端的
GameplayAbility
由于取消或自然完成而终止,将强制服务器的版本结束(无论其是否完成)。 后一个问题很重要,尤其是对于高延迟玩家使用的本地预测的
GameplayAbilities
。 通常要禁用此选项。
直接输入复制 Replicate Input Directly
开启此项将总是将输入的按下和释放事件传递到服务器。
Epic官方建议不要使用此选项,取而代之的应该使用已存在的输入相关的
内置的
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()
4.6.2 输入绑定
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<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(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
中读取这个变量。
4.6.3 赋予技能 Granting Abilities
为
ASC
赋予一个
GameplayAbility
会将其加入到
ASC
的
ActivatableAbilities
列表中,并允许
GameplayAbility
在满足
时可以被激活,只能在C++。
我们在服务器端赋予
GameplayAbilities
,对应的
会被自动复制到
Owning Client
。其他客户端(
Simulated Proxies
)将不会收到
GameplayAbilitySpec
。
示例项目在
Character
类中存储了一个
TArray<TSubclassOf<UGDGameplayAbility>>
,这些技能在游戏开始时将被自动赋予角色:
void AGDCharacterBase::AddCharacterAbilities()
{
// Grant abilities, but only on the server
if (Role != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->CharacterAbilitiesGiven)
{
return;
}
for (TSubclassOf<UGDGameplayAbility>& StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
}
AbilitySystemComponent->CharacterAbilitiesGiven = true;
}
当赋予这些
GameplayAbilities
时,我们会创建带有
UGameplayAbility
类、技能等级、绑定的输入、
SourceObject
(谁将
GameplayAbility
给了
ASC
)的
GameplayAbilitySpecs
。
4.6.4 激活技能 Activating Abilities
如果为一个
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<UGameplayAbility> 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
的激活序列过程:
- Owning client
调用
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
并回退任何可预测的修改。
被动技能 Passive Abilities
要自动激活执行运行的被动
GameplayAbilities
,可以重写
UGameplayAbility::OnAvatarSet()
(它将在赋予
GameplayAbility
并且设置
AvatarActor
时自动执行)调用
TryActivateAbility()
。
建议为自定义的
UGameplayAbility
类添加一个bool变量用以控制
GameplayAbility
在被赋予时是否自动激活。示例工程这样实现的被动护甲技能。
通常被动
GameplayAbilitites
的
设置为
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);
}
}
4.6.5 取消技能 Canceling Abilities
要从内部取消一个
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
)。
4.6.6 获取当前活动的技能 Getting Active Abilities
新手经常会问“怎样获得激活的技能?”。由于可以同时激活多个技能,因此需要在
ASC
的
ActivatableAbilities
(可激活技能)列表中查找匹配
Asset
或
Granted
GameplayTag
的技能。
UAbilitySystemComponent::GetActivatableAbilities()
返回一个可被迭代的
TArray<FGameplayAbilitySpec>
。
ASC
提供了另一个帮助方法,可以带有一个
GameplayTagContainer
参数(比上述方法方便很多),还有一个
bOnlyAbilitiesThatSatisfyTagRequirements
仅返回当前能够被激活的
GameplayAbilitySpecs
。
例如,你可能有两种基本的攻击技能,一个是带武器,一个是赤手空拳。可以根据装备武器的
GameplayTag
区分两者以激活正确的一个。详见Epic对此方法的注释。
UAbilitySystemComponent::GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true)
获取
FGameplayAbilitySpec
后可以通过
IsActive()
方法判断技能当前是否激活中。
4.6.7 实例化策略 Instancing Policy
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 |
4.6.8 网络执行策略 Net Execution Policy
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 执行。个人没怎么用过 |
4.6.9 技能标签 Ability Tags
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 设置 |
4.6.10 游戏技能细则 Gameplay Ability Spec
在赋予
GameplayAbility
后,
ASC
上会存在一个
GameplayAbilitySpec
,其定义了可被激活的
GameplayAbility
(其中包括
GameplayAbility
类,等级,输入绑定,运行时状态)。
当在
Server
赋予了一个
GameplayAbility
,
Server
将会把
GameplayAbilitySpec
复制给
Owning Client
(才可被激活)。
激活一个
GameplayAbilitySpec
是否会创建一个
GameplayAbility
的实例由
Instancing Policy
决定。
4.6.11 为技能传递数据 Passing Data to Abilities
GameplayAbilities
的通常范例是
激活->生成数据->应用->结束
。有时需要将外部的数据传递给
GameplayAbilities
,为此
GAS
提供了下述方式:
方法 | 描述 |
---|---|
Activate GameplayAbility by Event | 通过事件激活一个 GameplayAbility 带有一个 Payload 。对于本地预测的 GameplayAbilities ,事件的 Payload 将会从 Client 传递至 Server 。 Payload 除了包含一些变量外,还可以使用两个可选的 Objects 或者一个
。问题是不能使用输入绑定激活技能。要使用此项必须在 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 将不能保证由于潜在的数据包丢失而在接收器上发生的顺序。 |
4.6.12 技能的消耗与冷却
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
可能被修改,导致提交消耗时可能无法满足。如果提交时
是有效的则消耗和冷却是可以被本地预测的(
)。
对于实现细节详见 和 。
4.6.13 技能升级
升级技能有两种常用的方法:
技能升级方法 | 描述 |
---|---|
基于新的等级重新赋予技能 | 先从 ASC 中删除 GameplayAbility 然后在服务器端基于新的等级重新赋予 GameplayAbility 。如果技能此时处于激活状态会被终止 |
增加 GameplayAbilitySpec 的 Level | 在服务器端,找到 GameplayAbilitySpec ,增加它的 Level ,标记它为 Dirty 以复制到 Owning Client 。如果技能此时处于激活状态不会被终止 |
两种方法的主要区别是在技能升级时当前激活的技能是否会被终止。使用哪种方法依赖于你的
GameplayAbilities
,建议为你的
UGameplayAbility
子类添加一个
bool
变量控制使用哪种方式。
4.6.14 技能集 Ability Sets
GameplayAbilitySets
是一个便利的
UDataAsset
类,用于将其持有的带有输入绑定的
GameplayAbilities
赋予给
Characters
。
GameplayAbilitySets
子类化可以添加额外的逻辑和属性。Paragon对于每一个英雄都会有一个与之对应的
GameplayAbilitySet
。
这个类并不是必须要使用。示例工程在
GDCharacterBase
和它的子类中完成了和
GameplayAbilitySets
类似的功能。
4.6.15 技能批处理 Ability Batching
传统的
Gameplay Ability
生命周期涉及到最小两到三次的客户端到服务器端的RPC调用。
CallServerTryActivateAbility()
ServerSetReplicatedTargetData()
(可选)ServerEndAbility()
如果一个
GameplayAbility
可以在一帧内原子性的执行完上述步骤,那么我们可以通过
Batch(Combine)
优化这个工作流,将两到三个
RPCs
合并到一个
RPC
。
GAS
提供了这种优化叫作
Ability Batching
。使用
Ability Batching
通常的示例是基于命中检测的枪(hitscan guns),射击、射线检测、将命中结果(
)发送给服务器、结束一气呵成。
提供了一个这样的示例。
半自动的枪(按一下射一发)是最佳示例,将
CallServerTryActivateAbility()
,
ServerSetReplicatedTargetData()
(the bullet hit result)和
ServerEndAbility()
从三个
RPCs
合并到一个
RPC
。
全自动或连发的枪对第一发子弹将
CallServerTryActivateAbility()
和
ServerSetReplicatedTargetData()
这两个
RPCs
合并到一个
RPC
。随后的每一发子弹只有一个
ServerSetReplicatedTargetData()
RPC
。最终,当停止射击时发送一个单独的
RPC
ServerEndAbility()
。这是糟糕示例,我们仅将第一发子弹从两个
RPCs
优化成了一个
RPC
。这种情况也可以通过
来触发技能,使用
EventPayload
将子弹的
TargetData
从客户端发送到服务器,这种方法的缺点是必须在
Ability
的外部生成
TargetData
,而
Ability Batching
方式是在
Ability
的内部生成。
Ability Batching
在
中默认是被禁用的。要启用
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<UGSGameplayAbility>(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)]
4.6.15 网络安全策略 Net Security Policy
一个
GameplayAbility
的网络安全策略决定技能在网络上何处执行,这可以防止客户端尝试执行受限功能。
网络安全策略 | 描述 |
---|---|
ClientOrServer | 没有安全要求,客户端和服务器可以自由的执行和终止技能 |
ServerOnlyExecution | 客户端发送的执行请求会被服务器端忽略。客户端仅能请求取消或结束技能 |
ServerOnlyTermination | 客户端发送的取消或结束技能的请求会被服务器端忽略。客户端仅能请求技能的执行 |
ServerOnly | 服务器控制技能的执行和终止。客户端的任何请求都会被忽略 |
4.7 技能任务 Ability Tasks
4.7.1 Ability Task定义
GameplayAbilities
仅能执行一帧,其本身并没有太大的灵活性。要在技能的持续过程中做一些事情或者在稍后响应委托回调,我们可以使用
AbilityTasks
。
GAS
带了很多拆箱即用的
AbilityTasks
:
- 基于
RootMotionSource
的角色移动任务 - 播放动画蒙太奇的任务
- 响应属性修改的任务
- 响应游戏效果修改的任务
- 响应玩家输入的任务
- 等等
同时最多只能运行1000个并行的
AbilityTasks
(见
UAbilityTask
的构建方法)。在设计
GameplayAbilities
时需要谨记这一点,一个
RTS
游戏在同一时间可能有几百个角色。
4.7.2 自定义Ability Tasks
通常你将在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
。
4.7.3 使用Ability Tasks
在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
时将自然结束)。
4.7.4 Root Motion Source Ability Tasks
GAS
带有一些能够处理角色移动的
AbilityTasks
,比如对于角色击退、复杂的跳跃、拉、冲撞。其本质是使用连接到
CharacterMovementComponent
的
Root Motion Sources
。
注意:
可预测的
RootMotionSource
AbilityTasks
在4.19和4.25+版本能够正常工作,在4.20-4.24有BUG(如果要使用这些版本,可以自行修正详见
)。
4.8 游戏表现 Gameplay Cues
4.8.1 Gameplay Cue定义
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 类型 | 描述 |
---|---|---|---|
Execute | Instant or Periodic | 静态 GameplayCueNotifies 将使用 ClassDefaultObject (意味着没有实例),主要用于一次性的效果比如击中效果 | |
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
的
不是
Full
时,在服务器玩家(
Listen Server
)上
Add
and
Remove
GC
的事件将会触发两次,一次是应用GE,另一次是通过
NetMultiCast
广播给客户端,然而,
WhileActive
事件仅会触发一次。所有事件在客户端仅触发一次。
示例项目使用
GameplayCueNotify_Actor
实现了一个眩晕和一个冲刺效果。还使用了一个
GameplayCueNotify_Static
实现子弹击中效果。这些
GC
能够通过本地触发进一步优化,而不是通过GE复制它们,详见示例项目。
4.8.2 触发Gameplay Cues
当
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();
4.8.3 本地游戏表现 Local Gameplay Cues
从
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
删除。
4.8.4 游戏表现参数 Gameplay Cue Parameters
GameplayCues
接收一个
FGameplayCueParameters
结构体作为其参数其中包含了一些额外的信息。如果你通过
GameplayAbility
或者
ASC
中的方法手动激活一个
GameplayCue
,那么你也必须手动填充一个
GameplayCueParameters
用以传递给
GameplayCue
。如果
GameplayCue
是被
GameplayEffect
触发,那么
GameplayCueParameters
将会被自动填充。
- AggregatedSourceTags
- AggregatedTargetTags
- GameplayEffectLevel
- AbilityLevel
- Magnitude (if the
GameplayEffect
has anAttribute
for magnitude selected in the dropdown above theGameplayCue
tag container and a correspondingModifier
that affects thatAttribute
)
在
GameplayCueParameters
结构体中的
SourceObject
变量可能是当手动激活
GameplayCue
时向其传递任意数据的好地方。
**注意:**在参数结构体中的一些变量像
Instigator
可能已经存在于
EffectContext
之中。
EffectContext
也包含一个
FHitResult
用于指定
GameplayCue
在世界中生成的位置。通过子类化
EffectContext
向
GameplayCues
传递更多数据可能是一种好的方式,尤其是当
GameplayCues
是由
GameplayEffect
触发时。
下面
中的三个方法用于自动填充
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);
4.8.5 游戏表现管理器 Gameplay Cue Manager
默认情况下,
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;
}
4.8.6 阻止触发游戏表现 Prevent Gameplay Cues from Firing
有些时候我们不希望
GameplayCues
被触发,比如当我们格档了一次攻击时不希望播放
Damage GameplayEffect
上的受击效果或者希望换一个效果。这时我们可以在
中调用
OutExecutionOutput.MarkGameplayCuesHandledManually()
并且手动发送我们想要的
GameplayCue
事件给
Target
或
Source
的
ASC
即可。
如果在特定的
ASC
上不想触发任何
GameplayCues
,可以设置
AbilitySystemComponent->bSuppressGameplayCues = true
。
4.8.7 游戏表现批处理
每一个
GameplayCue
的触发都是一次不可靠的广播
RPC
,当我们同时触发多个
GCs
时,有几种优化的方法将他们压缩到一个
RPC
中或是发送少量的数据以节省带宽。
手动RPC
假设你有一把霰弹枪能够同时发射8发弹丸,有8个射线检测和8个
GameplayCues
。
使用了一种懒方法,通过将射线检测信息以
的形式保存到
中将其组合到一个
RPC
中。当然这种作法只是将
RPCs
从8个减少到1个,但还是会在这个
RPC
中通过网络发送大量的数据(大概是500字节)。一种更好的方法是,发送一个自定义结构体的
RPC
,将击中的位置信息或者一个能在接收端重建位置信息的随机种子编码到这个结构体中,在客户端通过这个自定义的结构体触发
即可。
具体步骤:
- 声明一个
FScopedGameplayCueSendContext
,它将阻止UGameplayCueManager::FlushPendingCues()
直到超出其作用域。这意味着所有在其作用域内的GameplayCues
都将进行排队。 - 重载
UGameplayCueManager::FlushPendingCues()
,根据GameplayTag
将能够合并到一个批次的GameplayCues
保存到自定义结构体中,调用RPC
将其发送到客户端。 - 客户端接收到自定义结体体并将其解压到本地执行的
GameplayCues
中。
这种方法还能将一些特定的参数传递给
GameplayCues
,这些参数并不在
GameplayCueParameters
中并且你也不想将它们添加到
EffectContext
中,比如伤害数字,是否暴击,是否破盾,是否致使一击等。
一个游戏技能上的多个游戏表现
在一个
GameplayEffect
上的所有
GameplayCues
已经通过一个
RPC
发送。默认情况下,
UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec()
将会通过不可靠的多播发送整个
GameplayEffectSpec
(但会转换成
FGameplayEffectSpecForRPC
)而无论
ASC
的
Replication Mode
是什么。根据
GameplayEffectSpec
中包含的内容这有可能需要大量的带宽,不过可以通过设置cvar
AbilitySystem.AlwaysConvertGESpecToGCParams 1
尝试进行优化。开启这个选项后将会在
RPC
的过程中把
GameplayEffectSpecs
转换成
FGameplayCueParameter
这样就不用发送整个
FGameplayEffectSpecForRPC
了。这将可能会节省带宽但也会少了一些信息,这依赖于
GESpec
是如何转换成
GameplayCueParameters
的,且必须知晓你的
GCs
需要哪些信息。
4.9 技能系统全局数据
类持有
GAS
的全局信息。其中的大多数变量可以通过
DefaultGame.ini
设置。通常并不需要与这个类进行交互,但需要知道它的存在。如果需要子类化
或
,则需要通过派生
AbilitySystemGlobals
完成。
派生
AbilitySystemGlobals
后需要在
DefaultGame.ini
中进行设置:
[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"
4.9.1 InitGlobalData()
从UE4.24开始,要使用
必须要调用
UAbilitySystemGlobals::InitGlobalData()
,否则你会遇到
ScriptStructCache
相关的错误且客户端将从服务器断开连接。在项目中这个方法只需要调用一次。堡垒之夜是在
AssetManager
类的开始初始加载方法调用,帕拉贡是在
UEngine::Init()
中调用。示例项目在
UEngineSubsystem::Initialize()
中调用,建议将这个直接拷贝到你的项目中以解决
TargetData
相关的问题。
如果在使用
AbilitySystemGlobals
GlobalAttributeSetDefaultsTableNames
时崩溃,则需要将
UAbilitySystemGlobals::InitGlobalData()
的调用置后一些,将其放到
AssetManager
或
GameInstance
中而不是
UEngineSubsystem::Initialize()
中。崩溃的原因是因为子系统的初始化顺序导致,
GlobalAttributeDefaultsTables
需要
EditorSubsystem
已被加载以绑定
UAbilitySystemGlobals::InitGlobalData()
里的一个委托。
4.10 预测 Prediction
GAS
支持拆箱即用的客户端预测(预判)功能,然而它并不能预测所有事情。
GAS
中的客户端预测意味着客户端不必等待服务器的许可就可以激活一个
GameplayAbility
并应用
GameplayEffects
。它也能预测做这件事需要的服务器许可和应用
GameplayEffects
的目标。在客户端激活后,服务器将在网络延迟时间后执行
GameplayAbility
并且告诉客户端它的预测执行是否正确。如果客户端的预测是错误的,它将从错误的预测中回滚它的修改匹配到服务器。
GAS
预测相关代码在插件的
GameplayPrediction.h
文件中。
Epic的倾向是仅预测可以回滚的,例如Paragon和Fortnite都不预测伤害,这两个游戏很可能使用 (不支持预测)做伤害计算。这并不是说你一定不能预测这些事情,只要符合需求并且能够工作。
我们也不是“无缝地,自动地预测一切”的解决方案。 我们仍然认为,将玩家的预测最好保持在最低限度。
摘自Epic的Dave Ratti新的
网络预测插件
什么是可以预测的:
- 技能激活
- 触发事件
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
4.10.1 预测键 Prediction Key
GAS
的预测机制基于一个叫作预测键(
Prediction Key
)的概念,它是一个当客户端激活一个
GameplayAbility
时在客户端生成的整型标识符。
- 客户端激活一个
GameplayAbility
时生成一个预测键(Activation Prediction Key
) - 客户端调用
CallServerTryActivateAbility()
将这个预测键发送给服务器 - 当预测键有效时,客户端将这个预测键添加给所有它应用的
GameplayEffects
- 客户端预测键超出范围,在相同的
GameplayAbility
中进一步预测效果需要一个新的 - 服务器接收到来自客户端的预测键
- 服务器将这个预测键添加给所有它应用的
GameplayEffects
- 服务器将这个预测键复制(Replicates)回客户端
- 客户端接收到从服务器复制回的
GameplayEffects
,如果复制回的GameplayEffects
与客户端应用的GameplayEffects
有相同的预测键,则预测正确。此时在目标上将有两个GameplayEffect
直到客户端删除它预测的那一个 - 客户端接收到从服务器返回的预测键(
Replicated Prediction Key
),这个预测键现在被标记为阵旧的 - 客户端删除所有它创建带有阵旧预测键的
GameplayEffects
,服务器复制回的GameplayEffects
将被保持。任何客户端添加的且没有接收到匹配的服务器返回版本的GameplayEffects
都是预测失败的
预测键在通过
Activation Prediction Key
激活的
GameplayAbilities
的原子指令组(也叫作
Window
)中保证是有效的。你也可以想像成仅在一帧有效。任何
AbilityTasks
中的回调将不再有一个有效的预测键除非
AbilityTask
中有内建的同步点生成一个新的
。
4.10.2 在技能中创建新的预测窗口
要在
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
解决。
4.10.3 可预测的生产Actor
在客户端可预测的生产
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项目参考其中如何实现的可预测生产子弹。它们仅在所属客户端上生成一个虚拟子弹,该虚拟子弹与服务器的复制子弹同步。
4.10.4 GAS预测机制的开发计划
将来官方可能会在
GameplayPrediction.h
中加入
GameplayEffect
的移除预测和周期性
GameplayEffects
的预测。
来自Epic的Dave Ratti 有兴趣解决冷却预测导致的低延迟玩家比高延迟玩家有优势的问题。
预计由Epic开发的新插件
Network Prediction
将能与
GAS
完全互用就像
CharacterMovementComponent
一样。
4.10.5 网络预测插件
Epic最近发起了一项计划,用新的
Network Prediction
插件替换
CharacterMovementComponent
。 该插件仍处于早期阶段,但仍可以在Unreal Engine GitHub上提早访问。现在还不确定此插件在未来哪个版本的引擎亮相。
4.11 技能目标
4.11.1 目标数据
是一个能在网络上传递用于描述目标数据的通用结构体。
TargetData
通常将持有
AActor
或
UObject
的引用以及
FHitResults
和位置/朝向/原点信息。不过,你也可以通过子类化在其中加入任何你需要的东西,这是一种通过
GameplayAbilities
在客户端和服务器端之间传递数据的简单方法。不要直接使用
FGameplayAbilityTargetData
结构体而应使用它的子类。在
GAS
的
GameplayAbilityTargetTypes.h
中包含了几个能够被直接使用的
FGameplayAbilityTargetData
派生类。
TargetData
通常是由
产生或者是手动创建,它会被
和
(通过
)消耗。
TargetData
作为
EffectContext
的结果时,
,
,
和
的
[Pre|Post]GameplayEffectExecute
方法都可以访问它。
通常我们不会直接传递
FGameplayAbilityTargetData
,而是使用一个
,其内部保存了一个
FGameplayAbilityTargetData
指针的TArray。
4.11.2 目标Actor
GameplayAbilities
使用
WaitTargetData
AbilityTask
生产
,其作用是呈现和捕获世界中的目标信息。
TargetActors
可以使用可选的
显示当前的目标。在目标选择确认之后,目标信息将会以
的形式返回,然后将其传递给
GameplayEffects
。
TargetActors
本质是
AActor
因此他们可以有任何的显示组件(
static meshes
或者
decals
)用以呈现在哪以及如何选择目标。
Static Meshes
被用来显示你的角色将要构建的一个对象(堡垒之夜的建造模式)。
Decals
用来显示地面上的作用区域。示例项目使用带有一个
Decal
的
呈现陨石技能的伤害区域。
TargetActors
也可以不显示任何东西,比如
GASShooter
中的霰弹枪会直接使用射线检测目标而不需要显示任何东西。
TargetActors
使用基本的射线检测或者碰撞检测获得目标信息并根据
TargetActor
的实现方式将结果转换成
FHitResults
或
AActor
的数组保存到
TargetData
。
WaitTargetData
AbilityTask
通过
TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType
参数决定目标何时被确定。当参数不是
TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant
的时候,
TargetActor
通常会在
Tick()
中执行trace/overlap并且根据它的实现更新
FHitResult
的位置。要注意它使用了
Tick()
并且复杂的
TargetActors
可能会做很多的事情,比如
GASShooter
中的火箭筒的第二技能。当在
Tick()
上追踪时对客户端是非常敏感的,如果消耗了太多性能你可能要考虑降低
TargetActor
的Tick频率。当参数是
TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant
的时候,
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
中有一个自定义
子类和一个新的
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为火箭筒的第二技能使用了永久目标。
4.11.3 游戏技能的标线
(
Reticles
)显示了
(必须是非
Instant
)已确定的目标。
TargetActors
负责所有
Reticles
的创建和销毁。
Reticles
是
AActors
因此它们可以使用任何可视化组件。
中常见的实现是使用
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
提供的
进行配置。默认的结构体仅提供了一个变量
FVector AOEScale
,当需要更多的变量时可以派生
FWorldReticleParameters
,当然也需要同时派生对应的
TargetActor
(原本的
TargetActor
仅能接受基类)。
Reticles
默认不会被复制,但当你的游戏需要将本地玩家的目标显示给其他玩家时可以打开复制。
默认的
TargetActors
仅当存在有效目标时
Reticles
才会显示,例如,你使用
AGameplayAbilityTargetActor_SingleLineTrace
追踪一个目标,当敌人在追踪路径上时
Reticle
才会显示出来如果你转移目光,当你转移目光敌人不再有效时
Reticle
将会消失。如果想要
Reticle
保持在最后一个有效目标上,你需要定制自己的
TargetActor
用来记住最后的有效目标并且将
Reticle
保持在目标上。
4.11.4 游戏效果容器
带有
TargetType
和
GameplayEffects
,当
EffectContainer
在客户端和服务器端被应用时立即获得目标并应用
GameplayEffects
。这样比
高效,因为它是运行在目标选取对象的
CDO
上(不需要创建和销毁
Actors
),但它会缺失玩家输入,只能被立即触发不能确认和取消选取,并且不能从客户端向服务器发送数据(因为它将同时在两端执行)。对于立即触发的射线或碰撞检测这将非常有效。Epic的
ActionRPG
示例工程中在
Containers
里包含了两种不同的目标选择类型,一个是选择技能施放者,一个是从事件中取得的
TargetData
。它还在蓝图中实现了一个功能,可以在玩家特定的偏移(可以在子蓝图中设置)位置立即触发球体追踪。你也可以通过在C++或蓝图中子类化
URPGTargetType
实现自己的目标选取类型。
5. 常用的技能和效果
5.1 眩晕
眩晕可以打断一个角色正在施放的技能,阻止他施放新的技能,在整个眩晕的过程中阻止其移动。示例项目的陨石技能在击中的目标上应用了眩晕。
取消目标正在施放的技能,可以在stun
GameplayTag
添加时调用
AbilitySystemComponent->CancelAbilities()
。
在眩晕时阻止施放技能,可以在
GameplayAbilities
的
中添加stun
GameplayTag
。
在眩晕时阻止角色移动,可以重载
CharacterMovementComponent
的
GetMaxSpeed()
方法,在其拥有者有stun
GameplayTag
时返回0。
5.2 冲刺
示例项目提供了冲刺技能-按住
Left Shift
键角色会加速跑。
加速跑是可预测的,由
CharacterMovementComponent
向服务器发送一个标记实现。详见
GDCharacterMovementComponent.h/cpp
GA
处理
Left Shift
的输入事件,通知
CharacterMovementComponent
开始和停止加速,当
Left Shift
按下后同时预测耐力。详见
GA_Sprint_BP
5.3 瞄准
示例项目处理瞄准和冲刺相似,但瞄准会降低移动速度。
可预测的降低移动速度,详见
GDCharacterMovementComponent.h/cpp
。
处理输入详见
GA_AimDownSight_BP
,瞄准时不会消耗耐力值。
5.4 生命偷取
我在伤害计算的
中处理生命偷取。
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<UGameplayEffect>(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());
}
5.5 服务器和客户端的随机数生成
有时你需要在
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
发送是更好的选择。
5.6 致命一击
我在伤害
中处理致命一击。在
ExecutionCalculation
检查
GameplayEffect
是否带有
Effect.CanCrit
标签,如果有会根据暴击率(
Source
中的属性)生成一个随机数并将其加到暴击伤害中(同样来源于
Source
)。因此我并没有预测伤害,也不需要担心随机数在客户端和服务器的同步问题因为
ExecutionCalculation
仅在服务器上运行。如果你要使用支持预测的
MMC
完成这个伤害计算,可以通过
GameplayEffectSpec->GameplayEffectContext->GameplayAbilityInstance
获取
Random Seed
。
查看
如何实现的爆头,本质和上述相同,只不过没有使用暴击率而是检查
FHitResult
中的骨骼名字。
5.7 减速效果
Paragon中的减速效果不会叠加,但每一个应用的减速效果都会像平常一样追踪自己的生命周期,不过只会应用最大的减速效果给角色。
GAS
提供了
AggregatorEvaluateMetaData
用于解决此问题。详见
5.8 暂停游戏时生成目标数据
如果玩家在
WaitTargetData
AbilityTask
中等待生成
时需要暂停游戏,建议使用
slomo 0
替代
pause
。
6. GAS调试
通常在调试
GAS
相关问题时,需要了解如下事情:
- “我的属性值是什么?”
- “我有哪些游戏标签?”
- “我当前有哪些游戏效果?”
- “我有哪些技能, 哪些正在施放, 哪些不允许施放?”.
GAS
有两种方式在运行时回答上述问题:
和挂钩(hooks in)到
.
提示:
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
6.1 showdebug abilitysystem
在游戏控制台输入
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已报告
。
6.2 Gameplay Debugger
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)]
6.3 GAS 日志
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 |
详见 。
7. 优化
7.1 技能批处理
的激活,发送
TargetData
到服务器(可选),结束所有这些事情如果在一帧完成可以使用
Ability Batching
将两到三个
RPCs
优化到一个
RPC
。这种类型的
Abilities
常用于霰弹枪。
7.2 游戏表现批处理
如果你在同时发送了多个
,也可以考虑将他们合并到一个
RPC
中。这可以减少
RPCs
的数量并且使发送的数据量尽可能的小。
7.3 AbilitySystemComponent 复制模式
默认情况下,
ASC
处于
模式。这将会把所有的
复制到每一个客户端(这对于单人游戏没问题)。在多人游戏中,将玩家拥有的
ASCs
设置为
Mixed Replication Mode
,将AI控制的角色设置为
Minimal Replication Mode
。这将会把玩家角色上应用的
GEs
只复制给其角色的拥有者,AI控制的角色上的
GEs
将不会复制到客户端。
GameplayTags
将会被复制,
GameplayCues
将会被不可靠的广播发送给所有客户端,不管
Replication Mode
是什么。当所有客户端不需要看到它们时,这将减少复制
GEs
的网络数据。
7.4 属性代理复制
在有大量玩家的游戏中比如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
因为不需要为它额外优化。
详见 *
7.5 ASC 延迟加载
Fortnite Battle Royale (FNBR)的世界中有大量可被破坏的
AActors
(树,建筑等),每一个都带有一个
ASC
。这将增加内存的消耗。FNBR使用延迟加载
ASCs
的方案解决此问题,仅当需要时才加载
ASCs
(当这些
AActors
第一次被玩家伤害时)。一场游戏中很多
AActors
可能从未被伤害,这将减少整体内存的开销。
8. 建议
8.1 Gameplay Effect Containers
combine
,
,
, 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.
Blueprint AsyncTasks to Bind to ASC Delegates
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)]
9. 疑难解答
LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local!
You need to .
ScriptStructCache
errors
You need to call .
10. GAS名词缩写
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 |
11. 其他资源
- Source Code!
- Especially
GameplayPrediction.h
- Especially
- has a text channel dedicated to GAS
#gameplay-abilities-plugin
- Check pinned messages
12. GAS 更新日志
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.
4.25
- Fixed prediction of
RootMotionSource
AbilityTasks
- now additionally takes in the old
Attribute
value. We must supply that as the optional parameter to ourOnRep
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. - Added
to
UGameplayAbility
. - Crash Fix: Fixed a crash when adding a gameplay tag without a valid tag source selection.
- Crash Fix: Removed a few ways for attackers to crash a server through the ability system.
- Crash Fix: We now make sure we have a GamplayEffect definition before checking tag requirements.
- Bug Fix: Fixed an issue with gameplay tag categories not applying to function parameters in Blueprints if they were part of a function terminator node.
- Bug Fix: Fixed an issue with gameplay effects’ tags not being replicated with multiple viewports.
- Bug Fix: Fixed a bug where a gameplay ability spec could be invalidated by the InternalTryActivateAbility function while looping through triggered abilities.
- Bug Fix: Changed how we handle updating gameplay tags inside of tag count containers. When deferring the update of parent tags while removing gameplay tags, we will now call the change-related delegates after the parent tags have updated. This ensures that the tag table is in a consistent state when the delegates broadcast.
- Bug Fix: We now make a copy of the spawned target actor array before iterating over it inside when confirming targets because some callbacks may modify the array.
- Bug Fix: Fixed a bug where stacking GamplayEffects that did not reset the duration on additional instances of the effect being applied and with set by caller durations would only have the duration correctly set for the first instance on the stack. All other GE specs in the stack would have a duration of 1 second. Added automation tests to detect this case.
- Bug Fix: Fixed a bug that could occur if handling gameplay event delegates modified the list of gameplay event delegates.
- Bug Fix: Fixed a bug causing GiveAbilityAndActivateOnce to behave inconsistently.
- Bug Fix: Reordered some operations inside FGameplayEffectSpec::Initialize to deal with a potential ordering dependency.
- New: UGameplayAbility now has an OnRemoveAbility function. It follows the same pattern as OnGiveAbility and is only called on the primary instance of the ability or the class default object.
- New: When displaying blocked ability tags, the debug text now includes the total number of blocked tags.
- New: Renamed UAbilitySystemComponent::InternalServerTryActiveAbility to UAbilitySystemComponent::InternalServerTryActivateAbility.Code that was calling InternalServerTryActiveAbility should now call InternalServerTryActivateAbility.
- New: Continue to use the filter text for displaying gameplay tags when a tag is added or deleted. The previous behaviour cleared the filter.
- New: Don’t reset the tag source when we add a new tag in the editor.
- New: Added the ability to query an ability system component for all active gameplay effects that have a specified set of tags. The new function is called GetActiveEffectsWithAllTags and can be accessed through code or blueprints.
- New: When root motion movement related ability tasks end they now return the movement component’s movement mode to the movement mode it was in before the task started.
- New: Made SpawnedAttributes transient so it won’t save data that can become stale and incorrect. Added null checks to prevent any currently saved stale data from propagating. This prevents problems related to bad data getting stored in SpawnedAttributes.
- API Change: AddDefaultSubobjectSet has been deprecated. AddAttributeSetSubobject should be used instead.
- New: Gameplay Abilities can now specify the Anim Instance on which to play a montage.
4.24
- Fixed blueprint node
Attribute
variables resetting toNone
on compile. - Need to call
to use
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. - Fixed crash when copying a
GameplayTag
setter to a blueprint that didn’t have the variable previously defined. UGameplayAbility::MontageStop()
function now properly uses theOverrideBlendOutTime
parameter.- Fixed
GameplayTag
query variables on components not being modified when edited. - Added the ability for
GameplayEffectExecutionCalculations
to support scoped modifiers against “temporary variables” that aren’t required to be backed by an attribute capture.- Implementation basically enables
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. - To use, an execution has to add a tag to the new member variable
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 feature
- Implementation basically enables
- Added restricted tag quality-of-life improvements. Removed the default option for restricted
GameplayTag
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 thePawn
to the newController
. Useful because expects the owner of thePawn
to be theController
if theASC
lives on thePawn
.- Fixed bug with POD (Plain Old Data) in
FAttributeSetInittterDiscreteLevels
.