4.6 游戏技能 - Gameplay Abilities

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流程图:
Simple GameplayAbility Flowchart

稍微复杂一些的GameplayAbility流程图:
Complex GameplayAbility Flowchart

复杂的技能也可以使用多个互相之间交互(激活、取消等)的GameplayAbilities来实现。

4.6.1.1 复制策略 - Replication Policy

不要使用这个选项。本身这个名字存在一定的误导性,你要知道其实你并不需要关心这个。默认情况下GameplayAbilitySpecs就会被从服务端复制到所属客户端。上面也提到过,GameplayAbilities不会在模拟代理上运行。他们使用AbilityTasksGameplayCues来复制或者远程过程调用可视化的变化到模拟代理。Epic的Dave Ratti也表明他希望能够在未来删除这个选项.

4.6.1.2 服务器端远程技能取消 - Server Respects Remote Ability Cancellation

这个选项常常会引发一些麻烦。即,如果客户端的GameplayAbility因为取消或者自然完成而结束,它会强制服务器也去结束(无论在服务器是否也完成)。这个问题很重要,特别是针对使用本地预测的GameplayAbilities高延迟的玩家来说。通常你最好禁用这个选项。

4.6.1.3 直接对输入的复制 - Replicate Input Directly

启用这个选项将会一直把输入的按下和释放事件复制给服务器。Epic并不建议这样使用,取而代之的,最好使用内置到已存在的输入相关的AbilityTasksGeneric 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还可以接受通用的ConfirmCancel输入。这些特殊的输入由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被复制到客户端时,ActorInputComponent可能会为空(比如说PlayerController通知客户端调用ClientRestart(),这一步会进行InputComponent的创建,而这一步可能是晚于OnRep_PlayerState(),那么此时就没有InputComponent可以用来去绑定了)。实例项目演示了如何在两处进行绑定,并且通过一个布尔字段进行控制,从而令绑定操作实际上只执行一次。

