问题
在 Unreal Engine 的 Gameplay Ability System (GAS) 中,GameplayEffect (GE) 是一个非常核心的概念。
GE 可以用来表示 Buff、Debuff、DOT(持续伤害)、HOT(持续治疗)等效果。
它们通常拥有三种持续策略:
- Instant:立即生效,瞬时结束。
- Infinite:无限持续,直到被移除。
- Has Duration:有固定持续时间。
但是,GAS 默认的机制下 GE 的持续时间一旦在应用时确定,就不能在运行时修改。
这就带来了问题:
- 如果一个 Buff 应该因为技能升级而延长时间怎么办?
- 如果一个 DOT 效果受到某个状态影响需要提前结束怎么办?
在本人开发的项目中,一名角色的大招持续时间可以通过击杀来延长,这当然可以通过Ability来控制GE生命周期,并动态延长Ability的持续时间解决。
但我想尝试对GE的Duration进行修改,来实现相应的功能,以便在后续不得不针对GE的Duration修改时,能够直接采用相同的方案。
 思路
首先尝试过直接修改 GameplayEffectSpec.Duration,但发现并不会影响已经应用的 ActiveGameplayEffect:
- 原因:FActiveGameplayEffect在应用时缓存了开始时间和持续时长,内部的计时器与复制系统不会因为修改Spec而自动更新。
- 因此,必须从 AbilitySystemComponent (ASC) 层面入手,直接操作 ActiveGameplayEffect。
我们通过继承 UAbilitySystemComponent,新增一个接口 SetGameplayEffectDurationHandle,实现运行时修改已应用 GE 的持续时间。
思路如下:
- 找到指定句柄对应的 FActiveGameplayEffect。
- 修改它的 Spec.Duration。
- 重置开始时间,使“剩余时间 = 新持续时间”。
- 标记 FastArray 条目脏数据,触发网络复制。
- 调用 CheckDuration刷新内部定时逻辑。
- 广播事件,让监听方(例如 AbilityTask)能收到变化。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 
 | void USuperAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
 {
 if (!Handle.IsValid()) { return; }
 // 检查传入句柄有效性。若句柄无效(例如未初始化或已被移除),
 // 直接返回不做任何操作,防止访问非法内存或无意义操作。
 
 const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
 // 使用 ASC 的只读接口通过句柄查询当前对应的 FActiveGameplayEffect(AGE)。
 // 注意这里返回的是 const 指针,说明 API 想保护 AGE 不被直接修改(常见设计)。
 
 if (!ActiveGameplayEffect) { return; }
 // 再次检查:如果没有找到对应 AGE(例如句柄对应的效果已被移除),
 // 就返回,防止后续对空指针的操作。
 
 FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
 // const_cast 把 const FActiveGameplayEffect* 转成可写指针。
 // 含义/理由:在运行时直接修改 AGE 的成员(比如 Spec、StartWorldTime 等),
 // 但 API 只给了 const 视图;因此通过 const_cast 绕过编译器限制。
 // 注意:这在语义上可行前提是底层对象确实不是 const。
 
 if (NewDuration > 0) {
 AGE->Spec.Duration = NewDuration;
 } else {
 AGE->Spec.Duration = 0.01f; // 避免为 0
 }
 
 AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
 // 将 AGE 的服务器起始世界时间重置为当前服务器世界时间。
 // 含义:把“起始时间”设为现在,从而使剩余时间 = 新的 Spec.Duration。
 
 AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
 // 更新缓存的服务器起始时间,确保内部逻辑/复制数据一致。
 
 AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
 // 将客户端可见的/本地世界时间的起始时间也重置为现在,
 // 这样客户端计时基线也能与服务器同步。
 
 ActiveGameplayEffects.MarkItemDirty(*AGE);
 // 标记 ActiveGameplayEffects(FastArray 容器)中的该条目为“脏”,
 // 确保修改通过网络复制给客户端。
 
 ActiveGameplayEffects.CheckDuration(Handle);
 // 重新评估 AGE 的到期逻辑。
 // 包括是否到期、是否需要重设定时器等。
 
 AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
 // 触发 OnTimeChanged 事件,通知监听者(例如 AbilityTask、UI)更新。
 
 OnGameplayEffectDurationChange(*AGE);
 // 调用 ASC 的回调,告诉上层 “某个 ActiveGameplayEffect 的持续时间已变更”。
 }
 
 | 
 使用
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 
 | void URiptideAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
 const FGameplayAbilityActorInfo* ActorInfo,
 const FGameplayAbilityActivationInfo ActivationInfo,
 const FGameplayEventData* TriggerEventData)
 {
 ...
 
 // 如果配置了 GE_Riptide(比如一个 Buff 或技能效果)
 else if (GE_Riptide)
 {
 // 给角色自己施加 GameplayEffect,并保存返回的句柄以便后续操作
 RiptideGameplayEffectHandle = ApplyGameplayEffectToOwner(
 Handle, ActorInfo, ActivationInfo, GE_Riptide.GetDefaultObject(), 1.f);
 
 ...
 
 // 仅在服务器(Authority)端绑定事件,避免多端重复逻辑
 if (HasAuthority(&ActivationInfo))
 {
 // 创建一个任务,等待指定的 GameplayEvent(KillEventTag)
 if (UAbilityTask_WaitGameplayEvent* WaitGameplayEvent =
 UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(this, KillEventTag))
 {
 // 绑定事件回调,当收到事件时调用 OnKillEvent
 WaitGameplayEvent->EventReceived.AddDynamic(this, &ThisClass::OnKillEvent);
 
 // 激活这个任务
 WaitGameplayEvent->ReadyForActivation();
 }
 }
 }
 }
 
 void URiptideAbility::OnKillEvent(FGameplayEventData EventData)
 {
 // 获取击杀者 Actor(Instigator),并确保它在服务器端
 AActor* KillerActor = const_cast<AActor*>(EventData.Instigator.Get());
 if (!KillerActor || !KillerActor->HasAuthority())
 {
 return;
 }
 
 // 定义额外增加的持续时间,这里固定为 5 秒
 constexpr float ExtraDuration = 5.f;
 
 // 调用扩展 Buff 持续时间的逻辑
 ExtendRiptideDurationOnKill(KillerActor, ExtraDuration);
 }
 
 void URiptideAbility::ExtendRiptideDurationOnKill(const AActor* KillerActor, float ExtraDuration)
 {
 if (!KillerActor)
 {
 return;
 }
 
 // 先获取 PlayerState,再通过 PlayerState 找到对应的角色 Pawn
 const APlayerState* PlayerState = Cast<APlayerState>(KillerActor);
 const ABaseCharacter* Character = Cast<ABaseCharacter>(PlayerState->GetPawn());
 if (!Character)
 {
 return;
 }
 
 // 如果当前没有 Riptide 的有效 GameplayEffect 句柄,则直接返回
 if (!RiptideGameplayEffectHandle.IsValid())
 {
 return;
 }
 
 // 获取角色的 AbilitySystemComponent,并确认是我们扩展过的 USuperAbilitySystemComponent
 if (USuperAbilitySystemComponent* ASC =
 Cast<USuperAbilitySystemComponent>(Character->GetAbilitySystemComponent()))
 {
 // 检查当前 Riptide Buff 是否还在生效
 if (const FActiveGameplayEffect* ActiveGE =
 ASC->GetActiveGameplayEffect(RiptideGameplayEffectHandle))
 {
 // 获取 Buff 剩余的持续时间
 float RemainingTime = ActiveGE->GetTimeRemaining(GetWorld()->TimeSeconds);
 
 // 计算新的总持续时间 = 剩余时间 + 额外时间
 float NewDuration = RemainingTime + ExtraDuration;
 
 // 调用自定义的接口,动态修改 GameplayEffect 的持续时间
 ASC->SetGameplayEffectDurationHandle(RiptideGameplayEffectHandle, NewDuration);
 
 UE_LOG(LogTemp, Log, TEXT("%f"), NewDuration);
 }
 }
 }
 
 |