UE5 GAS GameAbility源码解析 ActivateAbility

文章目录


一、ActivateAbility、PreActivate、CallActivateAbility

cpp 复制代码
// 游戏能力激活的主要函数
void UGameplayAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    // 检查是否有触发事件数据且该能力有基于事件的蓝图激活函数
    if (TriggerEventData && bHasBlueprintActivateFromEvent)
    {
        // 这是一个通过事件触发的蓝图能力
        // 注意:蓝图的ActivateAbility函数必须在执行链中的某处调用CommitAbility
        K2_ActivateAbilityFromEvent(*TriggerEventData);  // 调用蓝图的基于事件的激活函数
    }
    // 如果没有触发事件但有普通的蓝图激活函数
    else if (bHasBlueprintActivate)
    {
        // 这是一个标准的蓝图能力
        // 注意:蓝图的ActivateAbility函数必须在执行链中的某处调用CommitAbility
        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);
    }
    // 对于C++原生能力
    else
    {
        // 原生子类应该重写ActivateAbility并调用CommitAbility
        // CommitAbility用于最后检查资源消耗(如魔法值、冷却时间)
        // 旧版本的这个函数会自动调用CommitAbility,但这会阻止调用者知道结果
        // 你的重写应该调用它并检查结果
        
        // 这里提供了一些示例代码:
        //	
        //	// 尝试提交能力(检查资源是否足够)
        //	if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
        //	{			
        //		// 如果提交失败,结束能力
        //		constexpr bool bReplicateEndAbility = true;
        //		constexpr bool bWasCancelled = true;
        //		EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
        //	}
        // 注意:实际的原生能力需要在这里添加具体的激活逻辑
    }
}

// 能力预激活函数,在真正激活前进行准备工作
void UGameplayAbility::PreActivate(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
{
    // 获取能力系统组件
    UAbilitySystemComponent* Comp = ActorInfo->AbilitySystemComponent.Get();

    // 在激活能力前刷新任何剩余的服务器移动
    // 刷新服务器移动可以防止之前挂起的移动的DeltaTime被计入即将播放和更新的蒙太奇中
    // 当这种情况发生时,客户端会有比服务器更小的delta time,导致服务器提前接收到通知等
    // 系统依赖于服务器不会提前,因此在这里发送任何之前挂起的服务器移动很重要
    AActor* const MyActor = ActorInfo->AvatarActor.Get();  // 获取角色Actor
    if (MyActor && !ActorInfo->IsNetAuthority())  // 如果是客户端且角色有效
    {
        ACharacter* MyCharacter = Cast<ACharacter>(MyActor);  // 尝试转换为角色
        if (MyCharacter)  // 如果确实是角色
        {
            UCharacterMovementComponent* CharMoveComp = Cast<UCharacterMovementComponent>(MyCharacter->GetMovementComponent());  // 获取移动组件

            if (CharMoveComp)  // 如果移动组件有效
            {
                CharMoveComp->FlushServerMoves();  // 刷新服务器移动
            }
        }
    }

    // 如果能力的实例化策略不是非实例化的,初始化能力状态
    if (GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
    {
        bIsActive = true;                    // 标记能力为活跃状态
        bIsBlockingOtherAbilities = true;    // 默认阻塞其他能力
        bIsCancelable = true;                // 默认能力可被取消
    }

    RemoteInstanceEnded = false;  // 重置远程实例结束标志

    // 必须在开始应用标签和阻塞或取消其他能力之前调用此函数
    // 我们可能会触发一个链式反应,导致调用此能力上的函数,这些函数依赖于当前信息已被设置
    SetCurrentInfo(Handle, ActorInfo, ActivationInfo);  // 设置当前能力信息

    // 如果存在触发事件数据且能力已实例化,保存事件数据
    if (TriggerEventData && IsInstantiated())
    {
        CurrentEventData = *TriggerEventData;  // 复制事件数据
    }

    // 通知能力系统组件此能力现在可以被取消
    Comp->HandleChangeAbilityCanBeCanceled(GetAssetTags(), this, true);

    // 添加能力拥有的标签到能力系统组件
    Comp->AddLooseGameplayTags(ActivationOwnedTags);

    // 检查全局设置是否需要复制激活拥有的标签
    if (UAbilitySystemGlobals::Get().ShouldReplicateActivationOwnedTags())
    {
        // 如果是本地预测或服务器发起的能力
        if (GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted || GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerInitiated)
        {
            // 如果此能力也在客户端执行,则不要将标签通信到客户端(它已经在上面使用了AddLooseGameplayTags)
            Comp->AddMinimalReplicationGameplayTags(ActivationOwnedTags);  // 添加最小复制标签
        }
        else
        {
            // 其他情况,添加完整的复制标签
            Comp->AddReplicatedLooseGameplayTags(ActivationOwnedTags);
        }
    }

    // 如果提供了能力结束委托,添加到结束委托列表
    if (OnGameplayAbilityEndedDelegate)
    {
        OnGameplayAbilityEnded.Add(*OnGameplayAbilityEndedDelegate);  // 绑定结束回调
    }

    // 通知能力系统组件能力已被激活
    Comp->NotifyAbilityActivated(Handle, this);

    // 应用能力的阻塞和取消标签
    Comp->ApplyAbilityBlockAndCancelTags(GetAssetTags(), this, true, BlockAbilitiesWithTag, true, CancelAbilitiesWithTag);

    // 在应用阻塞或取消标签后,必须增加Spec的活动计数,否则能力有可能在完全激活之前意外取消自身
    FGameplayAbilitySpec* Spec = Comp->FindAbilitySpecFromHandle(Handle);  // 查找能力规格
    if (!Spec)  // 如果找不到对应的能力规格
    {
        // 记录警告日志
        ABILITY_LOG(Warning, TEXT("PreActivate called with a valid handle but no matching ability spec was found. Handle: %s ASC: %s. AvatarActor: %s"), *Handle.ToString(), *(Comp->GetPathName()), *GetNameSafe(Comp->GetAvatarActor_Direct()));
        return;  // 提前返回
    }

    // 确保如果超过uint8最大值不会发生回滚,如果变量大小改变则需要更新
    if (LIKELY(Spec->ActiveCount < UINT8_MAX))  // 很可能ActiveCount小于最大值
    {
        Spec->ActiveCount++;  // 增加活跃计数
    }
    else  // 如果已经达到最大值
    {
        // 记录警告日志
        ABILITY_LOG(Warning, TEXT("PreActivate %s called when the Spec->ActiveCount (%d) >= UINT8_MAX"), *GetName(), (int32)Spec->ActiveCount)
    }
}

// 完整的激活能力调用流程
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);  // 第二步:实际激活能力
}

