UE5 GAS 预测框架解析 - 实践


// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Engine/NetDriver.h"
#include "Engine/NetSerialization.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "UObject/ObjectKey.h"
#include "Templates/TypeCompatibleBytes.h"
#include "GameplayPrediction.generated.h"
class UAbilitySystemComponent
;
namespace UE::Net
{
struct FPredictionKeyNetSerializer;
}
// 预测键事件委托
DECLARE_DELEGATE(FPredictionKeyEvent);
/**
* Gameplay Ability Prediction 系统概述
*
* 高层次目标:在GameplayAbility级别实现预测透明化...
* [这里省略了详细的设计文档注释]
*/
PRAGMA_DISABLE_DEPRECATION_WARNINGS // PredictiveConnection
/**
* FPredictionKey - 游戏能力系统中支持客户端预测的通用方式
* 本质上是一个ID,用于标识客户端上完成的预测性操作和副作用
*/
USTRUCT()
struct GAMEPLAYABILITIES_API FPredictionKey
{
GENERATED_USTRUCT_BODY()
typedef int16 KeyType;
FPredictionKey() = default;
/** 此预测键的唯一ID */
UPROPERTY()
int16 Current = 0;
/** 如果非0,表示创建此键的原始预测键(依赖链中) */
UPROPERTY(NotReplicated) // 不复制到客户端
int16 Base = 0;
/** 如果为true,表示这是服务器发起的激活键,用于标识服务器激活但不能用于预测 */
UPROPERTY()
bool bIsServerInitiated = false;
/** 创建没有依赖关系的新预测键 */
static FPredictionKey CreateNewPredictionKey(const UAbilitySystemComponent*);
/** 创建新的服务器发起键,用于服务器激活的能力 */
static FPredictionKey CreateNewServerInitiatedKey(const UAbilitySystemComponent*);
/** 创建新的依赖预测键:保持现有的base或使用当前键作为base */
void GenerateDependentPredictionKey();
/** 创建仅在此键被拒绝时调用的新委托 */
FPredictionKeyEvent&
NewRejectedDelegate();
/** 创建仅当复制状态追上此键时调用的新委托 */
FPredictionKeyEvent&
NewCaughtUpDelegate();
/** 添加新的委托,在键被拒绝或追上时调用 */
void NewRejectOrCaughtUpDelegate(FPredictionKeyEvent Event);
/** 网络序列化函数 */
bool NetSerialize(FArchive& Ar, class UPackageMap
* Map, bool& bOutSuccess);
/** 键有效当且仅当非零,其他客户端的预测键会序列化为0并无效 */
bool IsValidKey() const
{
return Current >
0;
}
/** 如果键有效且不是服务器键,则表示由本地客户端生成 */
bool IsLocalClientKey() const
{
return Current >
0 &&
!bIsServerInitiated;
}
/** 是否为服务器发起的激活键 */
bool IsServerInitiatedKey() const
{
return bIsServerInitiated;
}
/** 此键是否可用于更多预测操作,或者是否已发送到服务器 */
bool IsValidForMorePrediction() const
{
return IsLocalClientKey();
}
/** 此PredictionKey是否从NetSerialize接收或本地创建 */
bool WasReceived() const
{
return PredictiveConnectionObjectKey != FObjectKey();
}
/** 是否本地生成 */
bool WasLocallyGenerated() const
{
return (Current >
0) &&
(PredictiveConnectionObjectKey == FObjectKey());
}
/** 相等运算符,忽略Base因为它不被复制 */
bool operator==(const FPredictionKey& Other) const
{
return Current == Other.Current && bIsServerInitiated == Other.bIsServerInitiated;
}
bool operator!=(const FPredictionKey& Other) const
{
return !(*this == Other);
}
/** 转换为字符串表示 */
FString ToString() const
{
return bIsServerInitiated ? FString::Printf(TEXT("[Srv: %d]"), Current) : FString::Printf(TEXT("[%d/%d]"), Current, Base);
}
/** 哈希函数,忽略Base */
friend uint32 GetTypeHash(const FPredictionKey& InKey)
{
return ((InKey.Current <<
1) | (InKey.bIsServerInitiated &
1));
}
/** 获取预测连接键 */
uint64 GetPredictiveConnectionKey() const {
return BitCast<uint64>(PredictiveConnectionObjectKey);
  }
  private:
  friend UE::Net::FPredictionKeyNetSerializer;
  /** 生成新的预测键 */
  void GenerateNewPredictionKey();
  /** 显式构造函数 */
  explicit FPredictionKey(int32 Key)
  : Current(static_cast<KeyType>(Key))
    {
    check(Key >= 0 && Key <= std::numeric_limits<KeyType>
      ::max());
      }
      /** 在服务器上,唯一标识此键序列化所在/来自的网络连接 */
      FObjectKey PredictiveConnectionObjectKey;
      };
      PRAGMA_ENABLE_DEPRECATION_WARNINGS
      // 为FPredictionKey启用网络序列化特性
      template<
      >
      struct TStructOpsTypeTraits<FPredictionKey>
        : public TStructOpsTypeTraitsBase2<FPredictionKey>
          {
          enum
          {
          WithNetSerializer = true, // 启用网络序列化
          WithIdenticalViaEquality = true // 启用相等性比较
          };
          };
          // -----------------------------------------------------------------
          /**
          * FPredictionKeyDelegates - 用于注册与预测键拒绝和复制状态追赶相关的委托的数据结构
          * 应注册委托来撤销使用预测键创建的副作用
          */
          struct FPredictionKeyDelegates
          {
          public:
          /** 委托容器结构 */
          struct FDelegates
          {
          public:
          /** 当预测键关联的操作被服务器显式拒绝时调用此委托 */
          TArray<FPredictionKeyEvent> RejectedDelegates;
            /** 当复制状态追上预测键时调用此委托。不暗示拒绝或接受 */
            TArray<FPredictionKeyEvent> CaughtUpDelegates;
              };
              /** 委托映射表,按键值索引 */
              TMap<FPredictionKey::KeyType, FDelegates> DelegateMap;
                /** 获取全局单例实例 */
                static FPredictionKeyDelegates&
                Get();
                /** 为指定键创建新的拒绝委托 */
                static FPredictionKeyEvent&
                NewRejectedDelegate(FPredictionKey::KeyType Key);
                /** 为指定键创建新的追上委托 */
                static FPredictionKeyEvent&
                NewCaughtUpDelegate(FPredictionKey::KeyType Key);
                /** 为指定键添加新的拒绝或追上委托 */
                static void NewRejectOrCaughtUpDelegate(FPredictionKey::KeyType Key, FPredictionKeyEvent NewEvent);
                /** 广播拒绝委托 */
                static void BroadcastRejectedDelegate(FPredictionKey::KeyType Key);
                /** 广播追上委托 */
                static void BroadcastCaughtUpDelegate(FPredictionKey::KeyType Key);
                /** 拒绝指定键(触发拒绝委托) */
                static void Reject(FPredictionKey::KeyType Key);
                /** 追上指定键(触发追上委托) */
                static void CatchUpTo(FPredictionKey::KeyType Key);
                /** 添加依赖关系:此键依赖于另一个键 */
                static void AddDependency(FPredictionKey::KeyType ThisKey, FPredictionKey::KeyType DependsOn);
                };
                // -----------------------------------------------------------------
                // 前向声明
                class UAbilitySystemComponent
                ;
                class UGameplayAbility
                ;
                /** 预测键处理结果的枚举 */
                enum class EGasPredictionKeyResult
                : uint8
                {
                SilentlyDrop, // 静默丢弃键(完全不确认)
                Accept, // 接受键(例如服务器确认事件发生)
                Reject // 拒绝键(例如服务器说事件从未发生)
                };
                /**
                * FScopedPredictionWindow - 用于允许作用域内预测窗口的结构
                * 在预测性代码发生处调用,生成新的PredictionKey并作为客户端和服务器之间该键的同步点
                */
                struct GAMEPLAYABILITIES_API FScopedPredictionWindow
                {
                /**
                * 在服务器上调用,当从客户端接收到新的预测键时(在RPC中)
                * InSetReplicatedPredictionKey应设置为false,当我们想要作用域预测键但已经复制了预测键时
                */
                FScopedPredictionWindow(UAbilitySystemComponent* AbilitySystemComponent, FPredictionKey InPredictionKey, bool InSetReplicatedPredictionKey = true);
                /** 在预测性代码发生处调用,生成新的PredictionKey并作为同步点 */
                FScopedPredictionWindow(UAbilitySystemComponent* AbilitySystemComponent, bool CanGenerateNewKey=true);
                /** 析构函数,清理作用域 */
                ~FScopedPredictionWindow();
                private:
                /** 所有者AbilitySystemComponent的弱引用 */
                TWeakObjectPtr<UAbilitySystemComponent> Owner;
                  /** 是否清除作用域预测键 */
                  bool ClearScopedPredictionKey;
                  /** 是否设置复制的预测键 */
                  bool SetReplicatedPredictionKey;
                  /** 要恢复的键 */
                  FPredictionKey RestoreKey;
                  #if !UE_BUILD_SHIPPING
                  // 调试信息(非Shipping构建)
                  FOnSendRPC DebugSavedOnSendRPC;
                  TWeakObjectPtr<UNetDriver> DebugSavedNetDriver;
                    TOptional<FPredictionKey::KeyType> DebugBaseKeyOfChain;
                      #endif
                      };
                      /**
                      * FScopedDiscardPredictions - 丢弃此窗口内发生的预测
                      * 用于不打算将生成的预测键链发送到服务器的情况
                      */
                      struct GAMEPLAYABILITIES_API FScopedDiscardPredictions
                      {
                      /**
                      * 构造函数
                      * @param AbilitySystemComponent 要丢弃预测的ASC
                      * @param HowToHandlePredictions 如何处理预测事件(默认为静默丢弃)
                      */
                      explicit FScopedDiscardPredictions(UAbilitySystemComponent* AbilitySystemComponent, EGasPredictionKeyResult HowToHandlePredictions = EGasPredictionKeyResult::SilentlyDrop);
                      /** 析构函数,执行预测链的最终处理 */
                      ~FScopedDiscardPredictions();
                      private:
                      /** 所有者ASC的弱指针 */
                      TWeakObjectPtr<UAbilitySystemComponent> Owner;
                        /** 要在所有者上恢复的键 */
                        FPredictionKey KeyToRestoreOnOwner;
                        /** 如何处理预测链的结果 */
                        EGasPredictionKeyResult PredictionKeyChainResult;
                        /** 最终要根据PredictionKeyChainResult确认的基准键 */
                        FPredictionKey BaseKeyToAck;
                        };
                        // -----------------------------------------------------------------
                        // 前向声明
                        struct FReplicatedPredictionKeyMap;
                        /**
                        * FReplicatedPredictionKeyItem - 通过FastArray复制预测键到客户端的结构
                        * 每个预测键单独确认,而不是只复制"最高编号的键"
                        */
                        USTRUCT()
                        struct FReplicatedPredictionKeyItem : public FFastArraySerializerItem
                        {
                        GENERATED_USTRUCT_BODY()
                        // 允许复制构造函数和赋值操作
                        FReplicatedPredictionKeyItem();
                        FReplicatedPredictionKeyItem(const FReplicatedPredictionKeyItem& Other);
                        FReplicatedPredictionKeyItem(FReplicatedPredictionKeyItem&& Other);
                        FReplicatedPredictionKeyItem&
                        operator=(FReplicatedPredictionKeyItem&& other);
                        FReplicatedPredictionKeyItem&
                        operator=(const FReplicatedPredictionKeyItem& other);
                        /** 预测键 */
                        UPROPERTY()
                        FPredictionKey PredictionKey;
                        /** 复制后添加或改变时调用 */
                        void PostReplicatedAdd(const struct FReplicatedPredictionKeyMap &InArray) {
                        OnRep(InArray);
                        }
                        void PostReplicatedChange(const struct FReplicatedPredictionKeyMap &InArray) {
                        OnRep(InArray);
                        }
                        /** 获取调试字符串 */
                        FString GetDebugString() {
                        return PredictionKey.ToString();
                        }
                        private:
                        /** 复制回调处理 */
                        void OnRep(const struct FReplicatedPredictionKeyMap& InArray);
                        };
                        /**
                        * FReplicatedPredictionKeyMap - 预测键映射的FastArray序列化器
                        * 用于将预测键从服务器复制回客户端(通过属性复制)
                        */
                        USTRUCT()
                        struct FReplicatedPredictionKeyMap : public FFastArraySerializer
                        {
                        GENERATED_USTRUCT_BODY()
                        FReplicatedPredictionKeyMap();
                        /** 预测键数组 */
                        UPROPERTY()
                        TArray<FReplicatedPredictionKeyItem> PredictionKeys;
                          /** 复制预测键 */
                          void ReplicatePredictionKey(FPredictionKey Key);
                          /** 网络增量序列化 */
                          bool NetDeltaSerialize(FNetDeltaSerializeInfo & DeltaParms);
                          /** 获取调试字符串 */
                          FString GetDebugString() const;
                          /** 键环缓冲区大小 */
                          static const int32 KeyRingBufferSize;
                          };
                          // 为FReplicatedPredictionKeyMap启用网络增量序列化特性
                          template<
                          >
                          struct TStructOpsTypeTraits< FReplicatedPredictionKeyMap >
                            : public TStructOpsTypeTraitsBase2< FReplicatedPredictionKeyMap >
                              {
                              enum
                              {
                              WithNetDeltaSerializer = true, // 启用网络增量序列化
                              };
                              };

