问题
在 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)能收到变化。
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 的持续时间已变更”。
}
使用
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);
}
}
}