注意: 示例项目中枚举的ConfirmCancel并没有和项目配置里的输入动作的名称匹配(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,是将其添加到ASCActivatableAbilities列表之中,并在满足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允许你传入一定量的数据。

GameplayAbilityTriggers可以允许你在添加或者移除GameplayTag时对GameplayAbility进行激活。

注意: 当在蓝图中通过事件激活某个GameplayAbility,你必须使用ActivateAbilityFromEvent节点,同时标准ActivateAbility节点必须不能在你的蓝图中出现。如果ActivateAbility节点存在的话,则会忽略ActivateAbilityFromEvent节点。

注意:GameplayAbility应该结束时不要忘记调用EndAbility(),除非该GameplayAbility是作为被动技能存在。

本地预测GameplayAbilities的激活步骤:

  1. 所属客户端 调用TryActivateAbility()
  2. 调用InternalTryActivateAbility()
  3. 调用CanActivateAbility(),返回值是去检查GameplayTag的要求是否满足,ASC是否能够承受消耗,GameplayAbility是否出于冷却状态,以及是否当前有其他实例出于激活状态
  4. 调用CallServerTryActivateAbility(),并且传递生成好的Prediction Key
  5. 调用CallActivateAbility()
  6. 调用PreActivate(),Epic将这个称为例行公事
  7. 调用ActivateAbility(),即最终激活这个技能

服务器接收CallServerTryActivateAbility()

  1. 调用ServerTryActivateAbility()
  2. 调用InternalServerTryActivateAbility()
  3. 调用InternalTryActivateAbility()
  4. 调用CanActivateAbility(),并且返回:GameplayTag的要求是否得到满足,ASC是否能够承受消耗,GameplayAbility是否出于冷却状态,以及是否当前有其他实例出于激活状态
  5. 调用ClientActivateAbilitySucceed(),如果成功的话,更新其ActivationInfo,表明其激活行为已被服务器确认,并且广播OnConfirmDelegate委托。这和输入的确认是不同的两回事。
  6. 调用CallActivateAbility()
  7. 调用PreActivate(),Epic将这个称为例行公事
  8. 调用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-InstancedGameplayAbilities时,CancelAllAbilities似乎并无法正常起作用。它似乎是在遇到Non-InstancedGameplayAbility时会取消掉。CancelAbilities在处理Non-InstancedGameplayAbilities时候表现更加良好,示例项目中也是用的这种处理方式(跳跃就是用的Non-InstancedGameplayAbilities来做的)。当然这方面你的做法可以是不同的。

4.6.6 获取处于激活状态的技能 - Getting Active Abilities

新手经常会提一些类似“我怎么获取到激活的技能”这样类似的问题,希望可能去操作其上的变量或者是去取消掉这个技能。某一个事件点上可以同时有多个GameplayAbility处于激活状态,所以并不会有某个所谓的“active ability”让你去获取。取而代之的,你必须在ASC的名为ActivatableAbilities的列表(ASC上存储赋予的GameplayAbilities的位置)中去查询,去尝试寻找匹配你所寻找的Asset或者GrantedGameplayTag的技能。

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

GameplayAbilityInstancing Policy决定了当GameplayAbility激活时是否去进行实例化以及如何实例化的问题。

Instancing Policy描述何时使用举例
Instanced Per Actor每个ASC只有一个GameplayAbility的实例,在技能的重复激活时进行复用。这可能是你使用的最多的Instancing Policy。你可以在任何技能上使用它,并且在技能的重复激活期间提供持久性。设计者需要负责在激活时去手动重置需要的变量。
Instanced Per Execution每次激活一个GameplayAbility,就会去创建一个新的GameplayAbility的实例。这种做法的好处是你每次激活GameplayAbilities时都会去重置所有变量(不需要手动)。相对于上面的Instanced Per Actor这种做法开销非常大,因为每当激活一个新的GameplayAbilities都需要进行一次实例化。示例项目种并没有使用这个。
Non-InstancedGameplayAbility在它的ClassDefaultObject上进行操作。不会去创建任何的实例。在三个做法之中这种做法开销最小,但是其使用起来颇为严苛,且限制颇多。Non-InstancedGameplayAbilities不能存储状态,意味着不能有动态变量且不能绑定AbilityTask的委托。这个的最佳用途是频繁使用的简单技能,例如MOBA或者RTS游戏种小兵的普通攻击。示例项目中跳跃的GameplayAbility就是Non-Instanced

4.6.8 网络执行策略 - Net Execution Policy

GameplayAbilityNet Execution Policy决定了谁来运行GameplayAbility以及以什么样的顺序来运行。

Net Execution Policy描述
Local OnlyGameplayAbility只运行在所属客户端上。这个对于那些只会有本地的视觉等装饰性的变化的技能来说是非常好用的。单人游戏应该使用Server Only
Local PredictedLocal PredictedGameplayAbilities首先在所属客户端上激活,然后才是服务器。服务器那边将会修正客户端预测的不正确的部分。参考Prediction
Server OnlyGameplayAbility只在服务器上运行。被动的GameplayAbilities通常是Server Only。。
Server InitiatedServer InitiatedGameplayAbilities首先在服务器上进行激活,然后才是所属客户端。我个人不太使用这个。。

4.6.9 技能标签 - Ability Tags

GameplayAbilities附带着GameplayTagContainers,其又有内置的逻辑。这些GameplayTags都是不去复制的。

GameplayTag Container描述
Ability TagsGameplayAbility所拥有的GameplayTags。这些只是用来描述GameplayAbilityGameplayTags
Cancel Abilities with Tag当这个GameplayAbility激活时,如果还有其他GameplayAbilitiesAbility Tags也有这种GameplayTags的话,那么这些其他的技能就会被取消掉。
Block Abilities with Tag当这个GameplayAbility激活时,如果还有其他GameplayAbilitiesAbility Tags也有这种GameplayTags的话,那么就会阻止其他的这些技能的激活。
Activation Owned TagsGameplayAbility激活时,这些GameplayTags会被给到GameplayAbility的所有者。再次强调这些不会被进行复制。
Activation Required Tags仅当所有者拥有所有这些GameplayTags时,GameplayAbility才能够被激活。
Activation Blocked Tags如果所有者有这些GameplayTags中的任意一些,那么GameplayAbility就不能够被激活。
Source Required Tags仅当Source拥有所有这些GameplayTags时,这个GameplayAbility才能够被激活。SourceGameplayTags仅在 事件触发GameplayAbility时进行设置。
Source Blocked Tags如果Source拥有这些GameplayTags中的任意一些,那么GameplayAbility就不能够被激活。SourceGameplayTags仅在 事件触发GameplayAbility时进行设置。
Target Required Tags仅当Target拥有所有这些GameplayTags时,这个GameplayAbility才能够被激活。TargetGameplayTags仅在 事件触发GameplayAbility时进行设置。
Target Blocked Tags如果Target拥有这些GameplayTags中的任意一些,那么GameplayAbility就不能够被激活。TargetGameplayTags仅在 事件触发GameplayAbility时进行设置。

4.6.10 Gameplay Ability Spec

在技能被赋予后,GameplayAbilitySpec就会存在于ASC,其定义了处于可被激活状态的GameplayAbility —— 根据GameplayAbility类,等级,输入绑定,以及运行时状态。

GameplayAbility在服务区上被赋予之后,服务器会复制GameplayAbilitySpec到所属客户端,然后才能够被进一步激活。

激活一个GameplayAbilitySpec将会依照其Instancing Policy创建一个GameplayAbility的实例(如果是Non-InstancedGameplayAbilities则并不会创建相应的实例)。

4.6.11 传递数据到技能 - Passing Data to Abilities

GameplayAbilities的一般的使用流程是Activate->Generate Data->Apply->End。有些时候你需要在已有数据上做一些操作。GAS为将外部数据传入到GameplayAbilities内部这样的操作提供了一些可选项:

方法描述
通过事件激活GameplayAbility可以在通过事件对GameplayAbility进行激活时包含一定量的数据。对于本地预测的GameplayAbilities,事件的数据们将会从客户端复制到服务端。如果有些数据无法利用已存在的变量去处理,这时可以使用Optional Object或者TargetData两种变量。这样做的不便之处是就无法利用输入绑定来激活技能了。要通过事件激活GameplayAbilityGameplayAbility本身必须配置好Triggers,指定GameplayTag并且选定GameplayEvent的选项。要发送事件,可以使用函数UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload)
使用WaitGameplayEventAbilityTaskGameplayAbility激活之后,使用WaitGameplayEvent这个AbilityTask去通知GameplayAbility监听带有数据的事件。这个事件和发送过程和通过事件去激活GameplayAbilities是一样的。这样做的不便之处是事件并不是由AbilityTask来进行复制,只能用于Local OnlyServer OnlyGameplayAbilities。你可以编写自己的AbilityTask,来支持复制带数据的事件。
使用TargetData使用一个自定义的TargetData结构是一个在客户端和服务端之间传递数据的好办法。
将数据存储到OwnerActor或是AvatarActor使用那些存储在OwnerActorAvatarActor或者任何其他你可以引用得到的对象的复制变量。这个方法是最灵活,且能够支持通过输入绑定来激活GameplayAbilities。但是,这个方式并不能保证在使用时复制来的数据一定的同步的。 你必须保证提前性——即如果你设置一个复制变量,然后立即激活GameplayAbility,那么由于可能的潜在的丢包问题就无法保证接收者上面的顺序。

