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

UE5 Gameplay Ability System (GAS) 的缺点与不足:源码层面分析

1. 学习曲线与复杂性问题

GAS的最大问题可能是其陡峭的学习曲线和复杂的架构设计。从源码层面观察:

// AbilitySystemComponent.h - 仅这一个头文件就超过7000行代码  
class GAMEPLAYABILITIES_API UAbilitySystemComponent : public UGameplayTasksComponent  
{  
    // 大量的内部系统、缓存和状态追踪机制  
    TArray<FGameplayAbilitySpecHandle> ActivatableAbilities;  
    FActiveGameplayEffectsContainer ActiveGameplayEffects;  
    FActiveGameplayEffectHandleMap GEHandleMap;  
    TMap<FGameplayTag, FGameplayEffectQueryHandle> EffectQueriesMap;  
    // ...数十个额外的系统组件  
}  

底层设计问题:

  • 系统组件高度耦合,单个系统理解需要掌握整体架构

  • 缺乏清晰的分层架构,导致即使简单功能也需要大量样板代码

实际影响:
在单机和联网游戏中,这种复杂性导致开发者需要更长时间实现基础功能,并且错误调试极为困难。

2. 网络复制与预测系统的缺陷

GAS的网络复制系统非常复杂,尤其是预测系统:

// AbilitySystemComponent.cpp 中的预测处理  
void UAbilitySystemComponent::ReplicatedPredictionKeyStruct::PostReplicatedChange(...)  
{  
    // 复杂的回滚和确认逻辑  
    AbilitySystemComponent->OnPredictionKeyChange(OldData, NewData);  
}  

void UAbilitySystemComponent::OnPredictionKeyChange(...)  
{  
    // 处理预测键变化,通常涉及数百行复杂逻辑  
    // 处理预测的能力激活、GameplayEffect应用等  
}  

源码层面的具体问题:

  1. 服务器权威模型过于严格:几乎所有操作都需要服务器确认

  2. 预测回滚机制复杂且不完善

    • 能力预测系统与效果预测系统使用不同机制,导致预测逻辑不一致

    • 预测键管理繁琐,容易导致"ghost abilities"(预测能力未正确清理)

联网游戏中的表现:

  • 高延迟环境下玩家体验差,预测失败导致能力激活被取消

  • 回滚可能导致视觉"跳跃"和不连贯的游戏体验

3. 性能开销问题

GAS在性能方面存在显著开销:

// GameplayEffectExecutionCalculation.cpp  
void FGameplayEffectExecutionCalculation::Execute_Implementation(...) const  
{  
    // 大量的标签检查、属性捕获与修改操作  
    // 动态计算逻辑会导致高额CPU开销  
}  

从剖析数据看,性能瓶颈集中在:

  1. 标签匹配系统FGameplayTagContainer::HasAll/HasAny 在大量标签存在时效率低下

  2. 动态委托调用:GAS大量使用委托通知机制,导致间接调用开销

  3. 过度的GC压力:许多临时对象创建(如FGameplayEffectSpec)增加GC负担

在大型联网游戏中的影响:

  • 服务器面对大量玩家时,ASC(AbilitySystemComponent)更新成为明显瓶颈

  • 复杂效果计算可能导致帧率下降

4. 调试复杂度与工具不足

GAS调试工具相对有限:

// AbilitySystemComponent.cpp  
void UAbilitySystemComponent::PrintDebug()  
{  
    // 基础调试信息输出,但缺乏深度分析工具  
}  

底层设计缺陷:

  1. 缺少内置的可视化调试工具,特别是网络预测问题

  2. 错误信息往往过于技术化且不明确

  3. 大量使用模板和宏,增加了调试难度

实际开发影响:

  • 开发人员需创建大量自定义调试工具

  • 问题诊断周期长,特别是网络同步问题

5. 内部系统的松散集成

GAS由多个子系统组成,但这些系统集成度不高:

// 多个相关但分离的系统  
UAbilitySystemComponent          // 核心系统组件  
UGameplayAbility                 // 能力基类  
UGameplayEffect                  // 效果系统  
UAttributeSet                    // 属性管理  
UGameplayTask                    // 任务系统  
FGameplayCueParameters          // 游戏提示系统  

源码层面的问题:

  • 各子系统有各自的生命周期和更新逻辑,缺乏统一的更新流程

  • 系统间通信依赖显式代码,而非内置流程

实际游戏开发中:

  • 开发者经常需要创建"胶水代码"连接系统

  • 单机游戏中可能过度设计,引入不必要的复杂性

