跳至主要內容

c8.1GAS|GameplayAbility实践-翻滚、受击

Mr.Si大约 6 分钟u++

回顾

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

网络权限

头像
服务端具有权威性,所有角色都持有技能
头像
客户端则本地玩家控制的角色中持有

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);
头像
下面是GA的基本流程
  1. 能力激活
  • 激活请求: 玩家或游戏逻辑触发能力的激活,通常通过调用 ActivateAbility 方法。
  • 能力检查: 通过 CommitAbility 方法检查能否激活能力(例如,检查能量、状态等)。
  1. 能力执行
  • 任务创建: 在能力中,可以创建和启动任务(如 AbilityTask),这些任务可以处理异步操作或延时执行。
  • 任务激活: 任务的 Activate 方法被调用,任务开始运行。
  1. 结果处理
  • 结束能力: 一旦完成所有操作,调用 EndAbility 方法结束能力的执行,并处理可能的状态更新。
	/** 
	 * Attempts to activate the given ability, will check costs and requirements before doing so.
	 * Returns true if it thinks it activated, but it may return false positives due to failure later in activation.
	 * If bAllowRemoteActivation is true, it will remotely activate local/server abilities, if false it will only try to locally activate the ability
	 */
	UFUNCTION(BlueprintCallable, Category = "Abilities")
	bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);
头像
GA本身,可以通过持有的ActorInfo来获取外部信息。就像下面这样
头像
如果想把跳跃封装成GA,可以用类似下面的方法。

实践1|闪避

头像
在这之前我们需要了解几个传递数据给GA的方法。

1.AbilityTask_WaitGameplayEvent

头像
GA不仅仅可以通过ASC->TryActivateAbility执行,还可以通过事件触发,

2.SendGameplayEventToActor

头像
也就是说我可以通过这个SendGameplayEventToActor 激活拥有Tag的事件?比如翻滚闪避动作? 外部判断闪避方向传递闪避方向给这个GA?
头像
是的,只不过你说的这种其实完全可以在GA中实现,通过事件传递更适合类似受击技能,通过Hit等伤害事件触发传递的。
头像
为什么?
头像
受击本身就是通过伤害碰撞事件传递的,事件本身是服务器行为,所以受击行为很好做,只需要通过伤害事件配合方向计算就行了。 但闪避则不同,闪避的基本思路是根据是客户端的前一帧输入方向,通过服务器授权播放对应的方向。(本地会预测播放)

方向计算

EDirectionType UAbilityUtility::DeterminePlayerInputDirection(const APawn* Pawn)
{
	if(!Pawn ||!Pawn->InputComponent) return EDirectionType::Invalid;
	
	// 计算相对方向向量
	const FVector InputDirection = Pawn->GetLastMovementInputVector().GetSafeNormal();
	
	if (InputDirection.IsNearlyZero())
	{
		return EDirectionType::Forward; // 默认向前
	}
	float ForwardDot = FVector::DotProduct(InputDirection, Pawn->GetActorForwardVector());
	float RightDot = FVector::DotProduct(InputDirection, Pawn->GetActorRightVector());

	if (ForwardDot > 0.5f)
	{
		return EDirectionType::Forward;
	}
	if (ForwardDot < -0.5f)
	{
		return EDirectionType::Backward;
	}
	if (RightDot > 0.5f)
	{
		return EDirectionType::Right;
	}
	if (RightDot < -0.5f)
	{
		return EDirectionType::Left;
	}
	return EDirectionType::Forward; 
}

扩展- GetLastInputVector 和 GetLastMovementInputVector区别传送门

头像
也就是说,闪避需要RPC提交给服务器执行?本地客户端可以拿到对应的Input方向,但对应在服务器上的角色 好像并不会监听客户端的输入方向。
头像
RPC肯定是要的,咱完全可以封装封装在GA中,利用GAS的框架函数来实现。
头像
可是GA中执行GetLastInputVector服务器端一样拿不到结果啊!GA本身并不是Actor,也没法发起RPC吧?
头像
GA本身确实不可以,但你别忘记GA是由ASC组件维护的,自然可以通过ASC来同步一些变量。并且这种任务多半是异步任务,我们可以封装在AbilityTasks中, 例如:我想同步一个方向枚举给服务器,以便能够正确的播放对应的动画。

