07 AbilityTasks 异步任务
目标:理解为什么 GAS 需要自己的异步任务系统,掌握内置 Task 的使用、自定义 Task 的完整创建流程(静态工厂/输出委托/Activate/OnDestroy/回调)、bSimulatedTask/bTickingTask 的含义、Root Motion Source 任务、Tick Task 使用场景,以及网络感知 Task 的权限分配。
7.1 为什么需要 AbilityTask
GA 本身只执行一帧。 这是理解 AbilityTask 存在意义的关键。
想象"蓄力射击":按下按钮开始蓄力动画,蓄力 2 秒后松开发射。如果只靠 GA,你无法"等待"2 秒——GA 在一帧内就执行完了。
AbilityTask(AT) 就是为此设计的延迟动作(Latent Action):能在多帧中持续运行,等待外部事件或条件满足后再继续。
类比:如果把 GA 比作一个函数调用,AbilityTask 就像 async/await:
普通函数(GA):开始 → 执行一帧逻辑 → 结束
异步函数(GA+AT):开始 → 创建异步任务 → 等待... → 任务完成/回调 → 继续 → 结束
底层由 GameplayTasks 模块的 UGameplayTasksComponent 驱动。Task 挂在 GA 上,随 GA 生命周期运行;可在特定权限(服务端/客户端)运行;能安全处理"技能被打断 → 所有子任务自动清理"。
关键结论:使用 AbilityTask 的技能,实例化策略必须为
InstancedPerExecution或InstancedPerActor(NonInstanced 不能使用 Task)。
7.2 内置 AbilityTask 列表
| 类别 | AbilityTask | 用途 |
|---|---|---|
| 移动 | AbilityTask_MoveToLocation |
Root Motion Source 移动到目标位置 |
| 移动 | AbilityTask_RotateToTarget |
旋转朝向目标 |
| 动画 | PlayMontageAndWait |
播放蒙太奇并等待完成 |
| 属性监听 | WaitAttributeChange / WaitAttributeChangeThreshold |
等待属性变化/超阈值 |
| GE 监听 | WaitGameplayEffectApplied |
等待 GE 被应用 |
| 输入响应 | WaitInputPress / WaitInputRelease |
等待输入按下/释放 |
| 事件 | WaitGameplayEvent |
等待 GameplayEvent |
| 目标选择 | WaitTargetData |
等待玩家选择目标 |
| 延迟 | WaitDelay |
等待指定时间 |
| 碰撞 | WaitGameplayOverlap |
等待碰撞重叠 |
| 确认/取消 | WaitConfirmCancel |
等待确认或取消输入 |
| 网络同步 | WaitNetSync |
等待客户端/服务器同步 |
| 生成 | SpawnActor |
生成指定 Actor(投射物) |
全局最大并发数
关键:
UAbilityTask构造函数强制执行一个硬编码的全局最大 1000 个并发 AbilityTask。设计大量角色的游戏(RTS)时注意此限制。
7.3 标准用法模板(每步都重要)
void UGA_Fireball::ActivateAbility(...)
{
// 1. 创建 Task(静态工厂函数)
UAbilityTask_WaitDelay* WaitTask = UAbilityTask_WaitDelay::CreateWaitDelay(this, CastTime);
// 2. 绑定完成委托
WaitTask->OnFinish.AddDynamic(this, &UGA_Fireball::OnCastFinished);
// 3. 关键:ReadyForActivation() 真正启动 Task
WaitTask->ReadyForActivation();
}
最常见 Bug:忘记调
ReadyForActivation(),Task 永远不会开始。
Blueprint 中:直接使用对应节点,不需要手动调用 ReadyForActivation()——它由 K2Node_LatentGameplayTaskCall(GameplayTasksEditor)自动调用(还会自动调 BeginSpawningActor/FinishSpawningActor)。
关键:这些自动魔法只在 Blueprint 中有效。在 C++ 中必须手动调用
ReadyForActivation()、BeginSpawningActor()、FinishSpawningActor()。
手动取消:EndTask()(C++)或 Async Task Proxy 的 EndTask 节点(BP)。
7.4 完整示例:近战连击(Montage + 等待输入)
UCLASS()
class UGA_ComboAttack : public UGameplayAbility
{
GENERATED_BODY()
public:
virtual void ActivateAbility(...) override;
UPROPERTY(EditDefaultsOnly) TArray<TObjectPtr<UAnimMontage>> ComboMontages;
UFUNCTION() void OnMontageCompleted();
UFUNCTION() void OnMontageInterrupted();
UFUNCTION() void OnInputPressed();
};
void UGA_ComboAttack::ActivateAbility(...)
{
if (!CommitAbility(...)) { EndAbility(...); return; }
const int32 ComboIndex = FMath::Min(CurrentComboIndex, ComboMontages.Num() - 1);
UAbilityTask_PlayMontageAndWait* MontageTask =
UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this, NAME_None, ComboMontages[ComboIndex]);
MontageTask->OnCompleted.AddDynamic(this, &UGA_ComboAttack::OnMontageCompleted);
MontageTask->OnInterrupted.AddDynamic(this, &UGA_ComboAttack::OnMontageInterrupted);
MontageTask->ReadyForActivation();
}
void UGA_ComboAttack::OnMontageCompleted()
{
// 等待"攻击输入"进入下一段连招(窗口期)
UAbilityTask_WaitInputPress* InputTask =
UAbilityTask_WaitInputPress::CreateWaitInputPress(this, true);
InputTask->OnPress.AddDynamic(this, &UGA_ComboAttack::OnInputPressed);
InputTask->ReadyForActivation();
}
7.5 自定义 AbilityTask 的完整组成
┌─────────────────────────────────────────────────┐
│ Custom AbilityTask │
├─────────────────────────────────────────────────┤
│ 1. 静态创建函数(BlueprintCallable 工厂) │
│ 2. 输出委托(BlueprintAssignable) │
│ 3. Activate() —— 开始主要工作、绑定外部委托 │
│ 4. OnDestroy() —— 清理工作、解绑外部委托 │
│ 5. 回调函数 —— 处理绑定的外部委托 │
│ 6. 成员变量和内部辅助函数 │
└─────────────────────────────────────────────────┘
输出委托限制
常见陷阱:AbilityTask 只能声明一种类型的输出委托。所有输出委托必须是同一类型,不用的参数传默认值。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FMyTaskDelegate, FGameplayTag, EventTag, FGameplayEventData, EventData);
UPROPERTY(BlueprintAssignable) FMyTaskDelegate OnSuccess;
UPROPERTY(BlueprintAssignable) FMyTaskDelegate OnFailure;
// 即使 OnFailure 不需要 EventData,也必须传默认值
bSimulatedTask(模拟客户端支持)
默认 Task 只在运行拥有 GA 的客户端或服务器上执行。要在模拟代理上运行需启用:
UMyAbilityTask::UMyAbilityTask(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bSimulatedTask = true;
}
// 必须重写:
virtual void InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent) override
{
Super::InitSimulatedTask(InGameplayTasksComponent);
// 初始化模拟所需成员变量
}
UPROPERTY(Replicated) float SimulatedValue; // 成员变量需要复制
使用场景:移动相关 Task(AbilityTask_MoveToLocation);不想复制每一个移动变化而是模拟整个任务。所有 Root Motion Source Task 都这样。
bTickingTask(帧更新支持)
UMyAbilityTask::UMyAbilityTask(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bTickingTask = true;
}
virtual void TickTask(float DeltaTime) override
{
Super::TickTask(DeltaTime);
CurrentAlpha = FMath::FInterpTo(CurrentAlpha, TargetAlpha, DeltaTime, InterpSpeed);
if (FMath::IsNearlyEqual(CurrentAlpha, TargetAlpha))
{
if (ShouldBroadcastAbilityTaskDelegates())
OnTargetReached.Broadcast(FGameplayTag(), FGameplayEventData());
EndTask();
}
}
7.6 实战级自定义 Task 示例:PlayMontageAndWaitForEvent
这是 GASDocumentation 中最经典的自定义 Task——把 PlayMontageAndWait 和 WaitGameplayEvent 合二为一,让蒙太奇通过 AnimNotify 发送 GameplayEvent 回到 GA。
头文件
// GDAT_PlayMontageAndWaitForEvent.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
FGDPlayMontageAndWaitForEventDelegate,
FGameplayTag, EventTag, FGameplayEventData, EventData);
UCLASS()
class UGDAT_PlayMontageAndWaitForEvent : public UAbilityTask
{
GENERATED_BODY()
public:
UGDAT_PlayMontageAndWaitForEvent(const FObjectInitializer& ObjectInitializer);
virtual void Activate() override;
virtual void ExternalCancel() override;
virtual FString GetDebugString() const override;
virtual void OnDestroy(bool AbilityEnded) override;
// ---- 输出委托(同一类型) ----
UPROPERTY(BlueprintAssignable) FGDPlayMontageAndWaitForEventDelegate OnCompleted;
UPROPERTY(BlueprintAssignable) FGDPlayMontageAndWaitForEventDelegate OnBlendOut;
UPROPERTY(BlueprintAssignable) FGDPlayMontageAndWaitForEventDelegate OnInterrupted;
UPROPERTY(BlueprintAssignable) FGDPlayMontageAndWaitForEventDelegate OnCancelled;
UPROPERTY(BlueprintAssignable) FGDPlayMontageAndWaitForEventDelegate EventReceived;
// ---- 静态创建函数 ----
UFUNCTION(BlueprintCallable, Category = "Ability|Tasks",
meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility",
BlueprintInternalUseOnly = "TRUE"))
static UGDAT_PlayMontageAndWaitForEvent* PlayMontageAndWaitForEvent(
UGameplayAbility* OwningAbility,
FName TaskInstanceName,
UAnimMontage* MontageToPlay,
FGameplayTagContainer EventTags,
float Rate = 1.f,
FName StartSection = NAME_None,
bool bStopWhenAbilityEnds = true,
float AnimRootMotionTranslationScale = 1.f);
private:
UPROPERTY() UAnimMontage* MontageToPlay;
UPROPERTY() FGameplayTagContainer EventTags;
UPROPERTY() float Rate;
UPROPERTY() FName StartSection;
UPROPERTY() float AnimRootMotionTranslationScale;
UPROPERTY() bool bStopWhenAbilityEnds;
bool StopPlayingMontage();
void OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted);
void OnAbilityCancelled();
void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted);
void OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload);
FOnMontageBlendingOutStarted BlendingOutDelegate;
FOnMontageEnded MontageEndedDelegate;
FDelegateHandle CancelledHandle;
FDelegateHandle EventHandle;
};
实现文件(关键模式)
// 静态创建函数:用 NewAbilityTask 创建并赋值
UGDAT_PlayMontageAndWaitForEvent* UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(
UGameplayAbility* OwningAbility, FName TaskInstanceName, UAnimMontage* InMontageToPlay,
FGameplayTagContainer InEventTags, float InRate, FName InStartSection,
bool InbStopWhenAbilityEnds, float InAnimRootMotionTranslationScale)
{
UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(InRate);
UGDAT_PlayMontageAndWaitForEvent* MyObj =
NewAbilityTask<UGDAT_PlayMontageAndWaitForEvent>(OwningAbility, TaskInstanceName);
MyObj->MontageToPlay = InMontageToPlay;
MyObj->EventTags = InEventTags;
MyObj->Rate = InRate;
MyObj->StartSection = InStartSection;
MyObj->AnimRootMotionTranslationScale = InAnimRootMotionTranslationScale;
MyObj->bStopWhenAbilityEnds = InbStopWhenAbilityEnds;
return MyObj;
}
// Activate:绑定事件 → 播放蒙太奇 → 绑定取消/混合输出/结束回调
void UGDAT_PlayMontageAndWaitForEvent::Activate()
{
if (Ability == nullptr) return;
bool bPlayedMontage = false;
if (AbilitySystemComponent.IsValid())
{
UAnimInstance* AnimInstance = Ability->GetCurrentActorInfo()->GetAnimInstance();
if (AnimInstance != nullptr)
{
EventHandle = AbilitySystemComponent->AddGameplayEventTagContainerDelegate(
EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(
this, &UGDAT_PlayMontageAndWaitForEvent::OnGameplayEvent));
if (AbilitySystemComponent->PlayMontage(
Ability, Ability->GetCurrentActivationInfo(), MontageToPlay, Rate, StartSection) > 0.f)
{
if (ShouldBroadcastAbilityTaskDelegates() == false) return;
CancelledHandle = Ability->OnGameplayAbilityCancelled.AddUObject(
this, &UGDAT_PlayMontageAndWaitForEvent::OnAbilityCancelled);
BlendingOutDelegate.BindUObject(this, &UGDAT_PlayMontageAndWaitForEvent::OnMontageBlendingOut);
AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, MontageToPlay);
MontageEndedDelegate.BindUObject(this, &UGDAT_PlayMontageAndWaitForEvent::OnMontageEnded);
AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, MontageToPlay);
ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
if (Character && (Character->GetLocalRole() == ROLE_Authority ||
(Character->GetLocalRole() == ROLE_AutonomousProxy &&
Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
{
Character->SetAnimRootMotionTranslationScale(AnimRootMotionTranslationScale);
}
bPlayedMontage = true;
}
}
}
if (!bPlayedMontage)
{
if (ShouldBroadcastAbilityTaskDelegates())
OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
}
SetWaitingOnAvatar();
}
// 回调:BlendingOut → 广播 OnBlendOut / OnInterrupted
// OnMontageEnded(非打断)→ OnCompleted.Broadcast + EndTask()
// OnGameplayEvent → EventReceived.Broadcast(复制 Payload 并更新 EventTag)
// OnDestroy:解绑取消委托;AbilityEnded 且 bStopWhenAbilityEnds 时停蒙太奇;
// 移除事件容器委托 → Super::OnDestroy
void UGDAT_PlayMontageAndWaitForEvent::OnDestroy(bool AbilityEnded)
{
if (Ability)
{
Ability->OnGameplayAbilityCancelled.Remove(CancelledHandle);
if (AbilityEnded && bStopWhenAbilityEnds) StopPlayingMontage();
}
if (AbilitySystemComponent.IsValid())
AbilitySystemComponent->RemoveGameplayEventTagContainerDelegate(EventTags, EventHandle);
Super::OnDestroy(AbilityEnded);
}
模式总结:
Activate()绑定外部委托 + 启动工作;OnDestroy()解绑所有委托 + 清理。绑定/解绑对称是自定义 Task 的资源管理铁律。
使用(在 GA 中)
UGDAT_PlayMontageAndWaitForEvent* Task =
UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(
this, NAME_None, MontageToPlay, FGameplayTagContainer(), 1.0f, NAME_None, false, 1.0f);
Task->OnBlendOut.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnCompleted.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnInterrupted.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->OnCancelled.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->EventReceived.AddDynamic(this, &UGDGA_FireGun::EventReceived);
Task->ReadyForActivation();
7.7 另一个示例:WaitReceiveDamage(监听 ASC 自定义委托)
演示如何监听 ASC 上的自定义委托(伤害事件),Activate 绑定 / OnDestroy 解绑:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(
FWaitReceiveDamageDelegate,
class UAbilitySystemComponent*, SourceASC,
float, UnmitigatedDamage,
float, MitigatedDamage);
UCLASS()
class UGDAT_WaitReceiveDamage : public UAbilityTask
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable) FWaitReceiveDamageDelegate OnDamage;
UFUNCTION(BlueprintCallable, Category = "Ability|Tasks",
meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility",
BlueprintInternalUseOnly = "TRUE"))
static UGDAT_WaitReceiveDamage* WaitReceiveDamage(UGameplayAbility* OwningAbility, bool TriggerOnce);
virtual void Activate() override;
virtual void OnDestroy(bool AbilityIsEnding) override;
UFUNCTION() void OnDamageReceived(class UAbilitySystemComponent* SourceASC,
float UnmitigatedDamage, float MitigatedDamage);
protected:
bool TriggerOnce;
};
void UGDAT_WaitReceiveDamage::Activate()
{
if (UMyAbilitySystemComponent* ASC = Cast<UMyAbilitySystemComponent>(AbilitySystemComponent))
ASC->ReceivedDamage.AddDynamic(this, &UGDAT_WaitReceiveDamage::OnDamageReceived);
}
void UGDAT_WaitReceiveDamage::OnDamageReceived(
UAbilitySystemComponent* SourceASC, float UnmitigatedDamage, float MitigatedDamage)
{
if (ShouldBroadcastAbilityTaskDelegates())
OnDamage.Broadcast(SourceASC, UnmitigatedDamage, MitigatedDamage);
if (TriggerOnce) EndTask(); // 只触发一次则结束
}
void UGDAT_WaitReceiveDamage::OnDestroy(bool AbilityIsEnding)
{
if (UMyAbilitySystemComponent* ASC = Cast<UMyAbilitySystemComponent>(AbilitySystemComponent))
ASC->ReceivedDamage.RemoveDynamic(this, &UGDAT_WaitReceiveDamage::OnDamageReceived);
Super::OnDestroy(AbilityIsEnding);
}
7.8 Root Motion Source AbilityTasks
GAS 提供钩入 CharacterMovementComponent 的 Root Motion Source Task,实现各种移动效果:
| 效果 | 说明 |
|---|---|
| 击退(Knockback) | 被攻击后向后飞退 |
| 复杂跳跃(Complex Jump) | 带弧线/多段跳跃 |
| 拉拽(Pull) | 被敌人拉向目标 |
| 冲刺(Dash) | 快速前冲 |
预测支持版本差异:
- UE 4.19 和 4.25+:预测正常 ✅;
- UE 4.20–4.24:预测有 bug ⚠️(任务仍执行功能,多人有轻微网络修正,单人完全正常)。可从 4.25 cherry-pick 预测修复。
7.9 Tick Task 的使用场景
| 场景 | 说明 |
|---|---|
| 移动到目标位置 | 平滑移动角色到坐标 |
| 缩放 FOV | 平滑改变摄像机视野 |
| 渐变透明度 | 平滑改变材质透明度 |
| 蓄力进度 | 每帧更新蓄力值 |
| 追踪目标 | 每帧更新追踪方向 |
MoveToLocation 式 Tick Task 骨架:
UCLASS()
class UAT_MoveToLocation : public UAbilityTask
{
GENERATED_BODY()
public:
UAT_MoveToLocation(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer) { bTickingTask = true; }
virtual void Activate() override
{ StartLocation = GetAvatarActor()->GetActorLocation(); CurrentAlpha = 0.f; }
virtual void TickTask(float DeltaTime) override
{
Super::TickTask(DeltaTime);
CurrentAlpha = FMath::Clamp(CurrentAlpha + DeltaTime / Duration, 0.f, 1.f);
GetAvatarActor()->SetActorLocation(
FMath::Lerp(StartLocation, TargetLocation, CurrentAlpha));
if (CurrentAlpha >= 1.f)
{
if (ShouldBroadcastAbilityTaskDelegates())
OnTargetReached.Broadcast(FGameplayTag(), FGameplayEventData());
EndTask();
}
}
UPROPERTY(BlueprintAssignable) FMyTaskDelegate OnTargetReached;
FVector StartLocation, TargetLocation;
float Duration, CurrentAlpha;
};
最佳实践:不需要每帧更新时,优先用事件驱动 Task(
WaitGameplayEvent、WaitAttributeChange),比 Tick 高效。
7.10 Task 的网络权限
Task 继承自 UGameplayTask,可声明在哪个权限上运行:
| 权限类型 | 说明 | 示例 |
|---|---|---|
| 默认 | 在创建它的权限上运行 | — |
SimulatedClientOnly |
仅模拟客户端运行 | 客户端表现类 Task |
AuthorityOnly |
仅服务端运行 | 伤害判定、生成投射物(权威) |
ServerInitiated |
服务端发起 | 服务端驱动技能 |
投射物的经典分工:拥有者客户端预测生成"客户端表现体",服务端生成"权威命中判定体",用预测键关联——细节在第 11 章。
7.11 常见技能形态设计(使用场景)
动画驱动能力(近战):
1. 激活 → 2. CommitAbility → 3. PlayMontageAndWaitForEvent 播挥砍动画
4. AnimNotify "Hit" 触发 → EventReceived → 命中帧做伤害检测/施加 GE
5. AnimNotify "EndAbility" → EndAbility()
持续施法(引导技能):
1. 激活 → 2. CommitCost → 3. PlayMontageAndWait(施法循环动画)
4. WaitInputRelease(等待松键)→ 5. 每秒 WaitDelay 循环应用治疗
6. 松键 → 7. CommitCooldown → 8. EndAbility
充能能力(蓄力射击):
1. 按下激活 → 2. PlayMontageAndWait 拉弓 → 3. Tick Task 监控蓄力进度更新 UI
4. 等待:松键(WaitInputRelease)→ 以当前蓄力值射击 / 蓄满自动射
5. CommitAbility → 6. 按蓄力值算倍率 → 7. 生成箭矢 → 8. EndAbility
7.12 Task 生命周期与 GC
- Task 由 GameplayTasksComponent 管理;GA 结束时其未完成任务默认被清理;
bStopWhenAbilityEnds控制蒙太奇在技能结束时是否停止(组合技/被动通常关掉);- Task 是 UObject,注意
TObjectPtr/WeakPtr 引用,避免悬垂; ShouldBroadcastAbilityTaskDelegates()用于广播前检查(防任务销毁后广播悬垂)。
7.13 本章验证
- [ ] 用
PlayMontageAndWait+WaitInputPress实现两段式连招; - [ ] 写一个自定义
bTickingTask的延迟伤害 Task,验证 3 秒后对目标结算; - [ ] 实现一个 PlayMontageAndWaitForEvent 式组合任务,用 AnimNotify 触发伤害帧;
- [ ] 故意忘记
ReadyForActivation()/EndTask(),观察技能卡住现象; - [ ] 理解
bSimulatedTask何时需要(移动/根运动任务); - [ ] 能画出自定义 Task 的 Activate/回调/OnDestroy 绑定解绑对称关系。