UE 网络同步进阶之路

前言

本文目的在于记录一些网络同步的进阶操作

1.PushModel

在UE的网路同步框架中,Server会定期检测Actor的变量是否发生变化,如果变量发生变化,那么就会触发变量同步逻辑。在同步量较小时,不会出现性能问题。但当游戏包含大量的Actor和变量需要同步到多个客户端时,这就会成为一个性能瓶颈。

PushModel的目的是降低Server因检测变量是否变化带来的性能损耗,让开发者主动标记变量为dirty,告知服务器变量发生变化从而进行同步逻辑。

1.1 开启PushLodel

视引擎版本不同,有些引擎版本会默认打开PushModel,有些则是关闭的。
第一步先在Target.cs文件中设置bWithPushModel=true。
在这里插入图片描述
然后在DefaultEngine.ini文件中设置net.IsPushModelEnabled=1
在这里插入图片描述
随后需要引入PushModel模块,在build.cs文件中引入NetCore模块。
在这里插入图片描述

1.2 变量使用PushModel管理

在GetLifetimeReplicatedProps使用DOREPLIFETIME_WITH_PARAMS_FAST或者DOREPLIFETIME_WITH_PARAMS来开启PushModel。
在这里插入图片描述

1.3 标记变量为dirty

一共有6个宏用于标记变量为dirty,最常用的为下面三个。

#else // PushModel.h

#define MARK_PROPERTY_DIRTY(Object, Property) 
#define MARK_PROPERTY_DIRTY_STATIC_ARRAY_INDEX(Object, RepIndex, ArrayIndex) 
#define MARK_PROPERTY_DIRTY_STATIC_ARRAY(Object, RepIndex, ArrayIndex) 

// 标记动态数组、结构体、对象、普通变量
#define MARK_PROPERTY_DIRTY_FROM_NAME(ClassName, PropertyName, Object) 
// 标记静态数组的某个元素(对于静态数组,每一个元素都有独立的ReplIndex, 所以可以只标记其中某一个元素发生了改变)
#define MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY_INDEX(ClassName, PropertyName, ArrayIndex, Object) 
// 标记静态数组发生了改变
#define MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY(ClassName, PropertyName, ArrayIndex, Object) 

1.4 补充

标记变量为dirty之后也会进行变量比较,会判断变量是否真的发生了改变,若改变才会进行同步。

2.UStruct网络同步

UStruct中的变量默认是同步的,在实际项目中一些变量无需进行网络同步,这时候需要手动标记NotReplicated
在这里插入图片描述
UStruct可以自定义网络序列化逻辑,能够通过逻辑优化流量大小。需要定义bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)函数并重写TStructOpsTypeTraits

USTRUCT(BlueprintType)
struct FRepTestStruct
{
	GENERATED_BODY()

	UPROPERTY(BlueprintReadWrite,EditAnywhere)
	bool bBoolTest = false;

	UPROPERTY(NotReplicated)
	int32 IntNotRepTest = -1;

	bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess);
};

template<>
struct TStructOpsTypeTraits<FRepTestStruct> : public  TStructOpsTypeTraitsBase2<FRepTestStruct>
{
	enum
	{
		WithNetSerializer = true,
	};
};
bool FRepTestStruct::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
	// 不使用Ar << bBoolTest, 这样会传32bit数据, 下面只会传1bit
	Ar.SerializeBits(&bBoolTest,1);
	return true;
}

3.Fast TArray Replication

当一个TArray的数据需要频繁改变且这个TArray的内容很多时,普通TArray的同步策略会带来很多性能问题。Fast TArray是对这一情况的缓解措施,缺点是逻辑较复杂,需要手动将数组中的Item标记为dirty,并且不能保证Client与Server数组的“顺序”是相同的。Fast TArray的使用如下,详情请参考FastArraySerializer.h。
首先需要将数组中的数据封装成一个Struct,Struct需要继承于FFastArraySerializerItem

/** Step 1: Make your struct inherit from FFastArraySerializerItem */
USTRUCT(BlueprintType)
struct FExampleArrayItemFA : public FFastArraySerializerItem
{
	GENERATED_USTRUCT_BODY()
	