4.6.12 技能的消耗和冷却 - Ability Cost and Cooldown

GameplayAbilities会带有可选的消耗和冷却的功能。技能消耗是为了激活由Instant类型的 GameplayEffect Cost GE)实现的GameplayAbility,所预定义的所需某些Attributes的数量。技能冷却则是为了控制GameplayAbility的重新激活所设定的计时器,其实现是通过一个Duration类型的GameplayEffectCooldown 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去根据自身信息检查是否能够激活的最后一道保险。所属的ASCAttributes可能在GameplayAbility激活之后就会发生变化,从而在技能提交时无法满足消耗。技能和冷却的提交可以是locally predicted,前提是prediction key在提交时是合法的。

参阅CostGECooldownGE获取更多实现细节。

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,即:

  1. CallServerTryActivateAbility()
  2. ServerSetReplicatedTargetData()(不是必须的)
  3. 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项目中暴露了一个蓝图节点,用以在本地执行的技能中来出发技能批处理。

Activate Batched Ability

4.6.16 网络安全策略 - Net Security Policy

GameplayAbilityNetSecurityPolicy决定了技能具体是在网络上的哪部分去执行。这可以防止客户端去尝试执行受限的技能。

NetSecurityPolicy描述
ClientOrServer没有安全要求。客户端和服务端可以自由得执行和结束技能。
ServerOnlyExecution服务端会忽略客户端发起的技能执行的请求。客户端仍然可以发起请求,令服务端取消或者结束这个技能。
ServerOnlyTermination服务端会忽略客户端发起的技能的取消和结束请求。客户端仍然可以发起技能执行的请求。
ServerOnly服务端控制技能的执行和结束。发起请求的客户端会被忽略。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值