一、FPredictionKey(预测键)

// 预测键示例
FPredictionKey PredictionKey;
// 客户端生成新的预测键
PredictionKey = FPredictionKey::CreateNewPredictionKey(AbilitySystemComponent);
// 在技能激活时使用
void UMyGameplayAbility::ActivateAbility()
{
// 获取当前预测键
FPredictionKey CurrentKey = GetCurrentPredictionKey();
if (CurrentKey.IsValidKey() && CurrentKey.IsLocalClientKey())
{
// 在这个预测窗口内执行预测操作
ApplyPredictiveGameplayEffect();
ExecutePredictiveGameplayCue();
}
}

作用:唯一标识预测操作,解决"重做"问题(避免重复执行预测的效果)。

二、预测键的生命周期管理

// 预测键委托管理示例
FPredictionKeyDelegates& Delegates = FPredictionKeyDelegates::Get();
// 注册预测失败时的回滚逻辑
Delegates.NewRejectedDelegate(PredictionKey.Current).BindLambda([]()
{
// 回滚预测的属性修改
RollbackAttributeChanges();
// 停止预测的特效
StopPredictiveEffects();
});
// 注册预测确认时的清理逻辑
Delegates.NewCaughtUpDelegate(PredictionKey.Current).BindLambda([]()
{
// 清理预测的临时效果
CleanupPredictiveEffects();
});

