UE5 GAS 预测框架解析


文章目录

cpp 复制代码
// 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(预测键)

cpp 复制代码
// 预测键示例
FPredictionKey PredictionKey;

// 客户端生成新的预测键
PredictionKey = FPredictionKey::CreateNewPredictionKey(AbilitySystemComponent);

// 在技能激活时使用
void UMyGameplayAbility::ActivateAbility()
{
    // 获取当前预测键
    FPredictionKey CurrentKey = GetCurrentPredictionKey();
    
    if (CurrentKey.IsValidKey() && CurrentKey.IsLocalClientKey())
    {
        // 在这个预测窗口内执行预测操作
        ApplyPredictiveGameplayEffect();
        ExecutePredictiveGameplayCue();
    }
}

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

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

cpp 复制代码
// 预测键委托管理示例
FPredictionKeyDelegates& Delegates = FPredictionKeyDelegates::Get();

// 注册预测失败时的回滚逻辑
Delegates.NewRejectedDelegate(PredictionKey.Current).BindLambda([]()
{
    // 回滚预测的属性修改
    RollbackAttributeChanges();
    // 停止预测的特效
    StopPredictiveEffects();
});

// 注册预测确认时的清理逻辑
Delegates.NewCaughtUpDelegate(PredictionKey.Current).BindLambda([]()
{
    // 清理预测的临时效果
    CleanupPredictiveEffects();
});

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

cpp 复制代码
// 在技能中使用作用域预测窗口
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();
}

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

cpp 复制代码
// 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预测实现

cpp 复制代码
// 应用预测的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);
    }
}

六、属性预测的具体实现

cpp 复制代码
// 属性集的复制通知
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的复制机制

cpp 复制代码
// 服务器端确认预测键
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. 选择性复制:预测键只复制给相关的客户端

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

相关推荐
努力的小钟4 小时前
UE5GAS GameAbility源码解析 CommitAbility
ue5
CandyU24 小时前
UE5 小知识点 —— 08 - 摄像机小问题
ue5
m0_552200824 小时前
《UE5_C++多人TPS完整教程》学习笔记59 ——《P60 投射物武器(Projectile Weapons)》
c++·游戏·ue5
玉龙20254 小时前
使用虚幻引擎|UE5制作自动开关门
ue5·游戏引擎·虚幻·虚幻引擎教程
windyjl14 小时前
UE5框选提示UI与目标对齐
ui·ue5
m0_5522008214 小时前
《UE5_C++多人TPS完整教程》学习笔记58 ——《P58 旋转奔跑动画(Rotate Running Animations)》
c++·游戏·ue5
UsagiKnight2 天前
UE5小游戏开发 - 武士决斗
ue5·游戏引擎·游戏程序
m0_552200822 天前
《UE5_C++多人TPS完整教程》学习笔记55 ——《P56 网络更新频率(Net Update Frequency)》
c++·游戏·ue5
每天回答3个问题10 天前
UE5C++编译遇到MSB3073
开发语言·c++·ue5