文章目录
- 一、EndAbility
- 二、IsEndAbilityValid
- 三、K2_EndAbility、K2_EndAbilityLocally
一、EndAbility
cpp
void UGameplayAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
// 检查是否可以结束这个能力
if (IsEndAbilityValid(Handle, ActorInfo))
{
// 检查作用域锁计数,如果大于0表示能力正在被锁定,不能立即结束
if (ScopeLockCount > 0)
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("Attempting to end Ability %s but ScopeLockCount was greater than 0, adding end to the WaitingToExecute Array"), *GetName());
// 将结束操作添加到等待执行队列,等锁释放后执行
WaitingToExecute.Add(FPostLockDelegate::CreateUObject(this, &UGameplayAbility::EndAbility, Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled));
return;
}
// 对于非实例化策略的能力,设置能力结束标志
if (GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
{
bIsAbilityEnding = true;
}
// 调用蓝图的结束事件,给蓝图机会处理结束逻辑
K2_OnEndAbility(bWasCancelled);
// 检查蓝图是否已经结束了这个能力
if (bIsActive == false && GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
{
return;
}
// 停止所有与这个能力相关的计时器和延迟操作
UWorld* MyWorld = GetWorld();
if (MyWorld)
{
MyWorld->GetLatentActionManager().RemoveActionsForObject(this);
if (FAbilitySystemTweaks::ClearAbilityTimers)
{
MyWorld->GetTimerManager().ClearAllTimersForObject(this);
}
}
// 广播能力结束事件并清除委托绑定
OnGameplayAbilityEnded.Broadcast(this);
OnGameplayAbilityEnded.Clear();
// 广播带数据的结束事件并清除委托绑定
OnGameplayAbilityEndedWithData.Broadcast(FAbilityEndedData(this, Handle, bReplicateEndAbility, bWasCancelled));
OnGameplayabilityEndedWithData.Clear();
// 对于非实例化策略的能力,重置活动状态
if (GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
{
bIsActive = false;
bIsAbilityEnding = false;
}
// 清理所有活跃的任务
for (int32 TaskIdx = ActiveTasks.Num() - 1; TaskIdx >= 0 && ActiveTasks.Num() > 0; --TaskIdx)
{
UGameplayTask* Task = ActiveTasks[TaskIdx];
if (Task)
{
Task->TaskOwnerEnded(); // 通知任务所有者已结束
}
}
ActiveTasks.Reset(); // 清空任务数组但不释放内存,因为对象可能很快被销毁
// 获取能力系统组件并进行清理
if (UAbilitySystemComponent* const AbilitySystemComponent = ActorInfo->AbilitySystemComponent.Get())
{
// 如果需要复制,通知网络结束能力
if (bReplicateEndAbility)
{
AbilitySystemComponent->ReplicateEndOrCancelAbility(Handle, ActivationInfo, this, false);
}
// 移除能力添加的标签
AbilitySystemComponent->RemoveLooseGameplayTags(ActivationOwnedTags);
// 根据网络设置处理标签复制
if (UAbilitySystemGlobals::Get().ShouldReplicateActivationOwnedTags())
{
if (GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted || GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerInitiated)
{
// 对于客户端也执行的能力,使用最小复制
AbilitySystemComponent->RemoveMinimalReplicationGameplayTags(ActivationOwnedTags);
}
else
{
// 其他情况使用完全复制
AbilitySystemComponent->RemoveReplicatedLooseGameplayTags(ActivationOwnedTags);
}
}
// 移除跟踪的GameplayCues
for (FGameplayTag& GameplayCueTag : TrackedGameplayCues)
{
AbilitySystemComponent->RemoveGameplayCue(GameplayCueTag);
}
TrackedGameplayCues.Empty();
// 处理能力取消相关逻辑
if (CanBeCanceled())
{
AbilitySystemComponent->HandleChangeAbilityCanBeCanceled(GetAssetTags(), this, false);
}
// 处理能力阻塞相关逻辑
if (IsBlockingOtherAbilities())
{
AbilitySystemComponent->ApplyAbilityBlockAndCancelTags(GetAssetTags(), this, false, BlockAbilitiesWithTag, false, CancelAbilitiesWithTag);
}
// 清除复制数据缓存
AbilitySystemComponent->ClearAbilityReplicatedDataCache(Handle, CurrentActivationInfo);
// 通知能力系统组件能力已结束
AbilitySystemComponent->NotifyAbilityEnded(Handle, this, bWasCancelled);
}
// 对于实例化能力,重置当前事件数据
if (IsInstantiated())
{
CurrentEventData = FGameplayEventData{};
}
}
}
具体例子说明
假设我们有一个火球术能力:
- 能力激活时:
- 添加标签
Gameplay.Activating.Fireball
(激活标签) - 启动GameplayCue显示施法特效
- 创建任务来处理火球飞行逻辑
- 当调用
EndAbility
时:
cpp
// 假设玩家在施法过程中被打断,bWasCancelled = true
FireballAbility->EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
执行流程:
- 作用域锁检查:如果没有锁定,继续执行
- 蓝图事件 :调用
K2_OnEndAbility(true)
,蓝图可以播放打断动画 - 清理任务:停止火球飞行任务和任何相关的计时器
- 移除标签 :移除
Gameplay.Activating.Fireball
标签 - 移除特效 :移除施法
GameplayCue
- 网络同步:如果是服务器,复制结束事件到客户端
- 取消阻塞:如果火球术阻塞了其他能力,现在解除阻塞
- 通知系统 :告诉
AbilitySystemComponent
火球术已结束
- 如果是正常结束(火球命中目标):
bWasCancelled = false
- 可能会在
K2_OnEndAbility
中播放命中特效 - 其他清理步骤相同
这个函数确保了能力结束时所有相关资源都被正确清理,状态被重置,系统回到稳定状态。
二、IsEndAbilityValid
cpp
bool UGameplayAbility::IsEndAbilityValid(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo) const
{
// 保护机制:防止EndAbility被多次调用
// 结束AbilityState可能会导致此函数被再次调用
if ((bIsActive == false || bIsAbilityEnding == true) && GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("IsEndAbilityValid returning false on Ability %s due to EndAbility being called multiple times"), *GetName());
return false;
}
// 检查能力是否有有效的所有者组件
UAbilitySystemComponent* AbilityComp = ActorInfo ? ActorInfo->AbilitySystemComponent.Get() : nullptr;
if (AbilityComp == nullptr)
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("IsEndAbilityValid returning false on Ability %s due to AbilitySystemComponent being invalid"), *GetName());
return false;
}
// 检查这是否是NonInstanced能力或者能力是否处于激活状态
const FGameplayAbilitySpec* Spec = AbilityComp->FindAbilitySpecFromHandle(Handle);
const bool bIsSpecActive = Spec ? Spec->IsActive() : IsActive();
if (!bIsSpecActive)
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("IsEndAbilityValid returning false on Ability %s due spec not being active"), *GetName());
return false;
}
return true;
}
逐行详细注释:
1. 重复调用保护
cpp
if ((bIsActive == false || bIsAbilityEnding == true) && GetInstancingPolicy() != EGameplayAbilityInstancingPolicy::NonInstanced)
- 检查条件:如果能力已经不活跃或正在结束,且不是NonInstanced实例策略
- 目的:防止对同一个能力实例多次调用EndAbility
- 实例策略说明:
NonInstanced
:能力不创建实例,所有调用共享同一份代码InstancedPerActor
:每个Actor创建一个实例InstancedPerExecution
:每次执行创建一个新实例
2. 能力系统组件有效性检查
cpp
UAbilitySystemComponent* AbilityComp = ActorInfo ? ActorInfo->AbilitySystemComponent.Get() : nullptr;
if (AbilityComp == nullptr)
- 检查条件:确保拥有有效的AbilitySystemComponent
- 重要性:AbilitySystemComponent是管理所有游戏能力的核心组件
3. 能力规格状态检查
cpp
const FGameplayAbilitySpec* Spec = AbilityComp->FindAbilitySpecFromHandle(Handle);
const bool bIsSpecActive = Spec ? Spec->IsActive() : IsActive();
- 功能:通过句柄查找对应的能力规格,并检查其激活状态
- 回退机制:如果找不到Spec,使用基础的IsActive()方法
具体例子说明:
例子1:角色冲刺能力
cpp
// 假设有一个冲刺能力
class UDashAbility : public UGameplayAbility
{
// 实现...
};
// 场景:玩家按下冲刺键
void APlayerCharacter::OnDashPressed()
{
// 激活冲刺能力
AbilitySystemComponent->TryActivateAbility(DashAbilityHandle);
// 2秒后尝试结束冲刺
GetWorld()->GetTimerManager().SetTimer(DashTimer, [this]() {
// 这里会调用IsEndAbilityValid进行检查
AbilitySystemComponent->CancelAbility(DashAbility);
}, 2.0f, false);
}
检查过程:
- 第一次调用IsEndAbilityValid:所有检查通过,可以结束
- 如果意外再次调用:bIsAbilityEnding为true,返回false,防止重复结束
例子2:火球法术能力
cpp
// 火球法术,使用InstancedPerExecution策略
class UFireballAbility : public UGameplayAbility
{
EGameplayAbilityInstancingPolicy::InstancedPerExecution
};
// 场景:法师连续施放火球
void AWizard::CastFireball()
{
// 每次施放都创建新实例
AbilitySystemComponent->TryActivateAbility(FireballAbilityHandle);
// 如果在火球飞行过程中再次施放
// 新的火球实例与旧的互不影响
// 每个实例独立检查IsEndAbilityValid
}
例子3:被动光环能力
cpp
// 被动能力,使用NonInstanced策略
class UAuraAbility : public UGameplayAbility
{
EGameplayAbilityInstancingPolicy::NonInstanced
};
// 场景:角色获得治疗光环
void APlayerCharacter::AcquireAura()
{
// 激活被动光环
AbilitySystemComponent->TryActivateAbility(AuraAbilityHandle);
// 即使光环已经激活,也可以安全地多次调用结束检查
// 因为NonInstanced能力不受重复调用保护的限制
}
返回值说明:
- 返回true:能力可以安全结束
- 返回false :能力不能结束,可能因为:
- 能力已经结束或正在结束(重复调用保护)
- 缺少有效的AbilitySystemComponent
- 能力规格不处于激活状态
这个函数是GameplayAbilitySystem中重要的安全机制,确保能力生命周期管理的正确性和稳定性。
三、K2_EndAbility、K2_EndAbilityLocally
cpp
void UGameplayAbility::K2_EndAbility()
{
ensure(CurrentActorInfo); // 确保当前有有效的Actor信息
bool bReplicateEndAbility = true; // 需要在网络上复制
bool bWasCancelled = false; // 正常结束,不是被取消
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo,
bReplicateEndAbility, bWasCancelled);
}
void UGameplayAbility::K2_EndAbilityLocally()
{
ensure(CurrentActorInfo); // 确保当前有有效的Actor信息
bool bReplicateEndAbility = false; // 不在网络上复制
bool bWasCancelled = false; // 正常结束,不是被取消
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo,
bReplicateEndAbility, bWasCancelled);
}
关键参数说明
bReplicateEndAbility
- true: 结束事件会在网络上的所有客户端同步
- false: 只在本地客户端结束,不进行网络同步
bWasCancelled
- false: 能力正常结束(完成任务)
- true: 能力被中断取消
具体例子说明
例子1:多人游戏中的治疗技能
cpp
class UHealAbility : public UGameplayAbility
{
UFUNCTION(BlueprintCallable)
void CompleteHealing()
{
// 治疗完成,需要在所有客户端同步结束
K2_EndAbility(); // bReplicateEndAbility = true
}
UFUNCTION(BlueprintCallable)
void InterruptHealing()
{
// 被攻击打断,本地立即结束,网络同步由其他系统处理
K2_EndAbilityLocally(); // bReplicateEndAbility = false
}
};
场景:
- 玩家A对玩家B使用治疗技能
- 治疗完成后调用
K2_EndAbility()
,所有玩家都能看到治疗结束的效果 - 如果治疗过程中玩家A被攻击,调用
K2_EndAbilityLocally()
立即在本地结束,避免延迟
例子2:本地视觉特效能力
cpp
class ULocalVFXAbility : public UGameplayAbility
{
UFUNCTION(BlueprintCallable)
void SpawnParticles()
{
// 生成本地粒子特效
SpawnLocalParticleSystem();
// 特效播放完毕后,只需要在本地结束
GetWorld()->GetTimerManager().SetTimer(
EndTimer,
this,
&ULocalVFXAbility::OnParticlesFinished,
3.0f,
false
);
}
void OnParticlesFinished()
{
// 纯视觉效果,不需要网络同步
K2_EndAbilityLocally();
}
};
例子3:客户端预测的移动能力
cpp
class UDashAbility : public UGameplayAbility
{
UFUNCTION(BlueprintCallable)
void StartDash()
{
// 客户端预测的冲刺
PerformDashMovement();
// 设置结束计时器
GetWorld()->GetTimerManager().SetTimer(
DashTimer,
this,
&UDashAbility::OnDashComplete,
DashDuration,
false
);
}
void OnDashComplete()
{
if (GetOwningActorFromActorInfo()->HasAuthority())
{
// 服务器端:同步结束到所有客户端
K2_EndAbility();
}
else
{
// 客户端:本地结束,等待服务器确认
K2_EndAbilityLocally();
}
}
};
网络同步场景对比
使用 K2_EndAbility()
的情况:
cpp
// 服务器端
void APlayerCharacter::OnAbilityCompleted()
{
// 服务器调用,所有客户端都会收到结束事件
Ability->K2_EndAbility();
}
// 客户端1、客户端2、客户端3都会同步结束该能力
使用 K2_EndAbilityLocally()
的情况:
cpp
// 客户端预测
void APlayerCharacter::OnLocalPrediction()
{
// 只有本地客户端结束,其他客户端不受影响
Ability->K2_EndAbilityLocally();
}
// 只有调用者本地结束,其他客户端保持原状态
实际应用建议
使用 K2_EndAbility()
当:
- 能力结束影响游戏状态(生命值、资源等)
- 需要在所有客户端同步视觉效果
- 服务器发起的结束操作
- 重要的游戏流程状态改变
使用 K2_EndAbilityLocally()
当:
- 纯本地视觉效果
- 客户端预测操作
- 临时性的本地状态
- 避免网络延迟影响的用户体验
总结
这两个函数提供了灵活的能力结束机制:
K2_EndAbility()
:用于需要网络同步的重要能力结束K2_EndAbilityLocally()
:用于本地化的、不需要同步的能力结束
正确选择使用哪个函数对于多人游戏的网络同步和性能优化至关重要。