	UPROPERTY(BlueprintReadWrite)
	int32 ExampleIntProperty = -1;	

	UPROPERTY(BlueprintReadWrite)
	float ExampleFloatProperty = 0.f;
	
	// 可选的函数实现,当数组元素发生变化时,Client同步到此变化会调用如下的函数(不是虚函数,通过模板调用)
	// 在这些函数中修改数组的内容是不安全的。这些函数在更新时被逐个调用,可能在大规模更新之中被调用,所以不能保证数组的内容是最新的。
	void PreReplicatedRemove(const struct FExampleArrayFA& InArraySerializer);
	void PostReplicatedAdd(const struct FExampleArrayFA& InArraySerializer);
	void PostReplicatedChange(const struct FExampleArrayFA& InArraySerializer);

	// 可选的Debug string, 在LogNetFastTArray(log or lower verbosity)
	FString GetDebugString();
};

然后需要将数组封装到一个Struct中,Struct需要继承于FFastArraySerializer

/** Step 2: You MUST wrap your TArray in another struct that inherits from FFastArraySerializer */
USTRUCT(BlueprintType)
struct FExampleArrayFA: public FFastArraySerializer
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY()
	TArray<FExampleArrayItemFA> Items;/** Step 3: You MUST have a TArray named Items of the struct you made in step 1. */
	
	/** Step 4: Copy this, replace example with your names */
	bool NetDeltaSerialize(FNetDeltaSerializeInfo & DeltaParms)
	{
		return FFastArraySerializer::FastArrayDeltaSerialize<FExampleArrayItemFA, FExampleArrayFA>( Items, DeltaParms, *this );
	}
};
/** Step 5: Copy and paste this struct trait, replacing FExampleArrayFA with your Step 2 struct. */
template<>
struct TStructOpsTypeTraits< FExampleArrayFA > : public TStructOpsTypeTraitsBase2< FExampleArrayFA >
{
	enum 
	{
		WithNetDeltaSerializer = true,
   };
};

最后就是在你需要的地方创建变量,在GetLifetimeReplicatedProps函数中声明为同步变量(不用PushModel)。当你修改数组内容时,记得标记数组为dirty。

UPROPERTY(BlueprintReadWrite,Replicated)
FExampleArrayFA ExampleArrayFA;

UFUNCTION(BlueprintCallable)
void AddExampleArrayFAItem(FExampleArrayItemFA AddItem);

UFUNCTION(BlueprintCallable)
void ChangeExampleArrayFAItem(int32 index);

UFUNCTION(BlueprintCallable)
void RemoveExampleArrayFAItem(int32 index);
void ARepTestActor::AddExampleArrayFAItem(FExampleArrayItemFA AddItem)
{
	if(!HasAuthority())
	{
		return;
	}
	ExampleArrayFA.Items.Add(AddItem);
	ExampleArrayFA.MarkArrayDirty();
}

void ARepTestActor::ChangeExampleArrayFAItem(int32 index)
{
	if(!ExampleArrayFA.Items.IsValidIndex(index) || !HasAuthority())
	{
		return;
	}
	FExampleArrayItemFA &Item = ExampleArrayFA.Items[index];
	Item.ExampleIntProperty = Item.ExampleIntProperty + 1;
	Item.ExampleFloatProperty = Item.ExampleFloatProperty + 2.0f;
	ExampleArrayFA.MarkItemDirty(Item);
}

void ARepTestActor::RemoveExampleArrayFAItem(int32 index)
{
	if(!ExampleArrayFA.Items.IsValidIndex(index) || !HasAuthority())
	{
		return;
	}
	ExampleArrayFA.Items.RemoveAt(index);
	ExampleArrayFA.MarkArrayDirty();
}

Step 6 and beyond:

  • Declare a UPROPERTY of your FExampleArray (step 2) type.
  • You MUST call MarkItemDirty on the FExampleArray when you change an item in the array. You pass in a reference to the item you dirtied.
  • You MUST call MarkArrayDirty on the FExampleArray if you remove something from the array.
  • In your classes GetLifetimeReplicatedProps, use DOREPLIFETIME(YourClass, YourArrayStructPropertyName);

