4.6 Gameplay Abilities
4.6.1 Gameplay Ability定义
GameplayAbilities
(GA
) 是游戏中 Actor
可以执行的任何动作或技能。多个 GameplayAbility
可以同时激活,例如奔跑和射击。这些可以在 Blueprint 或 C++ 中创建。
GameplayAbilities
示例:
- 跳跃
- 奔跑
- 射击
- 每隔X秒被动格挡一次攻击
- 使用药水
- 开门
- 收集资源
- 建造建筑
不应使用 GameplayAbilities
实现的内容:
- 基本移动输入
- 一些与UI的交互(不要使用
GameplayAbility
从商店购买物品)
这些不是规则,只是我的建议。您的设计和实现可能会有所不同。
GameplayAbilities
自带默认功能,可以通过等级来修改属性的变化量或改变 GameplayAbility
的功能。
GameplayAbilities
根据 Net Execution Policy
在拥有客户端和/或服务器上运行,但不在模拟代理上运行。Net Execution Policy
决定了 GameplayAbility
是否会在本地 预测。它们包括用于 可选成本和冷却时间 GameplayEffects
的默认行为。GameplayAbilities
使用 AbilityTasks
来处理随时间发生的动作,如等待事件、等待属性变化、等待玩家选择目标或使用 Root Motion Source
移动 Character
。模拟客户端不会运行 GameplayAbilities
。相反,当服务器运行能力时,任何需要在模拟代理上播放的视觉效果(如动画蒙太奇)将通过 AbilityTasks
或 GameplayCues
进行复制或RPC,以实现声音和粒子等化妆效果。
所有 GameplayAbilities
都会重写其 ActivateAbility()
函数以实现您的游戏逻辑。可以在 GameplayAbility
完成或取消时运行的 EndAbility()
中添加额外的逻辑。
一个简单的 GameplayAbility
流程图:
一个更加复杂的 GameplayAbility
流程图:
复杂的能力可以通过多个相互交互(激活、取消等)的 GameplayAbilities
来实现。
4.6.1.1 Replication Policy(复制策略)
不要使用此选项。名称具有误导性,您不需要它。GameplayAbilitySpecs
默认从服务器复制到拥有客户端。如上所述,GameplayAbilities
不会在模拟代理上运行。它们使用 AbilityTasks
和 GameplayCues
将视觉变化复制或RPC到模拟代理。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 的注释:
/** 直接输入状态复制。如果 bReplicateInputDirectly 在能力上为真,则会调用这些方法,通常不建议使用。(相反,建议使用通用复制事件)。 */
UAbilitySystemComponent::ServerSetInputPressed()
4.6.2 将输入绑定到 ASC
ASC
允许您直接将输入动作绑定到它,并在授予 GameplayAbilities
时将这些输入分配给 GameplayAbilities
。如果满足 GameplayTag
要求,分配给 GameplayAbilities
的输入动作在按下时会自动激活这些 GameplayAbilities
。分配的输入动作需要使用响应输入的内置 AbilityTasks
。
除了分配给激活 GameplayAbilities
的输入动作外,ASC
还接受通用的 Confirm
和 Cancel
输入。这些特殊输入由 AbilityTasks
用于确认诸如 Target Actors
之类的事情或取消它们。
要将输入绑定到 ASC
,您必须首先创建一个枚举,将输入动作名称转换为字节。枚举名称必须与项目设置中用于输入动作的名称完全匹配。DisplayName
无关紧要。
来自示例项目:
UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
// 0 无
None UMETA(DisplayName = "None"),
// 1 确认
Confirm UMETA(DisplayName = "Confirm"),
// 2 取消
Cancel UMETA(DisplayName = "Cancel"),
// 3 左键
Ability1 UMETA(DisplayName = "Ability1"),
// 4 右键
Ability2 UMETA(DisplayName = "Ability2"),
// 5 Q键
Ability3 UMETA(DisplayName = "Ability3"),
// 6 E键
Ability4 UMETA(DisplayName = "Ability4"),
// 7 R键
Ability5 UMETA(DisplayName = "Ability5"),
// 8 奔跑
Sprint UMETA(DisplayName = "Sprint"),
// 9 跳跃
Jump UMETA(DisplayName = "Jump")
};
如果您的 ASC
位于 Character
上,则在 SetupPlayerInputComponent()
中包含绑定到 ASC
的函数:
// 绑定到 AbilitySystemComponent
FTopLevelAssetPath AbilityEnumAssetPath = FTopLevelAssetPath(FName("/Script/GASDocumentation"), FName("EGDAbilityInputID"));
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"),
FString("CancelTarget"), AbilityEnumAssetPath, static_cast<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(EGDAbilityInputID::Cancel)));
如果您的 ASC
位于 PlayerState
上,在 SetupPlayerInputComponent()
中可能会出现潜在的竞争条件,即 PlayerState
可能尚未复制到客户端。因此,我建议尝试在 SetupPlayerInputComponent()
和 OnRep_PlayerState()
中绑定输入。OnRep_PlayerState()
本身不足以解决问题,因为可能会出现 Actor
的 InputComponent
为空的情况,当 PlayerState
在 PlayerController
告诉客户端调用 ClientRestart()
之前复制时,这会创建 InputComponent
。示例项目演示了在两个位置尝试绑定,并使用布尔值控制过程,以便只绑定一次输入。
注意: 在示例项目中,枚举中的 Confirm
和 Cancel
与项目设置中的输入动作名称(ConfirmTarget
和 CancelTarget
)不匹配,但我们在 BindAbilityActivationToInputComponent()
中提供了它们之间的映射。这些是特殊的,因为我们提供了映射,它们不必匹配,但可以匹配。枚举中的所有其他输入必须与项目设置中的输入动作名称匹配。
对于将始终由一个输入激活的 GameplayAbilities
(它们将始终存在于同一“槽”中,如MOBA),我更喜欢在我的 UGameplayAbility
子类中添加一个变量,在其中定义它们的输入。然后我可以在授予能力时从 ClassDefaultObject
中读取此信息。
4.6.2.1 绑定输入时不激活Abilities
如果您不希望 GameplayAbilities
在按下输入时自动激活,但仍然将它们绑定到输入以用于 AbilityTasks
,您可以在 UGameplayAbility
子类中添加一个新的布尔变量 bActivateOnInput
,默认为 true
,并重写 UAbilitySystemComponent::AbilityLocalInputPressed()
。
void UGSAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID)
{
// 如果此 InputID 被通用确认/取消重载并且通用确认/取消回调已绑定,则消耗输入
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);
// 调用 InputPressed 事件。此处不进行复制。如果有人在监听,他们可能会将 InputPressed 事件复制到服务器。
InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());
}
else
{
UGSGameplayAbility* GA = Cast<UGSGameplayAbility>(Spec.Ability);
if (GA && GA->bActivateOnInput)
{
// 能力未激活,因此尝试激活它
TryActivateAbility(Spec.Handle);
}
}
}
}
}
}
4.6.3 授予Ability
将 GameplayAbility
授予 ASC
时会将其添加到 ASC
的 ActivatableAbilities
列表中,允许其在满足 GameplayTag
要求 时随时激活 GameplayAbility
。
我们在服务器上授予 GameplayAbilities
,然后自动将 GameplayAbilitySpec
复制到拥有客户端。其他客户端/模拟代理不会接收 GameplayAbilitySpec
。
示例项目在 Character
类上存储一个 TArray<TSubclassOf<UGDGameplayAbility>>
,它在游戏开始时读取并授予:
void AGDCharacterBase::AddCharacterAbilities()
{
// 授予能力,但仅在服务器上
if (Role != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->bCharacterAbilitiesGiven)
{
return;
}
for (TSubclassOf<UGDGameplayAbility>& StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
}
AbilitySystemComponent->bCharacterAbilitiesGiven = true;
}
在授予这些 GameplayAbilities
时,我们正在创建 GameplayAbilitySpecs
,包括 UGameplayAbility
类、能力等级、绑定的输入以及 SourceObject
或者是谁将此 GameplayAbility
授予此 ASC
。
4.6.4 激活Abilities
如果 GameplayAbility
被分配了一个输入动作,当按下输入时,它将自动激活,如果满足其 GameplayTag
要求。这可能并不总是激活 GameplayAbility
的理想方式。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, const FGameplayEventData* GameplayEventData);
要通过事件激活 GameplayAbility
,必须在 GameplayAbility
中设置其 Triggers
。分配一个 GameplayTag
并选择一个 GameplayEvent
选项。要发送事件,请使用函数 UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload)
。通过事件激活 GameplayAbility
允许您传递包含数据的负载。
GameplayAbility
Triggers
还允许您在 GameplayTag
添加或移除时激活 GameplayAbility
。
注意: 在 Blueprint 中通过事件激活 GameplayAbility
时,必须使用 ActivateAbilityFromEvent
节点。
注意: 除非您有一个将始终运行的 GameplayAbility
(如被动能力),否则不要忘记在 GameplayAbility
应该终止时调用 EndAbility()
。
本地客户端预测 GameplayAbilities
的激活顺序:
- Owning客户端 调用
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 被动 Abilities
要实现自动激活并持续运行的被动 GameplayAbilities
,重写 UGameplayAbility::OnAvatarSet()
,该函数在授予 GameplayAbility
并设置 AvatarActor
时自动调用,并调用 TryActivateAbility()
。
我建议在您的自定义 UGameplayAbility
类中添加一个 bool
,指定是否在授予时激活 GameplayAbility
。示例项目为其被动护甲堆叠能力执行了此操作。
被动 GameplayAbilities
通常具有 Server Only
的 Net Execution Policy
。
void UGDGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec)
{
Super::OnAvatarSet(ActorInfo, Spec);
if (bActivateAbilityOnGranted)
{
ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
}
}
Epic 将此函数描述为启动被动能力和执行 BeginPlay
类型操作的正确位置。
4.6.4.2 激活失败标签
能力具有默认逻辑来告诉您为什么能力激活失败。要启用此功能,您必须设置与默认失败情况相对应的 GameplayTags。
将这些标签(或您自己的命名约定)添加到您的项目中:
+GameplayTagList=(Tag="Activation.Fail.BlockedByTags",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.CantAffordCost",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.IsDead",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.MissingTags",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.Networking",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.OnCooldown",DevComment="")
然后将它们添加到 GASDocumentation\Config\DefaultGame.ini
:
[/Script/GameplayAbilities.AbilitySystemGlobals]
ActivateFailIsDeadName=Activation.Fail.IsDead
ActivateFailCooldownName=Activation.Fail.OnCooldown
ActivateFailCostName=Activation.Fail.CantAffordCost
ActivateFailTagsBlockedName=Activation.Fail.BlockedByTags
ActivateFailTagsMissingName=Activation.Fail.MissingTags
ActivateFailNetworkingName=Activation.Fail.Networking
现在,每当能力激活失败时,相应的 GameplayTag 将包含在输出日志消息中或在 showdebug AbilitySystem
HUD 上可见。
LogAbilitySystem: Display: InternalServerTryActivateAbility. Rejecting ClientActivation of Default__GA_FireGun_C. InternalTryActivateAbility failed: Activation.Fail.BlockedByTags
LogAbilitySystem: Display: ClientActivateAbilityFailed_Implementation. PredictionKey :109 Ability: Default__GA_FireGun_C
4.6.5 取消Abilities
要从内部取消 GameplayAbility
,请调用 CancelAbility()
。这将调用 EndAbility()
并将其 WasCancelled
参数设置为 true。
要从外部取消 GameplayAbility
,ASC
提供了一些函数:
/** 取消指定的能力 CDO。 */
void CancelAbility(UGameplayAbility* Ability);
/** 取消由传入的 spec 句柄指示的能力。如果在重新激活的能力中找不到句柄,则不会发生任何事情。 */
void CancelAbilityHandle(const FGameplayAbilitySpecHandle& AbilityHandle);
/** 取消具有指定标签的所有能力。不会取消忽略实例 */
void CancelAbilities(const FGameplayTagContainer* WithTags=nullptr, const FGameplayTagContainer* WithoutTags=nullptr, UGameplayAbility* Ignore=nullptr);
/** 取消所有能力,无论标签如何。不会取消忽略实例 */
void CancelAllAbilities(UGameplayAbility* Ignore=nullptr);
/** 取消所有能力并销毁任何剩余的实例化能力 */
virtual void DestroyActiveState();
注意: 我发现 CancelAllAbilities
似乎在您有 Non-Instanced
GameplayAbilities
时无法正常工作。它似乎会命中 Non-Instanced
GameplayAbility
并放弃。CancelAbilities
可以更好地处理 Non-Instanced
GameplayAbilities
,这也是示例项目使用的(跳跃是一个非实例化的 GameplayAbility
)。您的体验可能会有所不同。
4.6.6 获取激活的 Abilities
初学者经常问“我如何获取活动Ability?”可能是为了在其上设置变量或取消它。多个 GameplayAbility
可以同时处于活动状态,因此没有一个“活动能力”。相反,您必须搜索 ASC
的 ActivatableAbilities
列表(授予的 GameplayAbilities
,ASC
拥有的)并找到与您正在寻找的 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 ) | 描述 | 使用示例 |
---|---|---|
按Actor实例化(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 | GameplayTags 是 GameplayAbility 拥有的。这些只是描述 GameplayAbility 的 GameplayTags 。 |
Cancel Abilities with Tag | 其他在其 Ability Tags 中具有这些 GameplayTags 的 GameplayAbilities 在此 GameplayAbility 激活时将被取消。 |
Block Abilities with Tag | 其他在其 Ability Tags 中具有这些 GameplayTags 的 GameplayAbilities 在此 GameplayAbility 激活时被阻止。 |
Activation Owned Tags | 这些 GameplayTags 在此 GameplayAbility 激活时赋予 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
类、等级、输入绑定和必须与 GameplayAbility
类分开的运行时状态。
当 GameplayAbility
在服务器上被授予时,服务器将 GameplayAbilitySpec
复制到拥有客户端,以便她可以激活它。
激活 GameplayAbilitySpec
将根据其 Instancing Policy
创建 GameplayAbility
的实例(或不创建实例)。
4.6.11 向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 | 使用 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(Ability消耗与冷却时间)
GameplayAbilities
带有可选成本和冷却时间的功能。成本是 ASC
必须具备的预定义 Attributes
数量,以便激活 GameplayAbility
,通过 Instant
GameplayEffect
实现(Cost GE
)。冷却时间是一个计时器,防止在到期之前重新激活 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
激活后发生变化,导致在提交时无法满足成本。提交成本和冷却时间可以 本地预测,如果 预测键 在提交时有效。
请参阅 CostGE
和 CooldownGE
以获取实现细节。
4.6.13 升级Abilities
有两种常见的Abilities升级方法:
升级方法 | 描述 |
---|---|
取消授予并在新等级重新授予 | 在服务器上从 ASC 中取消授予(移除)GameplayAbility ,并在下一个等级重新授予。这会在激活时终止 GameplayAbility 。 |
增加 GameplayAbilitySpec 的等级 | 在服务器上找到 GameplayAbilitySpec ,增加其等级,并标记为脏,以便复制到拥有客户端。如果在升级时激活,使用此方法不会终止 GameplayAbility 。 |
这两种方法的主要区别在于您是否希望在升级时取消激活的 GameplayAbilities
。根据您的 GameplayAbilities
,您很可能会使用这两种方法。我建议在您的 UGameplayAbility
子类中添加一个 bool
,指定使用哪种方法。
4.6.14 Ability Sets
GameplayAbilitySets
是方便的 UDataAsset
类,用于保存输入绑定和角色的启动 GameplayAbilities
列表,并具有授予 GameplayAbilities
的逻辑。子类还可以包括额外的逻辑或属性。Paragon 为每个英雄都有一个 GameplayAbilitySet
,其中包括他们所有的给定 GameplayAbilities
。
我发现这个类至少在我所见的情况下是不必要的。示例项目在 GDCharacterBase
及其子类中处理了 GameplayAbilitySets
的所有功能。
4.6.15 Ability批处理
传统的 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,而不是两个。此场景也可以通过事件激活能力来实现(/Chapter4/c4_6_Gameplay_Abilities#concepts-ga-data),这会将子弹的 TargetData
与 EventPayload
一起从客户端发送到服务器。后者方法的缺点是 TargetData
必须在能力外部生成,而批处理方法在能力内部生成 TargetData
。
Ability Batching
在 ASC
上默认禁用。要启用 Ability Batching
,重写 ShouldDoServerAbilityRPCBatch()
以返回 true:
virtual bool ShouldDoServerAbilityRPCBatch() const override { return true; }
现在 Ability Batching
已启用,在激活您想要批处理的能力之前,您必须事先创建一个 FScopedServerAbilityRPCBatcher
结构。此特殊结构将在其范围内尝试批处理任何后续的能力。一旦 FScopedServerAbilityRPCBatcher
超出范围,任何激活的能力将不再尝试批处理。FScopedServerAbilityRPCBatcher
通过在每个可以批处理的函数中拦截调用来工作,阻止 RPC 发送,并将消息打包到批处理结构中。当 FScopedServerAbilityRPCBatcher
超出范围时,它会自动在 UAbilitySystemComponent::EndServerAbilityRPCBatch()
中将此批处理结构 RPC 到服务器。服务器在 UAbilitySystemComponent::ServerAbilityRPCBatch_Internal(FServerAbilityRPCBatch& BatchInfo)
中接收批处理 RPC。BatchInfo
参数将包含能力是否应结束的标志,以及在激活时是否按下了输入,以及是否包含 TargetData
。这是一个很好的函数,可以在其中放置断点以确认您的批处理是否正常工作。或者,使用 cvar 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()
调用(全自动),并且 EndAbility()
调用将在稍后某个时间以其自己的 RPC 发生。
GASShooter 提供了一个 Blueprint 节点,允许批处理能力,该节点由上述仅本地的能力用于触发批处理能力。
4.6.16 Net Security Policy(网络安全策略)
GameplayAbility
的 NetSecurityPolicy
决定了能力应该在网络上执行的位置。它提供了对客户端尝试执行受限能力的保护。
NetSecurityPolicy | 描述 |
---|---|
ClientOrServer | 无安全要求。客户端或服务器可以自由触发此Ability的执行和终止。 |
ServerOnlyExecution | 客户端请求执行此Ability将被服务器忽略。客户端仍然可以请求服务器取消或结束此Ability。 |
ServerOnlyTermination | 客户端请求取消或结束此Ability将被服务器忽略。客户端仍然可以请求执行Ability。 |
ServerOnly | 服务器控制此Ability的执行和终止。客户端的任何请求将被忽略。 |