nicetry12138 c1e20016a1 feat: 添加 NetDriver 属性同步流程 há 4 meses atrás
..
Image c1e20016a1 feat: 添加 NetDriver 属性同步流程 há 4 meses atrás
README.md c1e20016a1 feat: 添加 NetDriver 属性同步流程 há 4 meses atrás

README.md

属性同步

简单使用

无论是 RPC 还是 属性赋值 都需要设置 Replicates 属性为 True

PawnCharacter 默认开启 ReplicatesActor 需要手动开启

如果想要属性同步,则需要设置属性的 Replication

枚举值 含义
None 不进行网络同步
Replicated 在服务器修改此值,会自动同步到客户端,但是有一定时间延迟
RepNotify 在服务器修改此值,会自动同步到客户端,但是有一定时间延迟,同时会自动调用回调函数

当选择 RepNotify 时,蓝图中会自动生成一个名为 OnRep_属性名 的一个函数,该函数作为回调函数

除了 Replication 之外,还有 ReplicationCondition 属性

ReplicationCondition 属性是 ELifetimeCondition 类型,用于精细控制属性复制的条件。定义了什么情况系啊服务器会将属性的更新发送给客户端

枚举值 作用
COND_None 无条件复制,属性每次变化时都会发送给所有相关客户端
COND_InitialOnly 仅限初始同步,属性只在 Actor 首次出现在客户端时发送(创建时),后续变化不再同步
COND_OwnerOnly 仅发送给拥有者,属性只同步给控制该 Actor 的客户端
COND_SkipOwner 排除拥有者,属性同步给所有客户端,除了拥有者
COND_SimulatedOnly 仅模拟客户端,只发送给非控制该 Actor 的客户端
COND_AutonomousOnly 仅自主客户端,只发送给控制该 Actor 的客户端
COND_SimulatedOrPhysics 模拟或物理Actor,发送给模拟客户端或启用了物理复制(bRepPhysics)的客户端
COND_InitialOrOwner 初始或拥有者,首次出现时发送给所有客户端,后续变化只发送给拥有者

还有很多其他的条件枚举,不一一列举

在 C++ 中如果想要定义某个属性需要被复制,则需要使用 Replicated

UPROPERTY(Replicated, ReplicatedUsing = OnRep_SpawnedAttributes, Transient)
TArray<TObjectPtr<UAttributeSet>>	SpawnedAttributes;

然后使用, 在 GetLifetimeReplicatedProps 函数中,定义哪些属性需要网络复制(Replicated)以及如何复制

对于需要复制的属性,通常需要 注册复制属性、配置复制规则

void UAbilitySystemComponent::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{	FDoRepLifetimeParams Params;
	Params.bIsPushBased = true;
	Params.Condition = COND_None;
	DOREPLIFETIME_WITH_PARAMS_FAST(UAbilitySystemComponent, SpawnedAttributes, Params);
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
}

除了 DOREPLIFETIME 之外,还有其他的,比如 DOREPLIFETIME_WITH_PARAMSDOREPLIFETIME_CONDITION 等。这些宏本质上就是将属性快速注册到 OutLifetimeProps 参数中

针对 SpawnedAttributes,当从服务器同步属性之后,会触发 ReplicatedUsing 配置的 OnRep_SpawnedAttributes 函数

NetDriver 同步流程

  1. 收集所有的 Actor
  2. 属性对比,对比哪些属性发生了变化
  3. 遍历 Component 并进行属性对比
  4. 遍历所有的 UActorChannel

引擎通过引入 ReplicationGraphNetDormancy 减少了 收集 Actor 阶段处理的 Actor 数量;通过 PushModel 减少了属性对比的执行次数

收集 Actor

在当前 UWorld::SpawnActor 的时候,会调用在最后调用 UWorld::AddNetworkActor 函数就该 Actor 通知给 UNetDriver

ForEachNetDriver(GEngine, this, [Actor](UNetDriver* const Driver)
{
	if (Driver != nullptr)
	{
		Driver->AddNetworkActor(Actor);
	}
});

最后所有的 Actor 都会存储在 UNetDriverNetworkObjects 容器中

GetNetworkObjectList().FindOrAdd(Actor, this);
if (ReplicationDriver)
{
	ReplicationDriver->AddNetworkActor(Actor);
}

ReplicationDriver 需要开启 Iris 才能使用,否则为 nullptr

https://dev.epicgames.com/documentation/zh-cn/unreal-engine/iris-replication-system-in-unreal-engine

开始处理复制

过滤/更新 ActorInfo

UNetDriver::TickFlush 时会调用 ServerReplicateActors 开启属性复制流程