4.条件属性复制

合理配置变量的同步Condition是降低网络流量的重要手段,开发中可能会使用到的condition如下

	COND_None = 0							UMETA(DisplayName = "None"),							// This property has no condition, and will send anytime it changes
	COND_InitialOnly = 1					UMETA(DisplayName = "Initial Only"),					// This property will only attempt to send on the initial bunch
	COND_OwnerOnly = 2						UMETA(DisplayName = "Owner Only"),						// This property will only send to the actor's owner
	COND_SkipOwner = 3						UMETA(DisplayName = "Skip Owner"),						// This property send to every connection EXCEPT the owner
	COND_SimulatedOnly = 4					UMETA(DisplayName = "Simulated Only"),					// This property will only send to simulated actors
	COND_AutonomousOnly = 5					UMETA(DisplayName = "Autonomous Only"),					// This property will only send to autonomous actors
	COND_SimulatedOrPhysics = 6				UMETA(DisplayName = "Simulated Or Physics"),			// This property will send to simulated OR bRepPhysics actors
	COND_InitialOrOwner = 7					UMETA(DisplayName = "Initial Or Owner"),				// This property will send on the initial packet, or to the actors owner
	COND_Custom = 8							UMETA(DisplayName = "Custom"),							// This property has no particular condition, but wants the ability to toggle on/off via SetCustomIsActiveOverride

其功能大部分见名思意,这里说一下笔主认为比较混淆的地方。

4.1 COND_OwnerOnly 与 COND_AutonomousOnly

一个可同步的Actor必须归属于某个PlayerController的连接,在这种情况下COND_OwnerOnly与 COND_AutonomousOnly的作用似乎是一样的,但在实际开发中会发现一些差异。查阅资料得到这一见解:

COND_AutonomousOnly is limited to pawn that are directly controlled by the PlayerController.
COND_OwnerOnly, is qualified for all actor whose most outer owner is PlayerController.
For example, a player can use a skill to deploy a turret. And We want HP of the turret to be known only to the player who create it (and server). We can implement it by setting the turret’s owner to the playerCharacter, and set the HP attribute to COND_OwnerOnly. If we set it to COND_AutonomousOnly, then HP will not replicate to the client create the turret because the playercontroller doesn’t directly control the turret.

大致意思是COND_AutonomousOnly只适用于PlayerController直接控制的pawn,而COND_OwnerOnly,适用于所有外部Owner为PlayerController的Actor。笔者尝试了了一下COND_AutonomousOnly在PlayerState不起作用,在Pawn所属的Component起作用。

4.2 COND_Custom

官方文档原话:该属性没有特定条件,但需要通过 SetCustomIsActiveOverride 得到开启/关闭能力。具体做法如下:

void ARepTestCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	
	DOREPLIFETIME_CONDITION( ARepTestCharacter, CustomTest, COND_Custom );
}

void ARepTestCharacter::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
	Super::PreReplication(ChangedPropertyTracker);
	
	UE::Net::Private::FNetPropertyConditionManager::Get().SetPropertyActiveOverride(ChangedPropertyTracker,this,
		GetReplicatedProperty(StaticClass(), ARepTestCharacter::StaticClass(), GET_MEMBER_NAME_CHECKED(ARepTestCharacter, CustomTest))->RepIndex,
		CustomTest%2 == 0);
	/*ChangedPropertyTracker.SetCustomIsActiveOverride(this,
		GetReplicatedProperty(StaticClass(), ARepTestCharacter::StaticClass(),GET_MEMBER_NAME_CHECKED(ARepTestCharacter, CustomTest))->RepIndex,
		CustomTest%2 == 0);*/
}

SetCustomIsActiveOverride函数在5.2被标记为废弃,改用SetPropertyActiveOverride代替。个人感觉这个函数不好用,推荐使用DOREPLIFETIME_ACTIVE_OVERRIDEDOREPLIFETIME_ACTIVE_OVERRIDE_FAST宏。

