文章目录
- 一、ActivateAbility、PreActivate、CallActivateAbility
- 二、ConfirmActivateSucceed
- 三、PostNetInit
- 接口
一、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); // 实际激活
}
具体场景示例
场景:玩家施放治疗术
- PreActivate阶段:
- 设置
bIsActive = true
- 添加 "Casting.Heal" 标签
- 阻塞其他施法类能力
- 增加ActiveCount计数
- ActivateAbility阶段:
- 因为是蓝图能力,调用
K2_ActivateAbility()
- 蓝图中执行具体的治疗逻辑
- 必须调用
CommitAbility
来消耗魔法值
- 如果提交失败:
cpp
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true); // 取消能力
}
网络同步示例
对于本地预测的火球术:
cpp
// 客户端立即施放,服务器验证
if (GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)
{
// 客户端先执行,标签通过最小复制同步
Comp->AddMinimalReplicationGameplayTags(ActivationOwnedTags);
}
关键设计要点
- 资源提交 :蓝图能力必须在执行链中调用
CommitAbility
- 状态管理:通过标签系统管理能力状态和交互
- 网络同步:支持服务器权威和本地预测两种模式
- 错误处理:对配置错误有完整的警告和取消机制
- 生命周期:通过ActiveCount跟踪能力实例的激活状态
这套系统确保了GameplayAbility在各种复杂场景下的稳定运行,包括网络游戏中的预测和回滚机制。
关键流程总结:
- CallActivateAbility → PreActivate → ActivateAbility
- PreActivate 负责状态设置、网络同步、标签管理
- ActivateAbility 根据能力类型调用具体的激活逻辑
- 整个流程确保了能力的正确初始化、网络同步和资源管理
二、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();
}
设计意义
- 网络预测:支持客户端预测,提升响应性
- 服务器权威:最终决定权在服务器,防止作弊
- 状态同步:确保客户端和服务器状态一致
- 错误恢复:预测失败时可以回滚客户端效果
这个机制是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());
}
}
设计意义
- 网络兼容性:确保通过网络复制的能力能够正常工作
- 延迟初始化:允许能力在网络复制完成后才设置关键引用
- 错误恢复:自动修复复制过程中丢失的引用信息
- 系统健壮性:通过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++的性能和控制力。