1. ActivateAbility 函数

这是能力激活的主要入口点,根据能力类型决定如何激活:

例子:火球术技能

cpp 复制代码
// 情况1:通过事件触发(如被攻击时自动触发反击)
if (TriggerEventData && bHasBlueprintActivateFromEvent)
{
    // 比如:当角色被近战攻击时,自动触发"闪电护盾"能力
    K2_ActivateAbilityFromEvent(*TriggerEventData);
}
// 情况2:玩家主动施放
else if (bHasBlueprintActivate) 
{
    // 比如:玩家按下Q键施放火球术
    K2_ActivateAbility();
}
// 情况3:配置错误处理
else if (bHasBlueprintActivateFromEvent)
{
    // 比如:设计师配置了事件触发但实际用成了主动施放
    UE_LOG(LogAbilitySystem, Warning, TEXT("Ability %s expects event data..."));
    EndAbility(Handle, ActorInfo, ActivationInfo, false, true); // 立即取消能力
}

2. PreActivate 函数

在真正激活前进行准备工作:

网络同步处理

cpp 复制代码
// 对于客户端角色,刷新服务器移动数据
ACharacter* MyCharacter = Cast<ACharacter>(MyActor);
if (MyCharacter && !ActorInfo->IsNetAuthority())
{
    UCharacterMovementComponent* CharMoveComp = Cast<UCharacterMovementComponent>(MyCharacter->GetMovementComponent());
    if (CharMoveComp)
    {
        CharMoveComp->FlushServerMoves(); // 防止时间不同步导致的蒙太奇问题
    }
}

能力状态初始化

