# Unreal Iris(三)Filtering && Prioritization

Unreal Iris 并不支持 ReplicationGraph,取而代之的提供了 Filtering System 和 Prioritization。虽然目前还没有 ReplicationGraph 强大,但是性能方面有了很大程度的提升。

# DirtyNetObjectTracker

Iris 的 Filtering 并不是每帧都需要处理所有对象,而是通过 DirtyTracker 追踪每一帧中变化的对象,然后通过 Filtering 得到这些对象应该出现在哪些 Connection 亦或者从哪些 Connection 中被移除。

# Filtering

Filtering 操作主要发生在 TickFlush 的 PreSendUpdate 阶段。其主要作用就是筛选出哪些对象是需要同步的:

image-20240307095246281

在讲解整个 Filter 流程之前,先来看看其组成内容有哪些。Filter System 的实现核心在 FReplicationFiltering 中,大致可以分为四大块:

  • Owner Filter:针对对象的过滤。
  • Connection Filter:只对某些连接可见的过滤。
  • Group Filter:多个 Actor 组成一个 Group,该 Group 只对某些连接可见。
  • Dynamic Filter:灵活多变,用于自定义的过滤规则。

# Owner Filter

通过 ObjectInternalIndex 进行访问,其功能是指定某个 Object 对某个 Connection 可见的一对一关系。

image-20240307165600924

bool UReplicationSystem::SetFilter(FNetRefHandle Handle, UE::Net::FNetObjectFilterHandle Filter)
    
constexpr FNetObjectFilterHandle ToOwnerFilterHandle = FNetObjectFilterHandle(1);
Server->ReplicationSystem->SetFilter(ServerObject->NetRefHandle, ToOwnerFilterHandle)

# Connection Filter

通过 ObjectInternalIndex 进行访问,其功能是指定某个 Object 对某些 Connection 可见的一对多关系。

image-20240307165910248

UsedPerObjectInfoStorage 中的每一位都是一个 ConnectionStateIdx,里面是 0/1 则表示对应的 ConnectionStateIdx 是否被使用中。

ConnectionStateIdx 会指向 PerObjectInfoStorage 的一块连续数据,这些数据中的每一位都代表一个有效 Connection 的可见性。

image-20240307170152836

bool UReplicationSystem::SetConnectionFilter(FNetRefHandle Handle, const TBitArray<>& Connections, UE::Net::ENetFilterStatus ReplicationStatus)
    
TBitArray<> AllowedConnections;
AllowedConnections.Init(false, Client->ConnectionIdOnServer + 1);
AllowedConnections[Client->ConnectionIdOnServer] = true;
Server->ReplicationSystem->SetConnectionFilter(ServerObject->NetRefHandle, AllowedConnections, ENetFilterStatus::Allow);

# Group Filter

通过 ObjectInternalIndex 进行访问,每个 NetObject 最多可以绑定到 4 个 Group 上。Group 通过 GroupHandle 进行访问,并关联一组 Connection。其功能是指定一组 NetObject 对若干个 Connection。

每个 GroupHandle 对应一个 FNetObjectGroup 和 一个 FPerGroupInfo

  • FNetObjectGroup :其中可以包含若干个 NetObject 的 ObjectInternalIndex。可以通过 Group 关联上多个 NetObject。
  • FPerGroupInfo :其中包含一个 ConnectionStateIndex。可以通过 Group 关联上多个 Connection。

image-20240307172049452

FNetObjectGroupHandle GroupHandle = Server->ReplicationSystem->CreateGroup();
Server->ReplicationSystem->AddToGroup(GroupHandle, ServerObject->NetRefHandle);
Server->ReplicationSystem->AddGroupFilter(GroupHandle);

# Dynamic Filter

动态过滤,用户自定义的过滤规则。通过 ObjectInternalIndex 进行访问,多个 NetObject 对应一个 DynamicFilter 规则(UNetObjectFilter)。

image-20240308104316008

目前 Iris 提供了三种过滤规则,可以通过配置文件的方式进行添加:

image-20240308104856522

  • UFilterOutNetObjectFilter:一刀切什么都不同步。
  • UNetObjectGridFilter:基于网格的距离同步。
  • UNopNetObjectFilter:一刀切什么都同步。
[/Script/IrisCore.NetObjectFilterDefinitions]
+NetObjectFilterDefinitions=(FilterName=Spatial, ClassName=/Script/IrisCore.NetObjectGridFilter, ConfigClassName=/Script/IrisCore.NetObjectGridFilterConfig)
+NetObjectFilterDefinitions=(FilterName=NotRouted, ClassName=/Script/IrisCore.FilterOutNetObjectFilter, ConfigClassName=/Script/IrisCore.FilterOutNetObjectFilterConfig)
class UNetObjectGridFilterConfig : public UNetObjectFilterConfig
{
	GENERATED_BODY()
public:
	/** How many frames a view position should be considered relevant. To avoid culling issues when player borders cells. */
	UPROPERTY(Config)
	uint32 ViewPosRelevancyFrameCount = 2;
	UPROPERTY(Config)
	float CellSizeX = 20000.0f;
	UPROPERTY(Config)
	float CellSizeY = 20000.0f;
	/** Objects with larger sqrt(NetCullDistanceSqr) will be rejected. */
	UPROPERTY(Config)
	float MaxCullDistance = 20000.0f;
	/** Objects without a NetCullDistanceSquared property will assume to have this value but squared unless there's a cull distance override. */
	UPROPERTY(Config)
	float DefaultCullDistance = 15000.0f;
	/** Coordinates will be clamped to MinPos and MaxPos. */
	UPROPERTY(Config)
	FVector MinPos = {-0.5f*2097152.0f, -0.5f*2097152.0f, -0.5f*2097152.0f};
	/** Coordinates will be clamped to MinPos and MaxPos. */
	UPROPERTY(Config)
	FVector MaxPos = {+0.5f*2097152.0f, +0.5f*2097152.0f, +0.5f*2097152.0f};
};