首先通过 ServerReplicateActors_BuildConsiderListGetNetworkObjectList 中缓存的 Actor 进行过滤,剔除无效或者不用复制的 Actor

  1. 过滤 不强制更新 并且 ActorInfo->NextUpdateTime 时间未到到的 ActorInfo
  2. 过滤 PendingKill 的 Actor,会从 NetworkObjects 容器中删除
  3. 过滤 RemoteRole 为 ROLE_None 的 Actor,会从 NetworkObjects 容器中删除
  4. 过滤与当前运行的 NetDriver 不一直的 Actor
  5. 过滤还没有 Initialize 的 Actor
  6. 过滤 Actor 所属的 Level 正处于 In 或者 Out 的 Actor
  7. 过滤没有被唤醒的 Actor

每个 Actor 都被封装在 FNetworkObjectInfo 结构体中,除了 Actor 之外还有一些其他信息

FNetworkObjectInfo 属性 作用
Actor,WeakActor 对 Actor 的强引用和软引用
NextUpdateTime 下一次更新时间
LastNetReplicateTime 上一次复制时间
OptimalNetUpdateDelta 期望的复制间隔
LastNetUpdateTimestamp 上一次基于 NextUpdateTime 进入 要考虑复制 状态时的内部时间戳

通过前面过滤,得到有效的 Actor 之后,对 Actor 所属的 ActorInfo 进行数据处理

  1. 通过 LastNetReplicateTime 计算更新频率,更新后短期内保持高频更新,长期不更新则降低频率
  2. 计算 ActorInfo->NextUpdateTime 下一次更新时间
  3. 通知 Actor->CallPreReplication 要开始复制了

处理 NetConnection

核心处理函数是

  1. 优先级排序,ServerReplicateActors_PrioritizeActors
  2. 同步处理,ServerReplicateActors_ProcessPrioritizedActorsRange
  3. 标记相关Actor,ServerReplicateActors_MarkRelevantActors

不过对 NetConnection 进行处理需要满足条件

  1. if (i >= NumClientsToTick)
  2. if (Connection->ViewTarge)

由于 NetDriver 中包含多个 NetConnection,i 就是遍历 NetConnection 的序号,NumClientsToTick 表示本次 Tick 更新多少个 NetConnection,这是为了解决服务器性能不足时跳过部分客户端

由于决定哪些 Actor 需要被赋值时,需要 ViewTarget,所以会要求 ViewTarget 有效

通过 UNetConnection 来构建 FNetViewer,计算得到 视图源点的世界坐标位置 和 视图方向的单位向量

FNetViewer::FNetViewer(UNetConnection* InConnection, float DeltaSeconds) :
	Connection(InConnection),
	InViewer(InConnection->PlayerController ? InConnection->PlayerController : InConnection->OwningActor),
	ViewTarget(InConnection->ViewTarget),
{
	APlayerController* ViewingController = InConnection->PlayerController;

	// Get viewer coordinates.
	ViewLocation = ViewTarget->GetActorLocation();
	if (ViewingController)
	{
		FRotator ViewRotation = ViewingController->GetControlRotation();
		ViewingController->GetPlayerViewPoint(ViewLocation, ViewRotation);
		ViewDir = ViewRotation.Vector();
	}
}
FNetViewer 属性 作用
Connection 当前视图关联的网络连接对象
InViewer 控制网络视图的 Actor(通常是 PlayerController)
ViewTarget 实际用于观察的对象 Actor,比如 PlayerCharacter
ViewLocation 视图源点的世界坐标位置
ViewDir 视图方向的单位向量

通过 NetConnection 和其 ChildConnection 得到 TArray<FNetViewer>

const bool bProcessConsiderListIsBound = OnProcessConsiderListOverride.IsBound();

if (bProcessConsiderListIsBound)
{
	// 通过事件,转发给 DisplayClusterReplication 进行处理
	OnProcessConsiderListOverride.Execute( { DeltaSeconds, Connection, bCPUSaturated }, Updated, ConsiderList );
}

if (!bProcessConsiderListIsBound)
{
	// 执行常规复制流程 
	// ServerReplicateActors_PrioritizeActors
	// ServerReplicateActors_ProcessPrioritizedActorsRange
	// ServerReplicateActors_MarkRelevantActors
}

通过前面一系列操作,已经收集到这些信息

  • TArray<FNetworkObjectInfo*> ConsiderList 存储着本帧需要被处理的对象
  • TArray<FNetViewer>& ConnectionViewers 存储着本 NetConnection 关联的视角信息
ServerReplicateActors_PrioritizeActors

主要是对 ConsiderListDestroyedStartupOrDormantActors 进行处理

DestroyedStartupOrDormantActors 主要是用于记录和管理 启动状态下被销毁 和 休眠状态下被销毁的 Actor

针对 ConsiderList