cpp 复制代码
if (GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
{
    bIsActive = true;                    // 标记为活跃状态
    bIsBlockingOtherAbilities = true;    // 阻塞其他能力
    bIsCancelable = true;                // 可被取消
}

标签管理例子

cpp 复制代码
// 假设火球术能力有以下标签
ActivationOwnedTags = { "Casting.Fireball", "Cannot.Move", "Channeling" };

// 添加这些标签到能力系统组件
Comp->AddLooseGameplayTags(ActivationOwnedTags);

// 如果是网络预测能力,还需要进行标签复制
if (GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)
{
    Comp->AddMinimalReplicationGameplayTags(ActivationOwnedTags);
}

阻塞和取消其他能力

cpp 复制代码
// 火球术施放时,阻塞所有带有"Spell.Cast"标签的能力
BlockAbilitiesWithTag = { "Spell.Cast" };
// 取消所有带有"Channeling"标签的能力  
CancelAbilitiesWithTag = { "Channeling" };

Comp->ApplyAbilityBlockAndCancelTags(GetAssetTags(), this, true, BlockAbilitiesWithTag, true, CancelAbilitiesWithTag);

3. CallActivateAbility 函数

这是完整的激活调用流程:

cpp 复制代码
void UGameplayAbility::CallActivateAbility(...)
{
    PreActivate(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData);  // 准备工作
    ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);  // 实际激活
}

具体场景示例

场景:玩家施放治疗术

  1. PreActivate阶段
  • 设置 bIsActive = true
  • 添加 "Casting.Heal" 标签
  • 阻塞其他施法类能力
  • 增加ActiveCount计数
  1. ActivateAbility阶段
  • 因为是蓝图能力,调用 K2_ActivateAbility()
  • 蓝图中执行具体的治疗逻辑
  • 必须调用 CommitAbility 来消耗魔法值
  1. 如果提交失败
cpp 复制代码
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
    EndAbility(Handle, ActorInfo, ActivationInfo, true, true); // 取消能力
}

网络同步示例

对于本地预测的火球术:

cpp 复制代码
// 客户端立即施放,服务器验证
if (GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)
{
    // 客户端先执行,标签通过最小复制同步
    Comp->AddMinimalReplicationGameplayTags(ActivationOwnedTags);
}

关键设计要点

  1. 资源提交 :蓝图能力必须在执行链中调用 CommitAbility
  2. 状态管理:通过标签系统管理能力状态和交互
  3. 网络同步:支持服务器权威和本地预测两种模式
  4. 错误处理:对配置错误有完整的警告和取消机制
  5. 生命周期:通过ActiveCount跟踪能力实例的激活状态

这套系统确保了GameplayAbility在各种复杂场景下的稳定运行,包括网络游戏中的预测和回滚机制。

关键流程总结:

  1. CallActivateAbilityPreActivateActivateAbility
  2. PreActivate 负责状态设置、网络同步、标签管理
  3. ActivateAbility 根据能力类型调用具体的激活逻辑
  4. 整个流程确保了能力的正确初始化、网络同步和资源管理

二、ConfirmActivateSucceed

cpp 复制代码
void UGameplayAbility::ConfirmActivateSucceed()
{
    // 检查:确保当前对象不是类的默认对象(CDO),而是实际的实例
    if (HasAnyFlags(RF_ClassDefaultObject) == false)
    {
        // 网络初始化后处理
        PostNetInit();
        
        // 确保当前Actor信息有效(断言检查)
        ensure(CurrentActorInfo);
        
        // 标记激活状态为已确认
        CurrentActivationInfo.SetActivationConfirmed();

        // 广播确认委托,通知所有监听者能力激活已确认
        OnConfirmDelegate.Broadcast(this);
        
        // 清除委托,避免重复调用
        OnConfirmDelegate.Clear();
    }
}

具体应用场景

场景1:本地预测的火球术

cpp 复制代码
// 客户端按下Q键施放火球术(本地预测)
void UFireballAbility::ActivateAbility(...)
{
    if (!CommitAbility(...))  // 本地预测提交资源
    {
        EndAbility(..., true);  // 提交失败,取消能力
        return;
    }
    
    // 在客户端立即开始施法动作
    StartCastAnimation();
    
    // 发送RPC到服务器请求激活
    ServerTryActivateAbility(...);
}

// 服务器验证通过后调用
void UFireballAbility::ServerConfirmAbility()
{
    // 服务器确认激活成功
    ConfirmActivateSucceed();
    
    // 现在可以在服务器上安全地生成火球
    SpawnFireballProjectile();
}

场景2:网络同步的时间线

cpp 复制代码
// 时间线示例:
// 帧0: 客户端按下按钮 → 本地预测激活
// 帧1: 客户端播放动画,消耗资源(预测)
// 帧2: 服务器收到RPC,验证通过
// 帧3: 服务器调用 ConfirmActivateSucceed()
// 帧4: 客户端收到确认,执行确认后的逻辑

逐行详细说明

1. 实例检查