还可以单独配置每个类型采用何种 Dyanmic Filter:

[/Script/IrisCore.ObjectReplicationBridgeConfig]
; Filters
DefaultSpatialFilterName=Spatial
; Clear all filters
!FilterConfigs=ClearArray
+FilterConfigs=(ClassName=/Script/Engine.LevelScriptActor, DynamicFilterName=NotRouted) ; Not needed
+FilterConfigs=(ClassName=/Script/Engine.Actor, DynamicFilterName=None))
; Info types aren't supposed to have physical representation
+FilterConfigs=(ClassName=/Script/Engine.Info, DynamicFilterName=None)
+FilterConfigs=(ClassName=/Script/Engine.PlayerState, DynamicFilterName=None)
; Pawns can be spatially filtered
+FilterConfigs=(ClassName=/Script/Engine.Pawn, DynamicFilterName=Spatial))
+FilterConfigs=(ClassName=/Script/EntityActor.SimObject, DynamicFilterName=None))

# Connection Filter Result

Connection 中有单独的结构用于存储过滤过程中每个步骤的结果 ——FReplicationFiltering::FPerConnectionInfo

image-20240308102217973

  • ConnectionFilteredObjects: 连接对对象的可见性状态。这个数据来源于 NetRefHandleManager->GetScopableInternalIndices()Owner FilterObject Connection Filter 的 并集。
  • GroupFilteredOutObjectsGroup Filter 规则特意让某些 Group 中的 NetObject 对于 Connection 不可见时,会把该状态汇总更新在这里。
  • ObjectsInScopeBeforeDynamicFiltering 基于前两者 AndNotOp 合并后的结果。既对象本身可见或者因为 ConnectionFilter or Owner Filter 可见但又没被 Group 给排除掉的对象。
  • DynamicFilteredOutObjects:因为 Dynamic Filter 而被排除的对象。
  • ObjectsInScope:最终的过滤结果。 ObjectsInScopeBeforeDynamicFilteringData & ~DynamicFilteredOutObjectsData 的结果。

通过上诉四轮过滤,最终的结果会保留在每个连接的 ObjectsInScope 中,整个过滤过程便会到此结束。对于外部系统来说,只需要关注 ObjectsInScope 的最终结果即可,并且可以通过前后两帧的差异实现进出视野的业务逻辑。

# Prioritization

image-20240308112813318

当对象的 PriorityValue 为 0 的时候,则表示该对象是不会参与同步。

image-20240310120339701

  • NetObjectPrioritizationInfos:记录每个对象计算优先级时,根据计算规则,存储所需的数据。
  • ObjectIndexToPrioritizer:标识对象具体需要何种计算规则。
  • PrioritizerInfos:存储所有种类的优先级计算规则。
  • ConnectionInfos:存储所有连接中,各个对象的优先级数值。

整体来看就是对所有对象,应用不同的优先级规则,计算出该 NetObject 在各个连接上的优先级数值。定义对象的优先级策略可以通过以下方式实现:

[/Script/IrisCore.ObjectReplicationBridgeConfig]
; PrioritizerConfigs
+PrioritizerConfigs=(ClassName=/Script/Engine.PlayerState, PrioritizerName=PlayerState)
[/Script/IrisCore.NetObjectPrioritizerDefinitions]
+NetObjectPrioritizerDefinitions=(PrioritizerName=Default, ClassName=/Script/IrisCore.SphereNetObjectPrioritizer, ConfigClassName=/Script/IrisCore.SphereNetObjectPrioritizerConfig)
+NetObjectPrioritizerDefinitions=(PrioritizerName=PlayerState, ClassName=/Script/IrisCore.NetObjectCountLimiter, ConfigClassName=/Script/Engine.PlayerStateCountLimiterConfig)

或者通过接口动态设置:

bool UReplicationSystem::SetPrioritizer(FNetRefHandle Handle, FNetObjectPrioritizerHandle Prioritizer)
    
ReplicationSystem->SetPrioritizer(RefHandle, DefaultSpatialNetObjectPrioritizerHandle);

Unreal 目前支持两种类别的优先级处理:基于距离,基于同步间隔。如果需要自定义规则可以继承 UNetObjectPrioritizer 实现相关的接口:

image-20240310120449248

亦或者指定某个对象的 StaticPriority:

void UReplicationSystem::SetStaticPriority(FNetRefHandle Handle, float Priority)
    
ReplicationSystem->SetStaticPriority(ServerDependentObject->NetRefHandle, 0.f);

# 依赖关系在优先级中的体现

实际上很多对象的同步顺序会收到依赖关系的影响:

  • 如果 Dependent 关系中 EDependentObjectSchedulingHint 设置的 ScheduleBeforeParent,那么会确保 Child 的优先级比 Parent 高一点,这里一般会加上一个 UE_KINDA_SMALL_NUMBER (1.e-4f)。
  • 如果没有这种强依赖关系的情况下,会让两者的优先级持平,存在多个依赖的情况下会取最大者。

# 参考链接

  • [UFSH2023] Iris Replication System 初探 | 陈宝康 Epic Games