AbilityTasks

UAbilityTask_CalculateInputDirection.cpp
#include "AbilitySystem/AbilityTasks/AbilityTask_CalculateInputDirection.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystem/AbilityLib/EDirectionType.h"
#include "AbilitySystem/AbilityLib/AbilityUtility.h"

UAbilityTask_CalculateInputDirection* UAbilityTask_CalculateInputDirection::CreateCalculateInputDirectionTask(UGameplayAbility* OwningAbility)
{
    UAbilityTask_CalculateInputDirection* MyTask = NewAbilityTask<UAbilityTask_CalculateInputDirection>(OwningAbility);
    return MyTask;
}

void UAbilityTask_CalculateInputDirection::Activate()
{
    Super::Activate();
    const bool bIsLocallyControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled();
    
    // 如果是本地控制,则发送预测请求
    if (bIsLocallyControlled)
    {
        SendActionDirectionToServer();
    }
    else
    {
        const FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle();
        const FPredictionKey ActivationPredictionKey = GetActivationPredictionKey();
        AbilitySystemComponent.Get()->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey).AddUObject(this, &UAbilityTask_CalculateInputDirection::OnActionDirectionReplicated);
        const bool bCalledDelegate = AbilitySystemComponent.Get()->CallReplicatedTargetDataDelegatesIfSet(SpecHandle, ActivationPredictionKey);
        if (!bCalledDelegate)
        {
            SetWaitingOnRemotePlayerData();
        }
    }
}

void UAbilityTask_CalculateInputDirection::SendActionDirectionToServer()
{
    // 使用预测窗口来封装客户端到服务器的预测机制
    FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent.Get());

    const APawn* Character = Cast<APawn>(Ability->GetCurrentActorInfo()->AvatarActor.Get());
    if (!Character)
    {
        EndTask(); // 如果角色无效,结束任务
        return;
    }
    
    // 根据输入方向判断动作类型
    const EDirectionType ActionDirection = UAbilityUtility::DeterminePlayerInputDirection(Character);
	
    // 封装方向枚举到一个简单的数据结构中进行传输
    FGameplayAbilityTargetDataHandle DataHandle;
    FGameplayAbilityTargetData_SimpleEnum* Data = new FGameplayAbilityTargetData_SimpleEnum();
    Data->EnumValue = ActionDirection;
    DataHandle.Add(Data);

    // 发送数据到服务器
    AbilitySystemComponent->ServerSetReplicatedTargetData(
        GetAbilitySpecHandle(),
        GetActivationPredictionKey(),
        DataHandle,
        FGameplayTag(),
        AbilitySystemComponent->ScopedPredictionKey);
    
    if (ShouldBroadcastAbilityTaskDelegates())
    {
        ValidDirection.Broadcast(ActionDirection);
    }
}


void UAbilityTask_CalculateInputDirection::OnActionDirectionReplicated(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ActivationTag)
{
    // 提取枚举类型的方向
  AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey());
  	if (ShouldBroadcastAbilityTaskDelegates())
  	{
  	    for(auto Data : DataHandle.Data)
  	    {
  	        if (Data)
  	        {
  	        	GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("OnActionDirectionReplicated"));
  	        	FGameplayAbilityTargetData_SimpleEnum* HitResultPtr = static_cast <FGameplayAbilityTargetData_SimpleEnum*>(Data.Get());
  	            if (HitResultPtr)
  	            {
  	            	ValidDirection.Broadcast(HitResultPtr->EnumValue);
  	            }
  	        }
  	    }
  	}
}

实例化策略

头像
不同的实例化策略,对数据的保存能力也不同
实例化类型描述优点适用场景
按执行实例化每次技能运行时生成新对象副本。可自由使用蓝图和成员变量,所有内容在执行前初始化。不频繁运行的技能,例如MOBA中的"终极技能"。
按Actor实例化每个Actor在首次执行时生成技能实例,以后复用。减少新对象创建的开销,适合大规模使用技能。大型战斗中频繁使用的技能,例如小兵的基本攻击。
非实例化使用类默认对象,不生成任何新实例。效率最高,无需创建对象,节省资源。高频率执行且不需内部变量的技能,例如RTS中的基本攻击。