6. GameplayTags系统扩展性问题

标签系统是GAS的基础,但存在限制:

// GameplayTagsManager.cpp  
void UGameplayTagsManager::AddTagToINI(FString NewTagName)  
{  
    // 标签添加逻辑 - 只能在开发时编辑,运行时受限  
}  

底层限制:

  1. 标签修改需要引擎重启

  2. 运行时无法创建新标签

  3. 层级系统过度依赖命名约定

实际影响:

  • 难以实现动态内容系统

  • 标签管理随项目增长变得复杂

7. 蓝图支持和可访问性

尽管GAS提供蓝图支持,但它并不完全友好:

// GameplayAbilityBlueprint.h  
class GAMEPLAYABILITIES_API UGameplayAbilityBlueprint : public UBlueprint  
{  
    // 有限的蓝图API暴露  
}  

API设计问题:

  1. 许多核心功能仅在C++中可用

  2. 缺少直观的蓝图节点,需要大量自定义节点

  3. 错误处理差,蓝图调用常见空指针风险

单机开发中的影响:

  • 设计师无法充分利用GAS,降低迭代速度

  • 即使简单原型也需要C++支持

8. 与引擎其他系统的集成问题

GAS与引擎其他系统的集成不够无缝:

// AnimNotify_GameplayTagEvent.h - 尝试连接动画系统和GAS  
UCLASS(meta=(DisplayName="Gameplay Tag Event"))  
class ENGINE_API UAnimNotify_GameplayTagEvent : public UAnimNotify  
{  
    // 有限的集成代码  
}  

源码层面的缺陷:

  1. 与动画系统集成需要额外代码

  2. 与AI系统集成点少,需要自定义任务和服务

  3. 与物理系统交互需要手动桥接

实际开发中的表现:

  • 需要创建大量适配器代码

  • 系统交互处理不当容易引起性能问题

9. 可扩展性和定制化困难

修改GAS核心行为常常困难:

// GameplayEffect.h  
FActiveGameplayEffect::FActiveGameplayEffect(...)  
{  
    // 硬编码逻辑,难以覆盖或扩展  
}  

底层设计问题:

  1. 许多核心行为使用私有函数和成员,难以继承修改

  2. 过度依赖模板实现,限制运行时多态性

  3. 缺少明确的扩展点和插件架构

开发影响:

  • 常需要复制并修改引擎代码

  • 难以适应非标准游戏类型需求

10. 文档和样例不足

虽然不是代码缺陷,但文档不足严重影响使用:

// AbilitySystemGlobals.h  
/**   
 * For development and debugging. We may want to make this a config  
 * variable or a commandline switch.  
 */  
UPROPERTY()  
bool bTreatCreatePredictionKeyAsNonLocal;  

// 大量类似缺乏详细说明的变量和函数  

底层设计思路缺少解释:

  1. 设计意图和原理记录不足

  2. 缺少完整的API参考和最佳实践

  3. 示例代码覆盖不全面

结论与改进建议

从源码和设计角度,GAS面临的主要挑战是其固有的复杂性与为实现高度灵活性所做的妥协。理解这些缺点后,我提出以下改进建议:

  1. 简化核心API:创建分层API,允许简单用例使用简化接口

  2. 改进网络预测:重构预测系统,使其更加统一和可靠

  3. 性能优化:针对标签匹配和委托系统进行专项优化

  4. 增强调试工具:开发专用的GAS可视化调试工具

  5. 改进与引擎集成:提供更多与核心引擎系统的集成

UE5中的移动预测机制

UE5的移动预测系统确实与GAS(Gameplay Ability System)有相似之处,但实现机制有所不同。让我从底层架构解析UE5中客户端移动是如何预测并同步到服务器的。

基础架构:Character Movement Component

UE5的移动预测核心是UCharacterMovementComponent,它实现了客户端预测、服务器权威和回滚纠正的整套机制。

// UCharacterMovementComponent内部关键结构  
struct FNetworkPredictionData_Client_Character  
{  
    // 预测键相关  
    uint32 ClientUpdateNumber;    // 客户端移动更新计数  
    TArray<FSavedMovePtr> SavedMoves;  // 已发送但未确认的移动  
    FSavedMovePtr PendingMove;    // 待发送的移动  
    FSavedMovePtr LastAckedMove;  // 服务器最后确认的移动  
    // ...  
};  

客户端移动同步到服务器的流程

1. 输入收集与移动创建

每帧客户端处理输入并创建FSavedMove_Character对象:

