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. 选择性复制:预测键只复制给相关的客户端

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

相关推荐
哎呦哥哥和巨炮叔叔2 小时前
虚幻引擎 5.5 能否取代 V-Ray?现代建筑可视化渲染技术对比解析
ue5·实时渲染·虚幻引擎5·建筑可视化·渲染101云渲染·v-ray渲染·建筑效果图
zhangzhangkeji4 小时前
UE5 多线程(4):资源竞争与原子变量。UE 建议使用 STL版本的原子量,不用自己版本的原子量 TAtomic<T> 的实现了
ue5
AI视觉网奇4 小时前
ue slot 插槽用法笔记
笔记·学习·ue5
lllljz4 小时前
Blender导出模型到Unity或UE5引擎材质丢失模型出错
unity·ue5·游戏引擎·blender·材质
AI视觉网奇4 小时前
blender fbx 比例不对 比例调整
笔记·学习·ue5
哎呦哥哥和巨炮叔叔5 小时前
Unreal Engine 是否支持光线追踪?UE5 光线追踪原理与性能解析
ue5·unreal engine·光线追踪·lumen·实时渲染·渲染101云渲染·ue云渲染
zhangzhangkeji5 小时前
UE5 多线程(3):线程退出与单例线程
ue5
AI视觉网奇5 小时前
static mesh 转skeleton mesh
笔记·学习·ue5
AI视觉网奇1 天前
metahuman 购买安装记录
笔记·学习·ue5
速冻鱼Kiel1 天前
虚幻状态树解析
ue5·游戏引擎·虚幻