三、FScopedPredictionWindow(作用域预测窗口)

// 在技能中使用作用域预测窗口
void UAbilityTask_WaitInputRelease::OnReleaseCallback()
{
// 创建新的预测窗口
FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent, true);
// 在这个作用域内的所有操作都会使用新的预测键
FPredictionKey NewKey = AbilitySystemComponent->
GetCurrentPredictionKey();
// 发送到服务器,使用相同的预测键
AbilitySystemComponent->
ServerInputRelease(NewKey);
// 服务器端也会在相同的作用域内执行,使用相同的预测键
}
// 服务器端对应的实现
void UAbilitySystemComponent::ServerInputRelease_Implementation(FPredictionKey PredictionKey)
{
// 设置作用域预测键
FScopedPredictionWindow ScopedPrediction(this, PredictionKey);
// 执行相同的逻辑,确保使用相同的预测键
OnInputRelease();
}

四、完整的技能预测流程示例

// 1. 客户端尝试激活技能
void UAbilitySystemComponent::TryActivateAbility(FGameplayAbilitySpecHandle Handle)
{
// 生成新的预测键
FPredictionKey PredictionKey = FPredictionKey::CreateNewPredictionKey(this);
// 发送到服务器
ServerTryActivateAbility(Handle, PredictionKey);
// 立即本地预测执行
InternalTryActivateAbility(Handle, PredictionKey);
}
// 2. 服务器验证并响应
void UAbilitySystemComponent::ServerTryActivateAbility_Implementation(FGameplayAbilitySpecHandle Handle, FPredictionKey PredictionKey)
{
if (CanActivateAbility(Handle))
{
// 激活成功,确认预测键
ClientActivateAbilitySucceed(Handle, PredictionKey);
InternalTryActivateAbility(Handle, PredictionKey);
}
else
{
// 激活失败,拒绝预测键
ClientActivateAbilityFailed(Handle);
FPredictionKeyDelegates::Reject(PredictionKey.Current);
}
}
// 3. 客户端处理响应
void UAbilitySystemComponent::ClientActivateAbilitySucceed_Implementation(FGameplayAbilitySpecHandle Handle, FPredictionKey PredictionKey)
{
// 等待属性复制来最终确认预测
// 当ReplicatedPredictionKeyMap复制下来时会触发CaughtUp委托
}
void UAbilitySystemComponent::ClientActivateAbilityFailed_Implementation(FGameplayAbilitySpecHandle Handle)
{
// 立即回滚预测效果
FPredictionKey CurrentKey = GetCurrentPredictionKey();
FPredictionKeyDelegates::Reject(CurrentKey.Current);
}