cpp 复制代码
if (HasAnyFlags(RF_ClassDefaultObject) == false)
  • 目的:确保操作的是能力实例,不是类的默认对象(CDO)
  • 例子:火球术能力的实际运行实例,不是蓝图编辑器中的模板

2. 网络初始化

cpp 复制代码
PostNetInit();
  • 目的:完成网络相关的初始化工作
  • 例子:在复制过程中确保所有网络数据正确设置

3. 安全检查

cpp 复制代码
ensure(CurrentActorInfo);
  • 目的:断言检查,确保有有效的Actor信息
  • 如果失败:在开发版本中会触发断言,发布版本中跳过
  • 例子:确保知道是哪个角色在施放能力

4. 标记确认状态

cpp 复制代码
CurrentActivationInfo.SetActivationConfirmed();
  • 作用 :将 CurrentActivationInfo.bServerActivationConfirmed 设置为 true
  • 意义:表示服务器已经确认了这个激活请求
  • 例子
cpp 复制代码
// 在能力逻辑中可以检查
if (CurrentActivationInfo.IsActivationConfirmed())
{
    // 服务器已确认,可以执行敏感操作
    DealDamage();  // 造成伤害
    SpawnProjectile();  // 生成投射物
}

5. 广播确认委托

cpp 复制代码
OnConfirmDelegate.Broadcast(this);

作用:通知所有注册的监听者,能力激活已确认

使用场景:

cpp 复制代码
// 在能力初始化时注册回调
OnConfirmDelegate.AddUObject(this, &UMyAbility::OnServerConfirmed);

void UMyAbility::OnServerConfirmed(UGameplayAbility* Ability)
{
    // 服务器确认后执行的特殊逻辑
    PlayConfirmationSound();
    ShowConfirmationVFX();
}

6. 清除委托

cpp 复制代码
OnConfirmDelegate.Clear();
  • 目的:避免委托被重复调用
  • 设计考虑:确认是一次性事件,不需要保持注册

完整工作流程示例

治疗术能力的网络预测流程

cpp 复制代码
// 1. 客户端激活(预测)
void UHealAbility::ActivateAbility(...)
{
    // 预测性提交资源
    if (!CommitAbilityCost(..., true))  // true表示是预测提交
    {
        return;
    }
    
    // 立即开始本地效果(预测)
    PlayHealAnimation();
    ShowHealVFX();
    
    // 发送到服务器验证
    Server_RequestHealActivation();
}

// 2. 服务器验证
void UHealAbility::Server_RequestHealActivation_Implementation()
{
    // 服务器验证资源是否足够
    if (CheckResourceCost())
    {
        // 服务器确认激活成功
        ConfirmActivateSucceed();
        
        // 执行服务器端逻辑
        ApplyHealToTarget();
    }
    else
    {
        // 验证失败,拒绝激活
        Client_RejectActivation();
    }
}

// 3. 客户端收到确认后的处理
void UHealAbility::OnConfirmDelegateCallback()
{
    // 可以在这里执行只有确认后才做的操作
    PlayHealSuccessSound();
    UpdateUICooldown();
}

设计意义

  1. 网络预测:支持客户端预测,提升响应性
  2. 服务器权威:最终决定权在服务器,防止作弊
  3. 状态同步:确保客户端和服务器状态一致
  4. 错误恢复:预测失败时可以回滚客户端效果

这个机制是UE能力系统实现流畅网络游戏体验的关键组成部分!

三、PostNetInit

cpp 复制代码
void UGameplayAbility::PostNetInit()
{
    /** 
     * 我们是通过网络复制动态生成的 - 需要通过查看Outer来初始化CurrentActorInfo。
     * 如果我们开始让能力存在于不同于玩家AbilitySystemComponents的Outer上,可能需要进一步更新。
     */
    
    // 如果当前Actor信息为空,需要进行初始化
    if (CurrentActorInfo == nullptr)
    {
        // 获取外部对象(期望是一个Actor)
        AActor* OwnerActor = Cast<AActor>(GetOuter());
        if (ensure(OwnerActor))  // 确保OwnerActor有效
        {
            // 从Actor获取能力系统组件
            UAbilitySystemComponent* AbilitySystemComponent = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(OwnerActor);
            if (ensure(AbilitySystemComponent))  // 确保能力系统组件有效
            {
                // 设置当前Actor信息
                CurrentActorInfo = AbilitySystemComponent->AbilityActorInfo.Get();
            }
        }
    }
}

核心问题解决

