Modify GameplayEffect Duration at Runtime in Unreal GAS

5.1k 词

问题

在 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 的持续时间。

思路如下:

  1. 找到指定句柄对应的 FActiveGameplayEffect
  2. 修改它的 Spec.Duration
  3. 重置开始时间,使“剩余时间 = 新持续时间”。
  4. 标记 FastArray 条目脏数据,触发网络复制。
  5. 调用 CheckDuration 刷新内部定时逻辑。
  6. 广播事件,让监听方(例如 AbilityTask)能收到变化。
1
2
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 的持续时间已变更”。
}

使用

1
2
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);
}
}
}
留言