五、GameplayEffect预测实现

// 应用预测的GameplayEffect
void UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpec& Spec, FPredictionKey PredictionKey)
{
if (PredictionKey.IsValidKey() &&
!IsOwnerActorAuthoritative())
{
// 客户端预测应用
// 将瞬时效果转换为无限持续时间效果
if (Spec.GetDuration() == INSTANT_APPLICATION)
{
// 创建预测版本的GE
FGameplayEffectSpec PredictiveSpec = Spec;
PredictiveSpec.Duration = FGameplayEffectConstants::INFINITE_DURATION;
ApplyGameplayEffectSpecToSelf(PredictiveSpec, PredictionKey);
}
}
else if (IsOwnerActorAuthoritative())
{
// 服务器端应用,但设置相同的预测键
// 这样复制到客户端时可以进行匹配
InternalApplyGameplayEffectSpec(Spec, PredictionKey);
}
}

六、属性预测的具体实现

// 属性集的复制通知
UCLASS()
class UMyAttributeSet
: public UAttributeSet
{
GENERATED_BODY()
public:
UPROPERTY(ReplicatedUsing=OnRep_Health)
float Health;
// 必须使用REPNOTIFY_Always确保总是触发
UFUNCTION()
void OnRep_Health()
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health);
}
};
// 在AbilitySystemComponent中处理属性聚合
void UAbilitySystemComponent::ActiveGameplayEffects_OnAttributeChange(FGameplayAttribute Attribute, float NewValue)
{
// 检查是否有预测的效果在修改这个属性
if (HasPredictiveEffectModifyingAttribute(Attribute))
{
// 重新计算最终值,考虑预测的修改
float FinalValue = RecalculateAttributeWithPredictions(Attribute, NewValue);
SetAttributeValue(Attribute, FinalValue);
}
else
{
SetAttributeValue(Attribute, NewValue);
}
}

