跳至主要內容

c8.2GAS|GameplayAbility|Ability

Mr.Si大约 6 分钟u++

头像
GameplayAbility 技能类,是对技能的抽象,后文用GA简称,ASC则是 AbilitySystemComponent 的简写

Ability

头像
怎么给我的角色设定技能呢?

GiveAbility

头像
GA本身受ASC组件维护,所以可以通过ASC本身授权、激活,激活操作则在服务器上执行。
	UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Gameplay Abilities", meta = (DisplayName = "Give Ability", ScriptName = "GiveAbility"))
	FGameplayAbilitySpecHandle K2_GiveAbility(TSubclassOf<UGameplayAbility> AbilityClass, int32 Level = 0, int32 InputID = -1);

TryActivateAbility

TryActivateAbility(GameplayAbilitySpec.Handle, bAllowRemoteActivation)
头像

激活一个 GA,并根据 网络角色能力的 NetExecutionPolicy 决定执行位置、是否预测、本地是否运行等。


InternalServerTryActivateAbility

头像

也就说无论是客户端还是服务器最终都会执行一次InternalServerTryActivateAbility

头像

没错,主要是用来检测一些触发条件、响应不同的网络策略。

void UAbilitySystemComponent::ServerTryActivateAbility_Implementation(FGameplayAbilitySpecHandle Handle, bool InputPressed, FPredictionKey PredictionKey)
{
	InternalServerTryActivateAbility(Handle, InputPressed, PredictionKey, nullptr);
}

网络权限

头像

也就说开启bAllowRemoteActivation 默认本地也会执行一次?

头像
是的,如果你关闭了就只会发起RPC,从Sever端执行。
头像
那岂不是更好?
头像
服务端具有权威性,所有角色都持有技能
头像
客户端则本地玩家控制的角色中持有
头像

虽然Sever是权威。但是,实际上我们不得不考虑网络延迟的影响,因此开启 bAllowRemoteActivation 主要目的还是为了能够使用客户端预测的功能。

可以看到开启了bAllowRemoteActivation后客户端上也会执行一次,只不过要注意客户端上没有权限。

头像
这也是我们实现网络预测同步的关键。

没有开启网络预测,会出现网络矫正带来的回滚卡顿

GIF

开启网络预测后,极大的优化了回滚

GIF

NetExecutionPolicy

NetExecutionPolicy本地调用位置远程调用处理逻辑
LocalOnly客户端不允许远程激活
LocalPredicted客户端允许客户端预测 → 发起激活 RPC
ServerOnly服务器客户端请求服务器执行 → CallServerTry...
ServerInitiated服务器客户端无法主动触发,必须由服务器调用
头像

除了外部控制要不要由服务器执行,同时GA内部也可以设置策略,默认用到是LocalPredicted

AbilityInstancingPolicy|实例化策略

头像

InstancedPerActor 会尝试复用某个创建好的Handle从而提高性能。不同的实例化策略,对数据的保存能力也不同

枚举值含义与用途备注
NonInstanced能力不创建实例。所有调用共享同一个 Ability 类默认对象(CDO)高性能、无状态的能力(如纯粹触发效果);不能调用 EndAbility,不能存储状态
InstancedPerActor每个拥有该能力的 Actor 创建一个实例,激活时复用该实例支持存储状态、调用 EndAbility、可以被多个激活流程控制;最常用的策略
InstancedPerExecution每次激活都会创建一个新的 Ability 实例用于需要并发多个激活、每次激活有独立状态的能力(如持续型技能、DOT)
头像
不是很懂啊!有什么区别?
头像
你用一个Flip节点就能看出猫腻了。

InstancedPerExecution

头像
第二次按下后并没有执行Flip ,并没有再次执行,同时这种模式也可以不加EndAbility,因为没错都会重新创建实例激活。

InstancedPerActor

头像
每个拥有该能力的 Actor 创建一个实例,激活时复用该实例 。

CallActivateAbility

头像
等该做的事都做完以后,包括网络预测键的检测等行为,才会正式开始激活我们的能力。这也是我们激活能力的入口函数之一。后面就是我们比较熟悉的激活流程了
void UGameplayAbility::CallActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
{
	PreActivate(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData);
	ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
}
void UGameplayAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	if (TriggerEventData && bHasBlueprintActivateFromEvent)
	{
		// A Blueprinted ActivateAbility function must call CommitAbility somewhere in its execution chain.
		K2_ActivateAbilityFromEvent(*TriggerEventData);
	}
	else if (bHasBlueprintActivate)
	{
		// A Blueprinted ActivateAbility function must call CommitAbility somewhere in its execution chain.
		K2_ActivateAbility();
	}
	else if (bHasBlueprintActivateFromEvent)
	{
		UE_LOG(LogAbilitySystem, Warning, TEXT("Ability %s expects event data but none is being supplied. Use 'Activate Ability' instead of 'Activate Ability From Event' in the Blueprint."), *GetName());
		constexpr bool bReplicateEndAbility = false;
		constexpr bool bWasCancelled = true;
		EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
	}
	else
	{
		// Native child classes should override ActivateAbility and call CommitAbility.
		// CommitAbility is used to do one last check for spending resources.
		// Previous versions of this function called CommitAbility but that prevents the callers
		// from knowing the result. Your override should call it and check the result.
		// Here is some starter code:
		
		//	if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
		//	{			
		//		constexpr bool bReplicateEndAbility = true;
		//		constexpr bool bWasCancelled = true;
		//		EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
		//	}
	}
}

CommitAbility

头像

但是我有个疑问啊!CommitAbility什么时候执行的?

头像

注释已经写的非常明白了,CommitAbility要在K2_ActivateAbilityFromEvent后执行,当然C++中习惯在重载ActivateAbility中执行

bool UGameplayAbility::CommitAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags)
{
	// Last chance to fail (maybe we no longer have resources to commit since we after we started this ability activation)
	if (!CommitCheck(Handle, ActorInfo, ActivationInfo, OptionalRelevantTags))
	{
		return false;
	}

	CommitExecute(Handle, ActorInfo, ActivationInfo);

	// Fixme: Should we always call this or only if it is implemented? A noop may not hurt but could be bad for perf (storing a HasBlueprintCommit per instance isn't good either)
	K2_CommitExecute();

	// Broadcast this commitment
	ActorInfo->AbilitySystemComponent->NotifyAbilityCommit(this);

	return true;
}

CommitAbility可选变体

函数名功能说明默认行为
CommitAbility()尝试提交能力,消耗 GE 资源(如消耗蓝量、加 CD)并应用冷却与花费会同时尝试调用 CommitAbilityCost()CommitAbilityCooldown()
CommitAbilityCooldown()单独提交冷却效果(Cooldown GE),不处理消耗资源(如 Mana)只提交冷却,不提交消耗
CommitAbilityCost()单独提交资源消耗(Cost GE),不处理冷却只提交资源消耗,不触发冷却

头像

注意,如果你做技能冷却类的效果,需要手动提交一次CommitAbilityCooldown()

头像
也就是说这里会主动触发一次我们给定的GE对吧。
头像
是的,这也是我们实现技能CD、消耗的关键。提到GE不得不提一句,UE5.3后进行了大幅改版。从原来的Config配置模式变成更解决策略模式。

上下文

头像
GA本身,可以通过持有的ActorInfo来获取外部信息。就像下面这样

EndAbility

头像

最后需要通过EndAbility结束掉本次GA