这个函数主要解决网络复制创建的能力实例缺少CurrentActorInfo的问题。

问题产生场景:

cpp 复制代码
// 服务器端
void APlayerCharacter::ServerActivateAbility()
{
    // 服务器激活能力并设置正确的ActorInfo
    UGameplayAbility* Ability = ActivateAbility(SpecHandle, CorrectActorInfo, ActivationInfo);
    // Ability->CurrentActorInfo 在这里是有效的
}

// 客户端 - 通过网络复制接收到能力实例
void APlayerCharacter::OnRep_Abilities()
{
    // 客户端通过复制接收到能力实例
    // 但此时能力实例的CurrentActorInfo可能是空的!
    // 因为复制系统只复制数据,不自动设置这些引用
}

具体应用示例

示例1:多人游戏中的治疗光环

cpp 复制代码
// 服务器创建一个持续治疗的光环能力
UGameplayAbility* AuraAbility = CreateAuraAbility();

// 当这个能力复制到客户端时:
void UAuraAbility::PostNetInit()
{
    // 检查CurrentActorInfo是否为空
    if (CurrentActorInfo == nullptr)
    {
        // 获取能力的Outer(拥有者Actor)
        // 假设能力的Outer是玩家角色
        AActor* OwnerActor = Cast<AActor>(GetOuter());  // 获取玩家角色
        
        if (ensure(OwnerActor))  // 确保角色有效
        {
            // 从玩家角色获取能力系统组件
            UAbilitySystemComponent* ASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(OwnerActor);
            
            if (ensure(ASC))  // 确保ASC有效
            {
                // 设置当前Actor信息
                CurrentActorInfo = ASC->AbilityActorInfo.Get();
                // 现在能力知道它属于哪个Actor,可以正确运行了
            }
        }
    }
}

示例2:怪物特殊能力复制

cpp 复制代码
// 场景:BOSS怪物有一个特殊的狂暴能力
// 当BOSS进入战斗时,服务器激活这个能力并复制到所有客户端

class UBerserkAbility : public UGameplayAbility 
{
    // 能力逻辑...
};

// 客户端接收到的复制流程:
void UBerserkAbility::PostNetInit()
{
    Super::PostNetInit();
    
    // 此时能力已经正确设置了CurrentActorInfo
    // 可以安全地访问OwnerActor、AvatarActor等信息
    if (CurrentActorInfo && CurrentActorInfo->AvatarActor.IsValid())
    {
        // 现在可以在客户端设置视觉特效
        SpawnBerserkVFX(CurrentActorInfo->AvatarActor.Get());
    }
}

网络复制流程详解

完整复制流程:

cpp 复制代码
// 1. 服务器创建并激活能力
UGameplayAbility* ServerAbility = NewObject<UGameplayAbility>(ActorASC);
ServerAbility->CurrentActorInfo = &ActorASC->AbilityActorInfo; // 服务器正确设置

// 2. 网络复制系统将能力复制到客户端
//    - 复制基本属性数据
//    - 但CurrentActorInfo指针不会被复制(因为它是引用)

// 3. 客户端接收复制,创建能力实例
UGameplayAbility* ClientAbility = ReplicatedCopy;
// ClientAbility->CurrentActorInfo == nullptr ❌

// 4. 客户端调用PostNetInit修复这个问题
ClientAbility->PostNetInit();
// ClientAbility->CurrentActorInfo = ClientActorASC->AbilityActorInfo ✅

关键组件解析

1. GetOuter() - 查找拥有者

cpp 复制代码
AActor* OwnerActor = Cast<AActor>(GetOuter());
  • 作用:在UE的对象系统中,查找这个能力的"外部对象"
  • 典型情况:能力通常以AbilitySystemComponent作为Outer
  • 例子:
cpp 复制代码
// 创建能力时指定Outer
UGameplayAbility* Ability = NewObject<UGameplayAbility>(AbilitySystemComponent);
// GetOuter() 返回 AbilitySystemComponent
// AbilitySystemComponent的Outer通常是Actor

2. 获取能力系统组件

cpp 复制代码
UAbilitySystemComponent* AbilitySystemComponent = 
    UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(OwnerActor);
  • 作用:通过全局函数从Actor获取ASC
  • 好处:统一的获取方式,处理各种边缘情况
  • 等同于:OwnerActor->FindComponentByClass()

设置Actor信息

cpp 复制代码
CurrentActorInfo = AbilitySystemComponent->AbilityActorInfo.Get();
  • 包含的信息