void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)  
{  
    // 1. 收集当前输入状态  
    // 2. 创建并填充FSavedMove对象  
    FSavedMove_Character* NewMove = AllocateNewMove();  
    NewMove->SetMoveFor(CharacterOwner, DeltaTime, Acceleration, NewRotation);  
    // 3. 执行预测移动  
    PerformMovement(DeltaTime);  
    // 4. 保存移动到预测数据中  
    ClientData->PendingMove = NewMove;  
}  

2. 网络RPCs发送移动数据

UE使用特殊的RPC机制发送移动信息:

void UCharacterMovementComponent::CallServerMove()  
{  
    FSavedMovePtr LastClientMove = ClientData->SavedMoves.Last();  
    
    // 每个服务器移动请求包含:  
    // - 时间戳  
    // - 预测键(ClientUpdateNumber)  
    // - 加速度、旋转等移动参数  
    // - 位置(用于验证)  
    
    if (CanSendLastMove())  
    {  
        ServerMove(  
            LastClientMove->TimeStamp,  
            LastClientMove->Acceleration,  
            LastClientMove->GetCompressedFlags(),  
            LastClientMove->ClientUpdateNumber  // 这是预测键  
        );  
    }  
}  

3. 服务器验证与权威处理

服务器接收到移动请求后验证和应用:

void UCharacterMovementComponent::ServerMove_Implementation(float TimeStamp, FVector_NetQuantize10 InAccel, uint8 CompressedMoveFlags, uint32 ClientUpdateNumber)  
{  
    // 验证时间戳防止作弊  
    if (!IsValidTimeStamp(TimeStamp))  
    {  
        return;  
    }  
    
    // 应用移动  
    CharacterOwner->MoveAutonomous(TimeStamp, InAccel, CompressedMoveFlags);  
    
    // 记录最后确认的移动请求编号  
    ServerData->LastClientUpdateNumber = ClientUpdateNumber;  
    
    // 如必要,发送纠正信息  
    if (NeedsClientCorrection())  
    {  
        ClientAdjustPosition(CurrentServerTimeStamp, GetActorLocation(), GetActorRotation());  
    }  
}  

4. 客户端纠正机制

当客户端预测与服务器权威计算不匹配时,执行纠正:

void UCharacterMovementComponent::ClientAdjustPosition_Implementation(float TimeStamp, FVector NewLocation, FVector NewVelocity, UPrimitiveComponent* NewBase)  
{  
    // 1. 查找匹配的预测移动  
    // 2. 计算错误量  
    // 3. 应用纠正并重新应用后续所有待确认移动  
    
    // 错误超过阈值时,直接纠正  
    if (ClientError > AllowedError)  
    {  
        // 丢弃所有待确认移动并直接采用服务器状态  
        UpdateComponentVelocity();  
        // 平滑过渡到正确位置  
        SmoothCorrection(NewLocation);  
    }  
    else  
    {  
        // 重新应用所有待确认移动  
        ForcePositionUpdate(TimeStamp);  
        ReplayMoves(ClientData);  
    }  
}  

与GAS预测系统的区别与联系

虽然角色移动和GAS都使用预测键(prediction key)概念,但有几个关键差异:

  1. 细粒度不同

    • 移动预测通常每帧或固定间隔发送

    • GAS预测通常基于能力激活事件触发

  2. 数据结构不同

    • 移动使用FSavedMove_Character保存状态

    • GAS使用FPredictionKeyFGameplayAbilitySpec

  3. 网络优化

    • 移动预测专门设计了压缩算法减少带宽

    • 移动预测有批量处理机制(ServerMoveBatch)减少RPC调用

底层源码关键点

在引擎源码层面,以下是几个核心实现点:

  1. 预测键生成

// 在客户端生成唯一递增的预测键  
ClientData->ClientUpdateNumber++;  
  1. 移动数据压缩

// 使用量化和标志位压缩移动数据  
uint8 CompressedFlags = 0;  
if (bPressedJump) CompressedFlags |= FLAG_Jump;  
if (bWantsToCrouch) CompressedFlags |= FLAG_Crouch;  
// FVector_NetQuantize10用于位置压缩  
  1. 网络带宽优化

// 仅当必要时发送完整数据  
if (PendingMove->GetCompressedFlags() != LastAckedMove->GetCompressedFlags())  
{  
    // 发送更完整的状态  
    ServerMoveFull();  
}  
else  
{  
    // 发送增量更新  
    ServerMoveMinimal();  
}  
  1. 物理状态同步
    当使用物理模拟时,底层还会同步FRigidBodyState确保物理状态一致。

