目录
- 4.6.1 游戏技能的定义 - Gameplay Ability Definition
- 4.6.2 绑定输入到技能系统组件 - Binding Input to the ASC
- 4.6.3 赋予技能 - Granting Abilities
- 4.6.4 激活技能 - Activating Abilities
- 4.6.5 取消技能 - Canceling Abilities
- 4.6.6 获取处于激活状态的技能 - Getting Active Abilities
- 4.6.7 实例化的策略 - Instancing Policy
- 4.6.8 网络执行策略 - Net Execution Policy
- 4.6.9 技能标签 - Ability Tags
- 4.6.10 Gameplay Ability Spec
- 4.6.11 传递数据到技能 - Passing Data to Abilities
- 4.6.12 技能的消耗和冷却 - Ability Cost and Cooldown
- 4.6.13 升级技能 - Leveling Up Abilities
- 4.6.14 技能组 - Ability Sets
- 4.6.15 技能批处理 - Ability Batching
- 4.6.16 网络安全策略 - Net Security Policy
4.6.1 游戏技能的定义 - Gameplay Ability Definition
GameplayAbilities
(常简称GA
),是游戏中Actor
可以完成的任意的动作或者技能。在同一时间,可以同时存在且激活的GameplayAbility
的数量并没有限制,比如说冲刺能力和射击的能力就可以同时存在。这些GameplayAbilities
在蓝图或者C++中都可以实现。
适合使用GameplayAbilities
来实现的动作举例:
- Jumping - 跳跃
- Sprinting - 冲刺
- Shooting a gun - 持枪射击
- Passively blocking an attack every X number of seconds - 每X秒被动阻挡一次攻击
- Using a potion - 使用药水
- Opening a door - 开门(机关)
- Collecting a resource - 收集资源
- Constructing a building - 构建建筑
不适合使用GameplayAbilities
来实现的动作:
- Basic movement input - 基本移动输入
- Some interactions with UIs - 一些UI相关的交互,建议不要使用
GameplayAbility
来实现商店购买相关的功能。
这些并不是规则,而只是我的建议。你的设计和实现可以根据具体情况和玩法去灵活变通。
GameplayAbilities
默认就带有一项功能,即在对属性进行修改时会根据等级具体决定修改的数值的多少,甚至于根据等级去改变GameplayAbility
的功能也是有可能的。
GameplayAbilities
会在所属客户端上运行,而在服务端则会根据Net Execution Policy
(而不是模拟代理节点)来决定是否也运行。Net Execution Policy
决定了GameplayAbility
是否进行本地的预测。对optional cost and cooldown GameplayEffects
他们会包含一些默认的行为。GameplayAbilities
使用AbilityTasks
来处理那些会持续一段时间的动作,比如等待某个事件,等待某个属性变化,等待玩家选择某个目标,或者通过Root Motion Source
来移动某个Character
。模拟的客户端将不会运行GameplayAbilities
。相对应的,当服务器运行技能时,任何需要在模拟代理上可视化呈现的部分(如播放动画蒙太奇)都将通过AbilityTasks
或者GameplayCues
(负责声音和粒子部分)来复制或者远程过程调用。
所有的GameplayAbilities
都需要重写ActivateAbility()
以实现你自己的游玩逻辑。当GameplayAbility
结束或者取消时,还可以在EndAbility()
添加一些额外的运行逻辑。
简单的GameplayAbility
流程图:
稍微复杂一些的GameplayAbility
流程图:
复杂的技能也可以使用多个互相之间交互(激活、取消等)的GameplayAbilities
来实现。
4.6.1.1 复制策略 - Replication Policy
不要使用这个选项。本身这个名字存在一定的误导性,你要知道其实你并不需要关心这个。默认情况下GameplayAbilitySpecs
就会被从服务端复制到所属客户端。上面也提到过,GameplayAbilities
不会在模拟代理上运行。他们使用AbilityTasks
和GameplayCues
来复制或者远程过程调用可视化的变化到模拟代理。Epic的Dave Ratti也表明他希望能够在未来删除这个选项.
4.6.1.2 服务器端远程技能取消 - Server Respects Remote Ability Cancellation
这个选项常常会引发一些麻烦。即,如果客户端的GameplayAbility
因为取消或者自然完成而结束,它会强制服务器也去结束(无论在服务器是否也完成)。这个问题很重要,特别是针对使用本地预测的GameplayAbilities
高延迟的玩家来说。通常你最好禁用这个选项。
4.6.1.3 直接对输入的复制 - Replicate Input Directly
启用这个选项将会一直把输入的按下和释放事件复制给服务器。Epic并不建议这样使用,取而代之的,最好使用内置到已存在的输入相关的AbilityTasks
的Generic Replicated Events
,前提是你已经将你的输入绑定到ASC
。
Epic留下的注释:
/** 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 绑定输入到技能系统组件 - Binding Input to the ASC
ASC
允许直接将输入绑定到它上面,并且当你赋予GameplayAbilities
时可以指定相应的输入。指定给GameplayAbilities
的输入动作会在输入触发后且GameplayTag
满足要求的情况下自动激活这些GameplayAbilities
。要使用内置的响应输入的AbilityTasks
就需要做好IA的分配。
除了指定输入动作从而激活GameplayAbilities
外,ASC
还可以接受通用的Confirm
和Cancel
输入。这些特殊的输入由AbilityTasks
来使用,从而进行一些操作的确认以及取消,比如Target Actors
,即目标的选取和取消选取。
要绑定输入到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()
,那么此时就没有InputComponent
可以用来去绑定了)。实例项目演示了如何在两处进行绑定,并且通过一个布尔字段进行控制,从而令绑定操作实际上只执行一次。
注意: 示例项目中枚举的Confirm
和Cancel
并没有和项目配置里的输入动作的名称匹配(ConfirmTarget
and CancelTarget
),但是我们在BindAbilityActivationToInputComponent()
进行了他们之间映射的构建。这里我们只是针对他们利用映射构建做了一下特殊处理,所以他们才不需要名称相同,当然他们也可以去进行依据名称的匹配。枚举中的其他输入都必须与项目设置中的输入动作名称相匹配。
对于只会被通过一个输入来激活的GameplayAbilities
(比如MOBA游戏中,技能始终都在一个固定的技能槽中),我偏向于在UGameplayAbility
的子类里添加一个变量,利用它来定义输入。然后我可以在赋予技能时从ClassDefaultObject
里读取这个变量。
4.6.2.1 在不激活技能的情况下绑定到输入 - Binding to Input without Activating Abilities
如果你不希望你的GameplayAbilities
在输入被按下时自动激活,但是仍然想要把输入绑定到对应技能并使用AbilityTasks
,你可以在你的UGameplayAbility
子类中添加一个新的布尔变量,bActivateOnInput
,默认设置为true
,并且重载UAbilitySystemComponent::AbilityLocalInputPressed()
:
void UGSAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID)
{
// Consume the input if this InputID is overloaded with GenericConfirm/Cancel and the GenericConfim/Cancel callback is bound
if (IsGenericConfirmInputBound(InputID))
{
LocalInputConfirm();
return;
}
if (IsGenericCancelInputBound(InputID))
{
LocalInputCancel();
return;
}
// ---------------------------------------------------------
ABILITYLIST_SCOPE_LOCK();
for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
{
if (Spec.InputID == InputID)
{
if (Spec.Ability)
{
Spec.InputPressed = true;
if (Spec.IsActive())
{
if (Spec.Ability->bReplicateInputDirectly && IsOwnerActorAuthoritative() == false)
{
ServerSetInputPressed(Spec.Handle);
}
AbilitySpecInputPressed(Spec);
// Invoke the InputPressed event. This is not replicated here. If someone is listening, they may replicate the InputPressed event to the server.
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());
}
else
{
UGSGameplayAbility* GA = Cast<UGSGameplayAbility>(Spec.Ability);
if (GA && GA->bActivateOnInput)
{
// Ability is not active, so try to activate it
TryActivateAbility(Spec.Handle);
}
}
}
}
}
}
4.6.3 赋予技能 - Granting Abilities
赋予GameplayAbility
到某个ASC
,是将其添加到ASC
的ActivatableAbilities
列表之中,并在满足GameplayTag
requirements时根据意愿允许其激活。
我们在服务器赋予GameplayAbilities
,然后会自动将GameplayAbilitySpec
复制到所属客户端。其他的客户端/模拟代理并不会接收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
的要求后自动激活。当然,这种激活方式不一定能够满足所有的需求。ASC
提供了四种其他的方法来激活GameplayAbilities
:通过GameplayTag
激活,通过GameplayAbility
类来激活,通过GameplayAbilitySpec
句柄来激活,以及通过事件进行激活。通过事件激活GameplayAbility
可以让你随事件传入一定量的数据。
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
允许你传入一定量的数据。
GameplayAbility
的Triggers
可以允许你在添加或者移除GameplayTag
时对GameplayAbility
进行激活。
注意: 当在蓝图中通过事件激活某个GameplayAbility
,你必须使用ActivateAbilityFromEvent
节点,同时标准ActivateAbility
节点必须不能在你的蓝图中出现。如果ActivateAbility
节点存在的话,则会忽略ActivateAbilityFromEvent
节点。
注意: 当GameplayAbility
应该结束时不要忘记调用EndAbility()
,除非该GameplayAbility
是作为被动技能存在。
本地预测的 GameplayAbilities
的激活步骤:
- 所属客户端 调用
TryActivateAbility()
- 调用
InternalTryActivateAbility()
- 调用
CanActivateAbility()
,返回值是去检查GameplayTag
的要求是否满足,ASC
是否能够承受消耗,GameplayAbility
是否出于冷却状态,以及是否当前有其他实例出于激活状态 - 调用
CallServerTryActivateAbility()
,并且传递生成好的Prediction Key
- 调用
CallActivateAbility()
- 调用
PreActivate()
,Epic将这个称为例行公事 - 调用
ActivateAbility()
,即最终激活这个技能
服务器接收CallServerTryActivateAbility()
- 调用
ServerTryActivateAbility()
- 调用
InternalServerTryActivateAbility()
- 调用
InternalTryActivateAbility()
- 调用
CanActivateAbility()
,并且返回:GameplayTag
的要求是否得到满足,ASC
是否能够承受消耗,GameplayAbility
是否出于冷却状态,以及是否当前有其他实例出于激活状态 - 调用
ClientActivateAbilitySucceed()
,如果成功的话,更新其ActivationInfo
,表明其激活行为已被服务器确认,并且广播OnConfirmDelegate
委托。这和输入的确认是不同的两回事。 - 调用
CallActivateAbility()
- 调用
PreActivate()
,Epic将这个称为例行公事 - 调用
ActivateAbility()
,即最终激活这个技能
无论任何时候服务器激活失败,它会去调用ClientActivateAbilityFailed()
,立即结束客户端的GameplayAbility
并且撤销任何可以预测的变化。
4.6.4.1 被动技能 - Passive Abilities
要实现一个被动技能GameplayAbilities
,其自动激活且可以持续允许,需要重载UGameplayAbility::OnAvatarSet()
(它在GameplayAbility
被赋予且AvatarActor
被设置时会自动调用),并且调用TryActivateAbility()
。
我建议在你自定义的UGameplayAbility
类里添加一个bool
量,用来指明在GameplayAbility
被赋予时是否应该去激活。示例项目中实际利用这个来实现了被动护甲堆叠的技能。
被动的GameplayAbilities
通常会将Net Execution Policy
设置为Server Only
。
void UGDGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec)
{
Super::OnAvatarSet(ActorInfo, Spec);
if (ActivateAbilityOnGranted)
{
bool ActivatedAbility = ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
}
}
Epic对该函数作了如下描述:这是去初始化被动技能并且干类似BeginPlay
这种类型的事情的位置。
4.6.5 取消技能 - Canceling Abilities
要从内部取消一个GameplayAbility
,你可以调用CancelAbility()
。这将会调用EndAbility()
并且将其中的WasCancelled
参数设置为真。
要从外部取消一个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();
注意: 我发现如果你有一个 Non-Instanced
的GameplayAbilities
时,CancelAllAbilities
似乎并无法正常起作用。它似乎是在遇到Non-Instanced
的GameplayAbility
时会取消掉。CancelAbilities
在处理Non-Instanced
的 GameplayAbilities
时候表现更加良好,示例项目中也是用的这种处理方式(跳跃就是用的Non-Instanced
的 GameplayAbilities
来做的)。当然这方面你的做法可以是不同的。
4.6.6 获取处于激活状态的技能 - Getting Active Abilities
新手经常会提一些类似“我怎么获取到激活的技能”这样类似的问题,希望可能去操作其上的变量或者是去取消掉这个技能。某一个事件点上可以同时有多个GameplayAbility
处于激活状态,所以并不会有某个所谓的“active ability”让你去获取。取而代之的,你必须在ASC
的名为ActivatableAbilities
的列表(ASC
上存储赋予的GameplayAbilities
的位置)中去查询,去尝试寻找匹配你所寻找的Asset
或者Granted
的GameplayTag
的技能。
UAbilitySystemComponent::GetActivatableAbilities()
函数会返回一个TArray<FGameplayAbilitySpec>
,你可以在这个基础上进行迭代。
ASC
也提供了另一个帮助函数,可以传入一个GameplayTagContainer
参数来进行查找,这比上面直接在GameplayAbilitySpecs
列表上进行迭代更加方便。其中的bOnlyAbilitiesThatSatisfyTagRequirements
参数只会返回匹配GameplayTag
要求且当前能够被激活的GameplayAbilitySpecs
。例如,你可以有两个基本的攻击GameplayAbilities
,其中一个是使用武器的,另一个使用的是拳头。根据是否装备武器设定相应的GameplayTag
,从而激活我们想要的那个。参考Epic对这个函数的注释以获取更多信息。
UAbilitySystemComponent::GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true)
一旦你获取到你想要找的FGameplayAbilitySpec
,你可以调用技能上的IsActive()
函数来判断其是否处于激活状态中。
4.6.7 实例化的策略 - Instancing Policy
GameplayAbility
的Instancing Policy
决定了当GameplayAbility
激活时是否去进行实例化以及如何实例化的问题。
Instancing Policy | 描述 | 何时使用举例 |
---|---|---|
Instanced Per Actor | 每个ASC 只有一个GameplayAbility 的实例,在技能的重复激活时进行复用。 | 这可能是你使用的最多的Instancing Policy 。你可以在任何技能上使用它,并且在技能的重复激活期间提供持久性。设计者需要负责在激活时去手动重置需要的变量。 |
Instanced Per Execution | 每次激活一个GameplayAbility ,就会去创建一个新的GameplayAbility 的实例。 | 这种做法的好处是你每次激活GameplayAbilities 时都会去重置所有变量(不需要手动)。相对于上面的Instanced Per Actor 这种做法开销非常大,因为每当激活一个新的GameplayAbilities 都需要进行一次实例化。示例项目种并没有使用这个。 |
Non-Instanced | GameplayAbility 在它的ClassDefaultObject 上进行操作。不会去创建任何的实例。 | 在三个做法之中这种做法开销最小,但是其使用起来颇为严苛,且限制颇多。Non-Instanced 的GameplayAbilities 不能存储状态,意味着不能有动态变量且不能绑定AbilityTask 的委托。这个的最佳用途是频繁使用的简单技能,例如MOBA或者RTS游戏种小兵的普通攻击。示例项目中跳跃的GameplayAbility 就是Non-Instanced 。 |
4.6.8 网络执行策略 - Net Execution Policy
GameplayAbility
的Net Execution Policy
决定了谁来运行GameplayAbility
以及以什么样的顺序来运行。
Net Execution Policy | 描述 |
---|---|
Local Only | GameplayAbility 只运行在所属客户端上。这个对于那些只会有本地的视觉等装饰性的变化的技能来说是非常好用的。单人游戏应该使用Server Only 。 |
Local Predicted | Local Predicted 的GameplayAbilities 首先在所属客户端上激活,然后才是服务器。服务器那边将会修正客户端预测的不正确的部分。参考Prediction。 |
Server Only | GameplayAbility 只在服务器上运行。被动的GameplayAbilities 通常是Server Only 。。 |
Server Initiated | Server Initiated 的GameplayAbilities 首先在服务器上进行激活,然后才是所属客户端。我个人不太使用这个。。 |
4.6.9 技能标签 - Ability Tags
GameplayAbilities
附带着GameplayTagContainers
,其又有内置的逻辑。这些GameplayTags
都是不去复制的。
GameplayTag Container | 描述 |
---|---|
Ability Tags | GameplayAbility 所拥有的GameplayTags 。这些只是用来描述GameplayAbility 的GameplayTags 。 |
Cancel Abilities with Tag | 当这个GameplayAbility 激活时,如果还有其他GameplayAbilities 的Ability Tags 也有这种GameplayTags 的话,那么这些其他的技能就会被取消掉。 |
Block Abilities with Tag | 当这个GameplayAbility 激活时,如果还有其他GameplayAbilities 的Ability Tags 也有这种GameplayTags 的话,那么就会阻止其他的这些技能的激活。 |
Activation Owned Tags | 在GameplayAbility 激活时,这些GameplayTags 会被给到GameplayAbility 的所有者。再次强调这些不会被进行复制。 |
Activation Required Tags | 仅当所有者拥有所有这些GameplayTags 时,GameplayAbility 才能够被激活。 |
Activation Blocked Tags | 如果所有者有这些GameplayTags 中的任意一些,那么GameplayAbility 就不能够被激活。 |
Source Required Tags | 仅当Source 拥有所有这些GameplayTags 时,这个GameplayAbility 才能够被激活。Source 的GameplayTags 仅在 事件触发GameplayAbility 时进行设置。 |
Source Blocked Tags | 如果Source 拥有这些GameplayTags 中的任意一些,那么GameplayAbility 就不能够被激活。Source 的GameplayTags 仅在 事件触发GameplayAbility 时进行设置。 |
Target Required Tags | 仅当Target 拥有所有这些GameplayTags 时,这个GameplayAbility 才能够被激活。Target 的GameplayTags 仅在 事件触发GameplayAbility 时进行设置。 |
Target Blocked Tags | 如果Target 拥有这些GameplayTags 中的任意一些,那么GameplayAbility 就不能够被激活。Target 的GameplayTags 仅在 事件触发GameplayAbility 时进行设置。 |
4.6.10 Gameplay Ability Spec
在技能被赋予后,GameplayAbilitySpec
就会存在于ASC
,其定义了处于可被激活状态的GameplayAbility
—— 根据GameplayAbility
类,等级,输入绑定,以及运行时状态。
当GameplayAbility
在服务区上被赋予之后,服务器会复制GameplayAbilitySpec
到所属客户端,然后才能够被进一步激活。
激活一个GameplayAbilitySpec
将会依照其Instancing Policy
创建一个GameplayAbility
的实例(如果是Non-Instanced
的GameplayAbilities
则并不会创建相应的实例)。
4.6.11 传递数据到技能 - Passing Data to Abilities
GameplayAbilities
的一般的使用流程是Activate->Generate Data->Apply->End
。有些时候你需要在已有数据上做一些操作。GAS为将外部数据传入到GameplayAbilities
内部这样的操作提供了一些可选项:
方法 | 描述 |
---|---|
通过事件激活GameplayAbility | 可以在通过事件对GameplayAbility 进行激活时包含一定量的数据。对于本地预测的GameplayAbilities ,事件的数据们将会从客户端复制到服务端。如果有些数据无法利用已存在的变量去处理,这时可以使用Optional Object 或者TargetData 两种变量。这样做的不便之处是就无法利用输入绑定来激活技能了。要通过事件激活GameplayAbility ,GameplayAbility 本身必须配置好Triggers ,指定GameplayTag 并且选定GameplayEvent 的选项。要发送事件,可以使用函数UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload) 。 |
使用WaitGameplayEvent 的AbilityTask | 在GameplayAbility 激活之后,使用WaitGameplayEvent 这个AbilityTask 去通知GameplayAbility 监听带有数据的事件。这个事件和发送过程和通过事件去激活GameplayAbilities 是一样的。这样做的不便之处是事件并不是由AbilityTask 来进行复制,只能用于Local Only 和Server Only 的GameplayAbilities 。你可以编写自己的AbilityTask ,来支持复制带数据的事件。 |
使用TargetData | 使用一个自定义的TargetData 结构是一个在客户端和服务端之间传递数据的好办法。 |
将数据存储到OwnerActor 或是AvatarActor | 使用那些存储在OwnerActor ,AvatarActor 或者任何其他你可以引用得到的对象的复制变量。这个方法是最灵活,且能够支持通过输入绑定来激活GameplayAbilities 。但是,这个方式并不能保证在使用时复制来的数据一定的同步的。 你必须保证提前性——即如果你设置一个复制变量,然后立即激活GameplayAbility ,那么由于可能的潜在的丢包问题就无法保证接收者上面的顺序。 |
4.6.12 技能的消耗和冷却 - Ability Cost and Cooldown
GameplayAbilities
会带有可选的消耗和冷却的功能。技能消耗是为了激活由Instant
类型的 GameplayEffect
( Cost GE
)实现的GameplayAbility
,所预定义的所需某些Attributes
的数量。技能冷却则是为了控制GameplayAbility
的重新激活所设定的计时器,其实现是通过一个Duration
类型的GameplayEffect
(Cooldown GE
)。
在GameplayAbility
调用UGameplayAbility::Activate()
之前,他会调用首先调用UGameplayAbility::CanActivateAbility()
。这个函数会去检查所属的ASC
是否能够承担技能的消耗(UGameplayAbility::CheckCost()
)并且GameplayAbility
并没有处于冷却回转期间(UGameplayAbility::CheckCooldown()
)。
在GameplayAbility
调用Activate()
之后,可选地,他可以使用UGameplayAbility::CommitAbility()
在任意时间点提交消耗和冷却,其内部实现实际上是去分别调用UGameplayAbility::CommitCost()
和UGameplayAbility::CommitCooldown()
。设计者可能会根据实际需求去选择单独调用CommitCost()
或是CommitCooldown()
。提交消耗和冷却会去再一次调用CheckCost()
以及CheckCooldown()
,这也是GameplayAbility
去根据自身信息检查是否能够激活的最后一道保险。所属的ASC
的Attributes
可能在GameplayAbility
激活之后就会发生变化,从而在技能提交时无法满足消耗。技能和冷却的提交可以是locally predicted,前提是prediction key在提交时是合法的。
参阅CostGE
和CooldownGE
获取更多实现细节。
4.6.13 升级技能 - Leveling Up Abilities
对于提升技能等级这件事,有两种通常的做法:
技能等级提升的方法 | 描述 |
---|---|
根据新的等级,剥离然后重新赋予技能 | 从ASC 中剥离(删除)掉GameplayAbility ,然后在服务器上以新的等级重新进行赋予。这种做法下,如果当时技能正处于激活状态,那么他就会立即被结束掉。 |
提升GameplayAbilitySpec 的等级 | 在服务器上,查找到GameplayAbilitySpec ,增加其等级,然后将其标记为dirty,这样就可以将其复制到所属的客户端了。这种做法之下,如果当时技能正处于激活状态下,是不会将其打断或者结束的。 |
上面两种方法的主要不同之处在于,技能升级的当口会不会把技能给取消掉。这一点你可以根据你的GameplayAbilities
的实际需求来灵活选择。我建议是在你的UGameplayAbility
子类中用一个bool变量去具体控制到底是使用哪一种方法。
4.6.14 技能组 - Ability Sets
GameplayAbilitySets
是一系列便捷的UDataAsset
类,可以用来存储输入绑定以及角色的初始的GameplayAbilities
的列表。可以继承它然后再添加一些额外的逻辑和属性。Paragon项目中为每个英雄准备了一个GameplayAbilitySet
,其中包含了所有的赋予到其身上的GameplayAbilities
.
就目前来看,我发现这个类并不是非常常用。实例项目在GDCharacterBase
及其子类中就完成了和GameplayAbilitySets
类似的功能。
4.6.15 技能批处理 - Ability Batching
传统的Gameplay Ability
的声明周期包含了至少两到三次的从客户端到服务端的RPC,即:
CallServerTryActivateAbility()
ServerSetReplicatedTargetData()
(不是必须的)ServerEndAbility()
如果GameplayAbility
在一帧中的一个原子组内执行所有这些操作的话,我们可以将这两个到三个的RPC打包成一个RPC进而优化操作。GAS
中将这种针对RPC的优化称为是Ability Batching
,即技能的批处理。Ability Batching
常见的一个使用情况就是扫射的枪械。枪械激活,执行一个射线检测,发送TargetData
到服务器,然后在一帧的一个原子组中结束技能。GASShooter示例工程中演示了这项技术的使用。
半自动枪械就是最好的案例,可以将CallServerTryActivateAbility()
,ServerSetReplicatedTargetData()
(子弹撞击结果),以及ServerEndAbility()
打包到一个RPC而不是三个单独的RPC。
全自动/爆破枪械可以将第一发子弹的CallServerTryActivateAbility()
和ServerSetReplicatedTargetData()
打包到一个RPC里而不是单独的两个RPC。后续的每发子弹则是它自己的ServerSetReplicatedTargetData()
的RPC。最后,ServerEndAbility()
则是作为一个单独的RPC,在枪械停火后发送。这种情况并不十分美好,我们仅仅在第一发子弹上节省了一个RPC。相对的,针对这种情况还有另外一种做法,即通过Gameplay Event
来进行技能的激活,从而将子弹的TargetData
放在EventPayload
里从客户端发送到服务端。后面这种方法的不便之处就是TargetData
其实是在技能之外生成的,而批处理的方法则是在技能里进行的生成过程。
Ability Batching
默认在ASC
上是关闭的。想要激活Ability Batching
,需要重载ShouldDoServerAbilityRPCBatch()
并返回true:
virtual bool ShouldDoServerAbilityRPCBatch() const override { return true; }
现在Ability Batching
已经被激活了,在激活你希望批处理的技能之前,你必须预先创建一个FScopedServerAbilityRPCBatcher
。这个特殊的结构体将会试着去打包在其作用域内的任何技能。一旦FScopedServerAbilityRPCBatcher
超出范围,其他任何技能都不会打包进去。FScopedServerAbilityRPCBatcher
的工作原理是在每个可批处理的函数中都有特殊的代码,这些特殊代码可拦截发送RPC的调用,并将消息打包为批处理结构。当FScopedServerAbilityRPCBatcher
超出作用域,它会自动 在UAbilitySystemComponent::EndServerAbilityRPCBatch()
中将这个批结构发送到服务器。服务器会在UAbilitySystemComponent::ServerAbilityRPCBatch_Internal(FServerAbilityRPCBatch& BatchInfo)
中接收这个批RPC。BatchInfo
参数包含了一些标签:技能是否应该结束,输入是否在激活时已经按下,是否包含TargetData
。如果你想想调试你的批处理是否正常工作,这里是个打断点的好地方。另外,可以使用控制台程序输入AbilitySystem.ServerRPCBatching.Log 1
来激活特定的技能批处理的日志。
这一机制只能使用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()
来进行技能的结束(它是由技能外部的另外一个本地技能来管理,具体就是根据当前的开火模式来管理玩家的输入以及技能的批处理调用)。因为所有的RPC必须被在FScopedServerAbilityRPCBatcher
的作用域内调用,我提供了一个EndAbilityImmediately
参数,从而令本地的控制/管理能够指出这个技能是否应该打包 EndAbility()
调用(半自动),亦或是不打包EndAbility()
调用(全自动),这样它可以在后面的某个时间用自己的RPC来发送EndAbility()
。
GASShooter项目中暴露了一个蓝图节点,用以在本地执行的技能中来出发技能批处理。
4.6.16 网络安全策略 - Net Security Policy
GameplayAbility
的NetSecurityPolicy
决定了技能具体是在网络上的哪部分去执行。这可以防止客户端去尝试执行受限的技能。
NetSecurityPolicy | 描述 |
---|---|
ClientOrServer | 没有安全要求。客户端和服务端可以自由得执行和结束技能。 |
ServerOnlyExecution | 服务端会忽略客户端发起的技能执行的请求。客户端仍然可以发起请求,令服务端取消或者结束这个技能。 |
ServerOnlyTermination | 服务端会忽略客户端发起的技能的取消和结束请求。客户端仍然可以发起技能执行的请求。 |
ServerOnly | 服务端控制技能的执行和结束。发起请求的客户端会被忽略。 |