一、预测系统的核心概念
1.1 预测的基本需求
在网络游戏中,存在一个根本矛盾:
服务器权威:最终游戏状态由服务器决定
玩家反馈:玩家需要即时反馈,不能等待网络往返
预测系统正是解决这一矛盾的核心机制。
1.2 预测键的本质
预测键(Prediction Key)本质上是一个唯一标识符,用于关联客户端本地预测执行的操作与服务器最终确认的操作:
// Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/GameplayPrediction.h
struct GAMEPLAYABILITIES_API FPredictionKey
{
int16 Current; // 当前键值
int16 Base; // 基础键值
bool bIsServerInitiated; // 是否服务器发起
bool bIsStale; // 是否已过期
/** 是否有效 */
FORCEINLINE bool IsValidForMorePrediction() const
{
return Current > 0;
}
// 唯一标识符功能
FORCEINLINE uint32 GetKeyID() const
{
return ((uint32)Base << 16) | (uint32)Current;
}
};
二、预测系统的工作流程
2.1 基本流程概述
从高层次看,预测系统的工作流程如下:
客户端预测:
客户端生成预测键
执行操作并记录预测键
发送操作请求和预测键给服务器
服务器处理:
接收请求和预测键
执行实际操作
将结果和预测键返回给客户端
客户端确认:
接收服务器结果
查找匹配的预测键
如果预测正确,保留结果;如不正确,回滚并应用服务器结果
2.2 源码层面的实现
在GAS中,预测系统主要由UAbilitySystemComponent
实现:
// Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Private/AbilitySystemComponent.cpp
// 客户端生成预测键
FPredictionKey UAbilitySystemComponent::GeneratePredictionKey()
{
static int16 MachinePredictionKey = 0;
FPredictionKey PredictionKey;
if (IsLocallyControlled())
{
MachinePredictionKey++;
PredictionKey.Current = MachinePredictionKey;
PredictionKey.Base = 0; // 本地生成的预测键Base为0
}
return PredictionKey;
}
// 服务器接收预测键
void UAbilitySystemComponent::ServerTryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool InputPressed, FPredictionKey PredictionKey)
{
// 验证请求
FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityToActivate);
if (!Spec)
{
// 能力不存在,返回失败
ClientActivateAbilityFailed(AbilityToActivate, PredictionKey.Current);
return;
}
// 将客户端预测键标记为已接收,避免重复确认
IncomingPredictionKeys.Add(PredictionKey);
// 使用客户端的预测键作为服务器的预测键,但标记为服务器发起
FPredictionKey ServerPredictionKey = PredictionKey;
ServerPredictionKey.bIsServerInitiated = true;
ScopedPredictionKey.Push(ServerPredictionKey);
// 执行实际能力激活
InternalServerTryActivateAbility(AbilityToActivate, InputPressed, ServerPredictionKey);
ScopedPredictionKey.Pop();
}
三、预测系统的深层机制
3.1 预测范围与作用域
GAS中预测系统的一个重要概念是"预测作用域":
// 预测作用域栈
struct FScopedPredictionWindow
{
UAbilitySystemComponent* AbilitySystemComponent;
FScopedPredictionWindow(UAbilitySystemComponent* InASC, FPredictionKey InPredictionKey)
{
AbilitySystemComponent = InASC;
AbilitySystemComponent->ScopedPredictionKey.Push(InPredictionKey);
}
~FScopedPredictionWindow()
{
AbilitySystemComponent->ScopedPredictionKey.Pop();
}
};
这允许当前执行的代码块与特定预测键关联,使一系列嵌套操作都能追踪到同一预测上下文。
3.2 预测键的传播
复杂之处在于预测键如何在系统中传播:
// 当生成GameplayEffect时
void UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(...)
{
// 将当前作用域的预测键附加到效果
if (ScopedPredictionKey.IsValidForMorePrediction())
{
Spec.SetPredictionKey(ScopedPredictionKey.Current);
}
// 应用效果...
}
这确保了从能力激活、任务执行到效果应用的完整预测链。
四、两大预测子系统
GAS中实际上包含两个相互配合但实现不同的预测系统:
4.1 能力预测系统
管理能力的激活、执行和取消:
// 客户端本地激活能力并预测
void UAbilitySystemComponent::TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool InputPressed)
{
// 检查本地控制权
if (!IsOwnerActorAuthoritative() && ScopedPredictionKey.Current.bIsServerInitiated == false)
{
// 本地预测激活
FPredictionKey PredictionKey = GeneratePredictionKey();
PredictionKey.Base = LocalPredictionKeyBase;
// 标记预测键为活跃
AddReplicatedPredictionKey(PredictionKey);
// 发送到服务器
ServerTryActivateAbility(AbilityToActivate, InputPressed, PredictionKey);
// 本地立即执行(预测)
InternalTryActivateAbility(AbilityToActivate, InputPressed, &PredictionKey);
}
else
{
// 服务器或已授权客户端,直接执行
InternalTryActivateAbility(AbilityToActivate, InputPressed, nullptr);
}
}
4.2 GameplayEffect预测系统
负责属性修改和状态效果的预测:
// 在应用GameplayEffect时
FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(...)
{
if (Spec.GetPredictionKey().IsValidForMorePrediction())
{
if (Spec.GetPredictionKey().bIsServerInitiated == false)
{
// 这是本地预测应用的效果
ActiveEffectHandle.CustomPredictionKey = Spec.GetPredictionKey();
// 添加到预测效果列表中,用于后续服务器确认
PredictedGameplayEffects.Add(Spec.GetPredictionKey().Current, ActiveEffectHandle);
}
}
// 应用实际效果...
}
五、预测确认与回滚机制
5.1 预测确认流程
当服务器确认结果返回时:
// 处理预测键的复制
void UAbilitySystemComponent::OnRep_ReplicatedPredictionKey()
{
// 遍历复制的预测键
for (auto& PredictionKeyElement : ReplicatedPredictionKeyMap)
{
FPredictionKeyDelegates* KeyDelegates = PredictionKeyDelegatesMap.Find(PredictionKeyElement.Key);
if (KeyDelegates)
{
// 执行所有等待该预测键的回调
KeyDelegates->BroadcastOnConfirm();
// 清理委托
PredictionKeyDelegatesMap.Remove(PredictionKeyElement.Key);
}
}
}
5.2 能力的回滚与确认
// 处理能力激活失败
void UAbilitySystemComponent::ClientActivateAbilityFailed(FGameplayAbilitySpecHandle Handle, int16 PredictionKey)
{
// 查找预测键对应的能力激活
FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
if (Spec)
{
// 遍历活跃的能力实例
for (int32 InstanceIdx = 0; InstanceIdx < Spec->ActiveCount; ++InstanceIdx)
{
if (Spec->ActivationInfo.PredictionKeyWhenActivated.Current == PredictionKey)
{
// 找到了预测激活的能力,需要回滚
UGameplayAbility* AbilityInstance = Spec->GetPrimaryInstance();
if (AbilityInstance)
{
// 通知能力需要取消
AbilityInstance->K2_EndAbility();
}
// 减少活跃计数
Spec->ActiveCount--;
MarkAbilitySpecDirty(*Spec);
}
}
}
}
5.3 GameplayEffect的回滚
// 在复制GameplayEffect时
void FActiveGameplayEffectsContainer::OnRep_GameplayEffects()
{
// 处理新增的效果
for (int32 idx = 0; idx < GameplayEffects_Internal.Num(); ++idx)
{
FActiveGameplayEffect& Effect = GameplayEffects_Internal[idx];
// 检查是否是预测的效果被确认
if (Effect.PredictionKey.IsValidKey())
{
FPredictionKeyDelegates* KeyDelegates = Owner->GetPredictionKeyDelegatesMap().Find(Effect.PredictionKey);
if (KeyDelegates)
{
// 执行确认委托
KeyDelegates->BroadcastOnConfirm();
}
// 从预测列表中移除
Owner->RemovePredictedGameplayEffect(Effect.Handle);
}
}
// 移除本地预测但未被服务器确认的效果
for (auto& PredictionPair : Owner->GetPredictedGameplayEffects())
{
// 检查是否超过了预测容忍时间
if (ShouldCancelPredictedEffect(PredictionPair.Key, PredictionPair.Value))
{
// 本地预测的效果没有被服务器确认,需要回滚
InternalRemoveActiveGameplayEffect(PredictionPair.Value);
}
}
}
六、属性预测的特殊处理
属性预测是GAS预测系统中最复杂的部分:
// 在AttributeSet中预测属性变化
void UAbilitySystemComponent::SetNumericAttributeBase(FGameplayAttribute Attribute, float NewValue)
{
// 检查是否在预测上下文中
if (ScopedPredictionKey.IsValidForMorePrediction() && ScopedPredictionKey.Current.bIsServerInitiated == false)
{
// 记录原始值用于可能的回滚
float CurrentValue = GetNumericAttribute(Attribute);
FPredictiveAttributeData NewData;
NewData.Attribute = Attribute;
NewData.OldValue = CurrentValue;
NewData.NewValue = NewValue;
// 保存预测数据
PredictiveAttributeChanges.Add(ScopedPredictionKey.Current, NewData);
// 设置临时预测值
UAttributeSet* AttributeSet = GetAttributeSubobjectChecked(Attribute.GetAttributeSetClass());
AttributeSet->SetNumericValue(Attribute, NewValue);
}
else
{
// 服务器或已授权客户端,直接设置
UAttributeSet* AttributeSet = GetAttributeSubobjectChecked(Attribute.GetAttributeSetClass());
AttributeSet->SetNumericValue(Attribute, NewValue);
}
}
七、实际应用中的复杂性
7.1 预测容错与同步处理
实际网络环境下,预测系统必须处理各种边缘情况:
// 处理过期预测键
void UAbilitySystemComponent::HandleStalePredictionKeys()
{
// 遍历所有预测键
for (auto It = PredictionKeyMap.CreateIterator(); It; ++It)
{
FPredictionKey& Key = It.Value();
// 检查是否已过时
if (Key.bIsStale || HasPredictionKeyBeenRejected(Key))
{
// 处理过期预测键
CancelAbilitiesByPredictionKey(Key);
RemovePredictedGameplayEffectsByPredictionKey(Key);
// 移除过期键
It.RemoveCurrent();
}
}
}
7.2 多级预测依赖
在复杂能力设计中,一个预测操作可能触发多个连锁操作:
// 一个典型的预测链示例
void UMyGameplayAbility::ActivateAbility(...)
{
// 使用当前预测键执行任务
UAbilityTask_PlayMontageAndWait* Task = UAbilityTask_PlayMontageAndWait::PlayMontageAndWaitForEvent(this, NAME_None, MontageToPlay);
Task->On