文章目录
- 一、FPredictionKey(预测键)
- 二、预测键的生命周期管理
- 三、FScopedPredictionWindow(作用域预测窗口)
- 四、完整的技能预测流程示例
- 五、GameplayEffect预测实现
- 六、属性预测的具体实现
- 七、FReplicatedPredictionKeyMap的复制机制
- 关键设计思想总结:
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);
}
关键设计思想总结:
- 预测键匹配:客户端和服务器使用相同的预测键来标识相关操作
- 作用域管理:通过FScopedPredictionWindow确保预测键的正确传播
- 委托系统:使用委托来处理预测成功/失败的回调
- Delta预测:属性预测基于差值而非绝对值
- 效果转换:瞬时效果在预测时转换为持续效果以便回滚
- 选择性复制:预测键只复制给相关的客户端
这个系统解决了网络游戏中的核心问题:让客户端能够预先执行操作,同时在服务器验证后能够正确协调客户端和服务器状态,提供流畅的玩家体验。