cpp 复制代码
struct FGameplayAbilityActorInfo
{
    TWeakObjectPtr<AActor> OwnerActor;      // 拥有者
    TWeakObjectPtr<AActor> AvatarActor;     // 化身(可能是控制的角色)
    TWeakObjectPtr<APlayerController> PlayerController; // 玩家控制器
    TWeakObjectPtr<UAbilitySystemComponent> AbilitySystemComponent; // ASC
    // ... 其他信息
};

实际应用场景

场景1:客户端能力初始化

cpp 复制代码
void UProjectileAbility::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo)
{
    Super::OnAvatarSet(ActorInfo);
    
    // 如果这是网络复制过来的能力,可能需要等待PostNetInit
    if (CurrentActorInfo == nullptr)
    {
        // 还不能安全使用Actor信息
        return;
    }
    
    // 安全地使用Actor信息
    if (CurrentActorInfo->AvatarActor.IsValid())
    {
        SetupProjectileSpawnPoint(CurrentActorInfo->AvatarActor.Get());
    }
}

场景2:网络游戏中的状态同步

cpp 复制代码
void UGameplayAbility::ExecuteServerRPC()
{
    // 在发送RPC之前确保CurrentActorInfo有效
    if (CurrentActorInfo && CurrentActorInfo->IsLocallyControlled())
    {
        Server_ExecuteAbility(CurrentActorInfo->AvatarActor.Get());
    }
}

设计意义

  1. 网络兼容性:确保通过网络复制的能力能够正常工作
  2. 延迟初始化:允许能力在网络复制完成后才设置关键引用
  3. 错误恢复:自动修复复制过程中丢失的引用信息
  4. 系统健壮性:通过ensure确保关键组件存在

这个机制是UE能力系统实现无缝网络同步的重要保障!

接口

cpp 复制代码
// --------------------------------------
//	ActivateAbility
// --------------------------------------

/**
 * 这是定义能力行为的主要函数
 * - 子类应该重写这个函数
 * - 这个函数图应该调用CommitAbility
 * - 这个函数图应该调用EndAbility
 * 
 * 在这个函数图中可以使用延迟/异步操作。注意Commit和EndAbility的调用要求是针对K2_ActivateAbility图的。
 * 在C++中,K2_ActivateAbility()可能会在CommitAbility或EndAbility被调用之前返回。但这只会在有延迟/异步操作挂起时发生。
 * 当K2_ActivateAbility逻辑上完成时,我们期望Commit/End已经被调用。
 */
UFUNCTION(BlueprintImplementableEvent, Category = Ability, DisplayName = "ActivateAbility", meta=(ScriptName = "ActivateAbility"))
void K2_ActivateAbility();

// 从事件激活能力的蓝图可实现事件
UFUNCTION(BlueprintImplementableEvent, Category = Ability, DisplayName = "ActivateAbilityFromEvent", meta=(ScriptName = "ActivateAbilityFromEvent"))
void K2_ActivateAbilityFromEvent(const FGameplayEventData& EventData);

// 标记是否在蓝图中实现了ActivateAbility
bool bHasBlueprintActivate;
// 标记是否在蓝图中实现了从事件激活的能力
bool bHasBlueprintActivateFromEvent;

/** 实际激活能力,不要直接调用这个函数 */
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, 
                            const FGameplayAbilityActorInfo* ActorInfo, 
                            const FGameplayAbilityActivationInfo ActivationInfo, 
                            const FGameplayEventData* TriggerEventData);

/** 执行初始化工作然后调用ActivateAbility */
virtual void PreActivate(const FGameplayAbilitySpecHandle Handle, 
                        const FGameplayAbilityActorInfo* ActorInfo, 
                        const FGameplayAbilityActivationInfo ActivationInfo, 
                        FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, 
                        const FGameplayEventData* TriggerEventData = nullptr);

/** 执行PreActivate和ActivateAbility */
void CallActivateAbility(const FGameplayAbilitySpecHandle Handle, 
                        const FGameplayAbilityActorInfo* ActorInfo, 
                        const FGameplayAbilityActivationInfo ActivationInfo, 
                        FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate = nullptr, 
                        const FGameplayEventData* TriggerEventData = nullptr);

/** 在预测性能力上,当服务器确认执行成功时调用 */
virtual void ConfirmActivateSucceed();

具体例子说明:

例子1:火球技能