七、FReplicatedPredictionKeyMap的复制机制

// 服务器端确认预测键
void UAbilitySystemComponent::ReplicatePredictionKey(FPredictionKey Key)
{
if (IsOwnerActorAuthoritative())
{
// 添加到复制映射中
ReplicatedPredictionKeyMap.ReplicatePredictionKey(Key);
}
}
// 客户端处理复制过来的预测键
void FReplicatedPredictionKeyItem::OnRep(const FReplicatedPredictionKeyMap& InArray)
{
// 触发"追上"委托,表示服务器已经处理了这个预测键
FPredictionKeyDelegates::CatchUpTo(PredictionKey.Current);
// 清理对应的预测效果
RemovePredictiveEffectsForKey(PredictionKey);
}

关键设计思想总结:

  1. 预测键匹配:客户端和服务器使用相同的预测键来标识相关操作
  2. 作用域管理:通过FScopedPredictionWindow确保预测键的正确传播
  3. 委托系统:使用委托来处理预测成功/失败的回调
  4. Delta预测:属性预测基于差值而非绝对值
  5. 效果转换:瞬时效果在预测时转换为持续效果以便回滚
  6. 选择性复制:预测键只复制给相关的客户端

这个系统解决了网络游戏中的核心问题:让客户端能够预先执行操作,同时在服务器验证后能够正确协调客户端和服务器状态,提供流畅的玩家体验。

posted on 2025-10-10 14:36  ljbguanli  阅读(13)  评论(0)    收藏  举报