DOREPLIFETIME_ACTIVE_OVERRIDE( ARepTestCharacter, CustomTest, CustomTest%2 == 0 );

这个宏的写法简洁了许多,而且不仅用于COND_Custom,其它COND也可使用

4.3 COND_InitialOnly

官方文档原话:该属性仅在初始数据组尝试发送。实践下来有以下几点注意事项:

  • 必须在Actor生成那一瞬间对属性进行初始化才会触发同步,后续对属性的任何更改都不会同步
  • COND_InitialOnly会同步给新加入的Client,如果InitialOnly变量随时间变化,则在不同时间加入的客户端将获得不同的值。

5.UObjec同步

UObjec必须作为Actor的子对象才能实现网络同步功能,同步的代码实现和Actor一样。开启UObject的网络同步额外操作如下:

// step.1 在UObject中重写IsSupportedForNetworking()函数
virtual bool IsSupportedForNetworking() const override { return true; }

// step.2 将UObject挂载到Actor上
UPROPERTY(Replicated)
TArray<URepSubObject*> RepSubObjectContainer;

// step.3 在Actor的GetLifetimeReplicatedProps声明同步属性
constexpr FDoRepLifetimeParams SharedParams{ COND_None, REPNOTIFY_Always, true };
DOREPLIFETIME_WITH_PARAMS(ARepTestActor, RepSubObjectContainer, SharedParams);

// step.4 在Actor中重写ReplicateSubobjects函数
bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
	bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);

	for (URepSubObject* SubAction : RepSubObjectContainer)
	{
		if (SubAction != nullptr)
		{	
			WroteSomething |= Channel->ReplicateSubobject(SubAction, *Bunch, *RepFlags);
		}
	}
	
	return WroteSomething;
}

6.Actor同步属性

Actor存在一些同步属性设置可以用于网络优化。

  • OnlyRelevantToOwner:是否只与Owner相关,Controller此项设置为true
  • AlwaysRelevant:总是相关(覆盖OnlyRelevantToOwner),GameState与PlayerState此项为true,若同时继承于AInfo,则可达到无视距离进行网络同步。
  • NetLoadonClient:Actor是否会在Client加载,当Client不需要此Actor时可设置为true,GameMode此项设置为false。
  • NetDormancy:网络休眠,当Actor休眠时不会接受网络更新消息,除非手动调用FlushNetDormancy或ForceNetUpdate。
  • NetUpdateFrequency:Actor的网络更新频率,因视情况而定适当降低此频率。默认值为100,但会小于NetServerMaxTickRate。
  • NetCullDistanceSquared:网络剔除距离平方。在Client中,玩家控制的Pawn此距离之外的Actor会被停止网络同步。
  • NetPriority:网络同步优先级,高优先级的Actor会被考虑优先同步。Controller与Pawn设置为3.0。
  • AdaptiveNetUpdateFrequency:此项不是Actor的属性,是一个引擎自带功能(net.UseAdaptiveNetUpdateFrequency),默认关闭。作用是自动调整Actor的网络更新频率。

7.Mix

  • FVector的同步优化:FVector使用double存储每个分量,在很多情况下并不会用到这么多的位数,可考虑使用NetQuantize优化。
    • FVector_NetQuantize:无小数
    • FVector_NetQuantize10:1位小数
    • FVector_NetQuantize100:2位小数
    • FVector_NetQuantizeNormal:归一化向量
  • 避免将FName变量进行网络同步。

参考

  1. Unreal Engine 4. New network model: PushModel
  2. PushModel.h
  3. NetSerialization.h
  4. Fast tarray replication
  5. FastArraySerializer.h
  6. 容器在GamePlay中的属性同步
  7. 条件属性复制
  8. In What Situation Is There A Difference Between COND_OwnerOnly and COND_AutonomousOnly?
  9. New blog post: Network Tips and Tricks
  10. COND_InitialOnly replicates to newly connected clients?
  11. ue4 网络的最佳实践
  12. Networking in UE4: Server Optimizations | Live Training | Unreal Engine
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MustardJim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值