cpp 复制代码
// 在蓝图中实现的K2_ActivateAbility
void UFireballAbility::K2_ActivateAbility()
{
    // 1. 检查资源消耗 (CommitAbility)
    if (!CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo))
    {
        EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
        return;
    }
    
    // 2. 生成火球actor
    AFireballProjectile* Fireball = GetWorld()->SpawnActorDeferred<AFireballProjectile>(
        FireballClass, 
        GetAvatarActorFromCurrentInfo()->GetTransform()
    );
    
    // 3. 设置火球属性
    Fireball->SetDamage(DamageValue);
    Fireball->FinishSpawning(GetAvatarActorFromCurrentInfo()->GetTransform());
    
    // 4. 结束能力
    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, false, false);
}

例子2:从事件触发的治疗技能

cpp 复制代码
// 当收到治疗事件时激活
void UHealAbility::K2_ActivateAbilityFromEvent(const FGameplayEventData& EventData)
{
    // 从事件数据中获取治疗目标
    AActor* Target = EventData.Target;
    
    // 提交能力消耗
    if (CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo))
    {
        // 应用治疗效果
        UGameplayEffect* HealEffect = CreateHealEffect(EventData.EventMagnitude);
        ApplyGameplayEffectToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, 
                                  HealEffect, Target);
    }
    
    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}

执行流程示例:

cpp 复制代码
// 系统调用流程:
// 1. CallActivateAbility被调用
void UGameplayAbility::CallActivateAbility(...)
{
    PreActivate(...);      // 初始化工作
    ActivateAbility(...);  // 调用实际的激活函数
}

// 2. ActivateAbility根据实现类型调用相应的函数
void UGameplayAbility::ActivateAbility(...)
{
    if (bHasBlueprintActivate)
    {
        K2_ActivateAbility();  // 调用蓝图实现
    }
    else if (bHasBlueprintActivateFromEvent && TriggerEventData)
    {
        K2_ActivateAbilityFromEvent(*TriggerEventData);  // 调用事件触发的蓝图实现
    }
    else
    {
        // C++ 实现
    }
}

预测执行例子:

cpp 复制代码
// 对于支持预测的能力(如移动技能)
void UDashAbility::ConfirmActivateSucceed()
{
    // 服务器确认预测执行成功
    // 这里可以执行需要服务器确认后的逻辑
    Super::ConfirmActivateSucceed();
    
    // 给予奖励或执行确认后的效果
    GrantPredictionReward();
}

这是一个来自Unreal Engine Gameplay Ability System (GAS) 的代码片段,我将逐行注释并结合具体例子详细说明:

cpp 复制代码
// --------------------------------------
//	ActivateAbility
// --------------------------------------

/**
 * 这是定义能力行为的主要函数
 * - 子类应该重写这个函数
 * - 这个函数图应该调用CommitAbility
 * - 这个函数图应该调用EndAbility
 * 
 * 在这个函数图中可以使用延迟/异步操作。注意Commit和EndAbility的调用要求是针对K2_ActivateAbility图的。
 * 在C++中,K2_ActivateAbility()可能会在CommitAbility或EndAbility被调用之前返回。但这只会在有延迟/异步操作挂起时发生。
 * 当K2_ActivateAbility逻辑上完成时,我们期望Commit/End已经被调用。
 */
UFUNCTION(BlueprintImplementableEvent, Category = Ability, DisplayName = "ActivateAbility", meta=(ScriptName = "ActivateAbility"))
void K2_ActivateAbility();

// 从事件激活能力的蓝图可实现事件
UFUNCTION(BlueprintImplementableEvent, Category = Ability, DisplayName = "ActivateAbilityFromEvent", meta=(ScriptName = "ActivateAbilityFromEvent"))
void K2_ActivateAbilityFromEvent(const FGameplayEventData& EventData);

// 标记是否在蓝图中实现了ActivateAbility
bool bHasBlueprintActivate;
// 标记是否在蓝图中实现了从事件激活的能力
bool bHasBlueprintActivateFromEvent;

/** 实际激活能力,不要直接调用这个函数 */
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, 
                            const FGameplayAbilityActorInfo* ActorInfo, 
                            const FGameplayAbilityActivationInfo ActivationInfo, 
                            const FGameplayEventData* TriggerEventData);

/** 执行初始化工作然后调用ActivateAbility */
virtual void PreActivate(const FGameplayAbilitySpecHandle Handle, 
                        const FGameplayAbilityActorInfo* ActorInfo, 
                        const FGameplayAbilityActivationInfo ActivationInfo, 
                        FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, 
                        const FGameplayEventData* TriggerEventData = nullptr);