通过 AActor::IsNetRelevantFor 筛选出与当前 NetConnection 相关的 Actor

  • bAlwaysRelevant 强制相关开关
  • ActorOwner 或者 InstigatorViewTarget 或者 InViewer
  • ActorOwnerIsNetRelevantFor 返回值,向 Owner 递归
  • ActorRootComponentAttachParetnOwnerIsNetRelevantFor
  • 距离判断,ActorViewTarget 距离不嫩超过 NetCullDistanceSquared

如果全局变量设置为允许休眠 也就是 GSetNetDormancyEnabled,需要判断 Actor 能否休眠

/** 如果需要休眠 返回 true */
ENGINE_API virtual bool GetNetDormancy(const FVector& ViewPos, const FVector& ViewDir, class AActor* Viewer, AActor* ViewTarget, UActorChannel* InChannel, float Time, bool bLowBandwidth);

需要子类重写该函数, Actor 默认函数返回 false

将符合条件的 Actor 封装到 FActorPriority

OutPriorityList[FinalSortedCount] = FActorPriority( PriorityConnection, Channel, ActorInfo, ConnectionViewers, bLowNetBandwidth );

最后对 OutPriorityList 进行根据 Priority 属性大小,从大到小进行排序

Algo::SortBy(MakeArrayView(OutPriorityActors, FinalSortedCount), &FActorPriority::Priority, TGreater<>());

FActorPriority 在构造的时候会计算 Priority 也就是优先级

Priority = 0;
const float Time = Channel ? (InConnection->Driver->GetElapsedTime() - Channel->LastUpdateTime) : InConnection->Driver->SpawnPrioritySeconds;
for (int32 i = 0; i < Viewers.Num(); i++)
{
	Priority = FMath::Max<int32>(Priority, FMath::RoundToInt(65536.0f * ActorInfo->Actor->GetNetPriority(Viewers[i].ViewLocation, Viewers[i].ViewDir, Viewers[i].InViewer, Viewers[i].ViewTarget, InChannel, Time, bLowBandwidth)));
}

如果是新的 Actor 它没有 Channel,则使用 SpawnPrioritySeconds 配置进行赋值;否则使用当前时间减去上次更新时间的差值

然后调用 AActor::GetNetPriority 进行优先级计算

  1. 自己就是 ViewTarget 则 Time * 4
  2. 如果在 ViewTarget 后方,距离 ViewTarget 超过远距离 则 Time * 0.2;超过近距离 则 Time * 0.4
  3. 如果在 ViewTarget 前方,距离 ViewTarget 不超过一定数值,且与角色夹角小于一定数值,则 Time * 2
  4. 如果在 ViewTarget 前方,距离 VieTarget 超过一定数值,则 Time * 0.4
  5. 其他情况 Time 不变

最后将 Time * AActor::NetPriority 得到该 Actor 真正的优先级

可见,一个 Actor 的复制优先级,是根据 ActorViewTarget视角距离 进行评判

如果想要手动设置 Actor 的优先级,可以直接其 NetPriority 属性的大小

NetPriorityBlueprintReadWrite

ServerReplicateActors_ProcessPrioritizedActorsRange

优先级顺序 对一段 Actors 子集进行复制

  • 传入 ActorsIndexRange 表示数组子区间
  • 传入 bIgnoreSaturation 表示忽略带宽饱和,默认 false
  • 输出 OutUpdated 计数
FNetworkObjectInfo*	ActorInfo = PriorityActors[j]->ActorInfo;

PriorityActors 中包含需要被删除的 Actor

在处理一个 FNetworkObjectInfo 时,先判断其是否是要被销毁的 Actor 判断条件就是 ActorInfo == NULL && PriorityActors[j]->DestructionInfo

// 发送销毁信息
SendDestructionInfo(Connection, PriorityActors[j]->DestructionInfo);

其他情况表示需要同步 Actor

如果 Actor 没有对应的 Channel 则创建对应的 Channel 并绑定 Actor

Channel = (UActorChannel*)Connection->CreateChannelByName( NAME_Actor, EChannelCreateFlags::OpenedLocally );
if ( Channel )
{
	Channel->SetChannelActor(Actor, ESetChannelActorFlags::None);
}

设置本次复制的一些信息:OptimalNetUpdateDeltaLastNetReplicateTimeRelevantTime 用于以后的一些条件判断

在检查通道带宽之后,开始 进行复制

// 检查 通道是否饱和
if ( Channel->IsNetReady( 0 ) || bIgnoreSaturation)
{
	// do something
	if(Channel->ReplicateActor())	// 真正复制 Actor 数据 并 发送数据
	{
		// do something
	}
	// do somehting
}
ServerReplicateActors_MarkRelevantActors

网络带宽不足时,智能标记那些需要在下帧优先处理的 Actor

PriorityActors[k]->ActorInfo->bPendingNetUpdate = true;

ReplicateActor

属性复制的核心逻辑就在 Channel->ReplicateActor()