UE5中的预测键与预测系统深入分析

一、预测系统的核心概念

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 基本流程概述

从高层次看,预测系统的工作流程如下:

  1. 客户端预测

    • 客户端生成预测键

    • 执行操作并记录预测键

    • 发送操作请求和预测键给服务器

  2. 服务器处理

    • 接收请求和预测键

    • 执行实际操作

    • 将结果和预测键返回给客户端

  3. 客户端确认

    • 接收服务器结果

    • 查找匹配的预测键

    • 如果预测正确,保留结果;如不正确,回滚并应用服务器结果

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值