/** 执行PreActivate和ActivateAbility */
void CallActivateAbility(const FGameplayAbilitySpecHandle Handle, 
                        const FGameplayAbilityActorInfo* ActorInfo, 
                        const FGameplayAbilityActivationInfo ActivationInfo, 
                        FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate = nullptr, 
                        const FGameplayEventData* TriggerEventData = nullptr);

/** 在预测性能力上,当服务器确认执行成功时调用 */
virtual void ConfirmActivateSucceed();

具体例子说明:

例子1:火球技能

cpp 复制代码
// 在蓝图中实现的K2_ActivateAbility
void UFireballAbility::K2_ActivateAbility()
{
    // 1. 检查资源消耗 (CommitAbility)
    if (!CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo))
    {
        EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
        return;
    }
    
    // 2. 生成火球actor
    AFireballProjectile* Fireball = GetWorld()->SpawnActorDeferred<AFireballProjectile>(
        FireballClass, 
        GetAvatarActorFromCurrentInfo()->GetTransform()
    );
    
    // 3. 设置火球属性
    Fireball->SetDamage(DamageValue);
    Fireball->FinishSpawning(GetAvatarActorFromCurrentInfo()->GetTransform());
    
    // 4. 结束能力
    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, false, false);
}

例子2:从事件触发的治疗技能

cpp 复制代码
// 当收到治疗事件时激活
void UHealAbility::K2_ActivateAbilityFromEvent(const FGameplayEventData& EventData)
{
    // 从事件数据中获取治疗目标
    AActor* Target = EventData.Target;
    
    // 提交能力消耗
    if (CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo))
    {
        // 应用治疗效果
        UGameplayEffect* HealEffect = CreateHealEffect(EventData.EventMagnitude);
        ApplyGameplayEffectToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, 
                                  HealEffect, Target);
    }
    
    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}

执行流程示例:

cpp 复制代码
// 系统调用流程:
// 1. CallActivateAbility被调用
void UGameplayAbility::CallActivateAbility(...)
{
    PreActivate(...);      // 初始化工作
    ActivateAbility(...);  // 调用实际的激活函数
}

// 2. ActivateAbility根据实现类型调用相应的函数
void UGameplayAbility::ActivateAbility(...)
{
    if (bHasBlueprintActivate)
    {
        K2_ActivateAbility();  // 调用蓝图实现
    }
    else if (bHasBlueprintActivateFromEvent && TriggerEventData)
    {
        K2_ActivateAbilityFromEvent(*TriggerEventData);  // 调用事件触发的蓝图实现
    }
    else
    {
        // C++ 实现
    }
}

预测执行例子:

cpp 复制代码
// 对于支持预测的能力(如移动技能)
void UDashAbility::ConfirmActivateSucceed()
{
    // 服务器确认预测执行成功
    // 这里可以执行需要服务器确认后的逻辑
    Super::ConfirmActivateSucceed();
    
    // 给予奖励或执行确认后的效果
    GrantPredictionReward();
}

这套系统允许设计者在蓝图中轻松创建复杂的能力逻辑,同时保持C++的性能和控制力。

相关推荐
HELLOMILI8 小时前
[UnrealEngine] 虚幻编辑器界面 | 虚幻界面详解 | UE5界面详解
游戏·ue5·编辑器·游戏引擎·虚幻·unreal engine
AA陈超1 天前
虚幻引擎UE5专用服务器游戏开发-32 使用Gameplay Tags阻止连招触发
c++·游戏·ue5·游戏引擎·虚幻
努力的小钟1 天前
UE5 GAS GameAbility源码解析 CanActivateAbility
ue5
危险库1 天前
【UE4/UE5】在虚幻引擎中创建控制台指令的几种方法
c++·ue5·游戏引擎·ue4·虚幻
m0_552200822 天前
《UE5_C++多人TPS完整教程》学习笔记60 ——《P61 开火蒙太奇(Fire Montage)》
c++·游戏·ue5
avi91113 天前
Unreal虚幻粒子系统二三事
ue5·虚幻引擎·unreal
一梦、んんん3 天前
UE 雷达干扰效果模拟
ue5
玉龙20254 天前
虚幻引擎|UE5制作DeepSeek插件并打包发布
ue5·游戏引擎·虚幻·虚幻引擎基础入门·=学习·虚幻引擎插件
努力的小钟4 天前
Unreal Engine GameplayTag匹配功能详解
ue5