深入定制移动预测

如果需要扩展默认预测系统,可以:

  1. 派生自己的FSavedMove类:

class FMySavedMove : public FSavedMove_Character  
{  
    // 添加自定义状态  
    bool bMyCustomFlag;  

    virtual void SetMoveFor(...) override  
    {  
        Super::SetMoveFor(...);  
        // 保存自定义状态  
        bMyCustomFlag = MyCharacter->IsCustomFlagActive();  
    }  

    virtual void PrepareForReplay() override  
    {  
        Super::PrepareForReplay();  
        // 设置回放状态  
        MyCharacter->SetCustomFlag(bMyCustomFlag);  
    }  
};  
  1. 扩展UCharacterMovementComponent

UMyMovementComponent::AllocateNewMove()  
{  
    return new FMySavedMove();  
}  

高级网络优化考虑

  1. 客户端权威与服务器权威平衡
    可以通过修改ShouldUsePackedMovementRPCs()ClientAuthorativePosition调整。

  2. 移动补偿(Lag Compensation)
    UWorld::GetTimeSeconds()基础上,UE提供FRewindData支持服务器回滚验证。

  3. 智能合并与优先级
    可定制ProcessQueuedMoves()控制移动请求的优先级和合并策略。

UE5的移动预测系统是一个精心设计的客户端预测、服务器权威的混合系统,它平衡了响应性和安全性,同时通过复杂的带宽优化确保了多人游戏的可扩展性。

UE中UPROPERTY特性参数详解

UPROPERTY 是虚幻引擎反射系统的核心宏,用于声明类成员变量并赋予其特殊属性。ReplicatedUsing = OnRep_ReplicatedAttributes 是其中一种网络复制参数设置。

主要UPROPERTY特性参数分类

1. 网络复制相关

  • Replicated: 标记属性将从服务器复制到客户端

  • ReplicatedUsing = 函数名: 属性复制到客户端后会调用指定的回调函数

  • NotReplicated: 显式标记不复制(用于覆盖父类设置)

2. 编辑器可见性

  • EditAnywhere: 所有实例可编辑

  • EditDefaultsOnly: 仅原型(CDO)可编辑

  • EditInstanceOnly: 仅实例可编辑

  • VisibleAnywhere: 所有地方可见但不可编辑

  • VisibleDefaultsOnly: 仅原型可见

  • VisibleInstanceOnly: 仅实例可见

3. 蓝图交互

  • BlueprintReadWrite: 蓝图可读写

  • BlueprintReadOnly: 蓝图只读

  • BlueprintAssignable: 用于多播委托,可在蓝图中订阅

  • BlueprintCallable: 用于单播委托,可在蓝图中调用

  • BlueprintAuthorityOnly: 只在服务器上运行的代码可访问

4. 序列化控制

  • SaveGame: 属性会在保存游戏时序列化

  • Transient: 不会被序列化(不保存)

  • Config: 可从配置文件加载

  • DuplicateTransient: 复制对象时不会复制此属性

5. 高级特性

  • Meta=(参数列表): 提供元数据,如

    • Meta=(AllowPrivateAccess="true"): 允许私有成员在蓝图中访问

    • Meta=(ClampMin="0.0", ClampMax="1.0"): 值的范围限制

    • Meta=(EditCondition="bEnableSomething"): 条件编辑

  • Category="分类名": 在编辑器中的分类

  • Instanced: 对象被视为外部对象的一部分,而不是独立对象

6. 网络相关优先级

  • ReplicationCondition = COND_XXX: 复制条件

  • UPROPERTY(ReplicatedUsing=OnRep_X, RepNotify=XXX): 复制通知选项

复制示例代码

// 基本复制  
UPROPERTY(Replicated)  
float Health;  

// 复制带回调  
UPROPERTY(ReplicatedUsing=OnRep_CurrentHealth)  
float CurrentHealth;  

// 回调函数实现  
UFUNCTION()  
void OnRep_CurrentHealth();  

// 复制带元数据控制  
UPROPERTY(Replicated, Meta=(ClampMin="0.0", ClampMax="100.0"))  
float Stamina;  

官方文档链接

拓展提示

在GAS系统中,利用ReplicatedUsing属性可以精确控制何时更新客户端的属性值,并在客户端属性值变化时触发特定逻辑,比如UI更新、特效播放等。这是实现高响应性网络游戏的重要机制。

RepNotify

RepNotify

文章作者: 周几
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 周几小屋
喜欢就支持一下吧