Table of Contents
世界分区源码分析(一)
最近一直在写DS管理器的底层部分(BLEngine),已经实现底层数据结构,对象管理树,反射系统,基于TCP协议和Protobuf包管理的通信线程框架处理等。
接下来需要将虚幻的世界分区拆分成自定义粒度的多个DS服务器。这几天会陆续梳理世界分区的核心逻辑和数据结构,记录在这里,方便自己,也方便大家。
网上世界分区的资料比较少,也比较浅。只好来分析代码。
起始
世界分区的代码是从这个类入手的:UWorldPartition
先来看BeginPlay这个初始化函数
void UWorldPartition::OnBeginPlay()
{
// 关键在这里
GenerateStreaming(Params, Context);
// Prepare GeneratedStreamingPackages
check(GeneratedStreamingPackageNames.IsEmpty());
for (const FString& PackageName : OutGeneratedStreamingPackageNames)
{
// Set as memory package to avoid wasting time in UWorldPartition::IsValidPackageName (GenerateStreaming for PIE runs on the editor world)
FString Package = FPaths::RemoveDuplicateSlashes(FPackageName::IsMemoryPackage(PackageName) ? PackageName : TEXT("/Memory/") + PackageName);
GeneratedStreamingPackageNames.Add(Package);
}
}
GenerateStreaming函数中,这个函数的主要功能是生成Streaming包,并将其加入到GeneratedStreamingPackageNames集合中。
这个函数是关键,具体实现我摘重要的记录一下。经过重重调用,关键实现在这里。
bool UWorldPartitionRuntimeSpatialHash::GenerateStreaming(UWorldPartitionStreamingPolicy* StreamingPolicy, const IStreamingGenerationContext* StreamingGenerationContext, TArray<FString>* OutPackagesToGenerate)
{
UWorldPartition* WorldPartition = GetOuterUWorldPartition();
if (!Grids.Num())
{
UE_LOG(LogWorldPartition, Error, TEXT("Invalid partition grids setup"));
return false;
}
// Fix case where StreamingGrids might have been persisted.
bIsNameToGridMappingDirty = true;
StreamingGrids.Empty();
// Append grids from ASpatialHashRuntimeGridInfo actors to runtime spatial hash grids
TArray<FSpatialHashRuntimeGrid> AllGrids;
AllGrids.Append(Grids);
TArray<const FWorldPartitionActorDescView*> SpatialHashRuntimeGridInfos = StreamingGenerationContext->GetMainWorldContainer()->ActorDescViewMap->FindByExactNativeClass<ASpatialHashRuntimeGridInfo>();
for (const FWorldPartitionActorDescView* SpatialHashRuntimeGridInfo : SpatialHashRuntimeGridInfos)
{
FWorldPartitionReference Ref(WorldPartition, SpatialHashRuntimeGridInfo->GetGuid());
ASpatialHashRuntimeGridInfo* RuntimeGridActor = CastChecked<ASpatialHashRuntimeGridInfo>(SpatialHashRuntimeGridInfo->GetActor());
if (const FSpatialHashRuntimeGrid* ExistingRuntimeGrid = AllGrids.FindByPredicate([RuntimeGridActor](const FSpatialHashRuntimeGrid& RuntimeGrid) { return RuntimeGrid.GridName == RuntimeGridActor->GridSettings.GridName; }))
{
UE_LOG(LogWorldPartition, Log, TEXT("Got duplicated runtime grid actor '%s' for grid '%s'"), *SpatialHashRuntimeGridInfo->GetActorLabelOrName().ToString(), *RuntimeGridActor->GridSettings.GridName.ToString());
check(*ExistingRuntimeGrid == RuntimeGridActor->GridSettings);
}
else
{
AllGrids.Add(RuntimeGridActor->GridSettings);
}
}
TMap<FName, int32> GridsMapping;
GridsMapping.Add(NAME_None, 0);
for (int32 i = 0; i < AllGrids.Num(); i++)
{
const FSpatialHashRuntimeGrid& Grid = AllGrids[i];
check(!GridsMapping.Contains(Grid.GridName));
GridsMapping.Add(Grid.GridName, i);
}
TArray<TArray<const IStreamingGenerationContext::FActorSetInstance*>> GridActorSetInstances;
GridActorSetInstances.InsertDefaulted(0, AllGrids.Num());
StreamingGenerationContext->ForEachActorSetInstance([&GridsMapping, &GridActorSetInstances](const IStreamingGenerationContext::FActorSetInstance& ActorSetInstance)
{
int32* FoundIndex = GridsMapping.Find(ActorSetInstance.RuntimeGrid);
if (!FoundIndex)
{
//@todo_ow should this be done upstream?
UE_LOG(LogWorldPartition, Error, TEXT("Invalid partition grid '%s' referenced by actor cluster"), *ActorSetInstance.RuntimeGrid.ToString());
}
int32 GridIndex = FoundIndex ? *FoundIndex : 0;
GridActorSetInstances[GridIndex].Add(&ActorSetInstance);
});
const FBox WorldBounds = StreamingGenerationContext->GetWorldBounds();
for (int32 GridIndex=0; GridIndex < AllGrids.Num(); GridIndex++)
{
const FSpatialHashRuntimeGrid& Grid = AllGrids[GridIndex];
const FSquare2DGridHelper PartionedActors = GetPartitionedActors(WorldBounds, Grid, GridActorSetInstances[GridIndex]);
if (!CreateStreamingGrid(Grid, PartionedActors, StreamingPolicy, OutPackagesToGenerate))
{
return false;
}
}
return true;
}
UWorldPartitionRuntimeSpatialHash::GenerateStreaming 的主要工作是创建 StreamingGrid,并将其添加到 StreamingGrids 数组中。
世界分区管理者-UWorldPartitionRuntimeSpatialHash
我们仔细看一下UWorldPartitionRuntimeSpatialHash这个Class.
先假设这是世界分区的顶层结构,那么应该在WorldPartitionRuntimeHash(世界分区运行时哈希)中保存所有的分区Grids信息,并且提供操控、组合、遍历、删除这些Grids信息的方法。
UCLASS(MinimalAPI)
class UWorldPartitionRuntimeSpatialHash : public UWorldPartitionRuntimeHash
{
public:
ENGINE_API virtual void PreSave(const class ITargetPlatform* TargetPlatform) override;
virtual void PreSave(FObjectPreSaveContext ObjectSaveContext) override;
ENGINE_API virtual void Serialize(FArchive& Ar) override;
#if WITH_EDITOR
ENGINE_API virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
ENGINE_API virtual void SetDefaultValues() override;
virtual bool SupportsHLODs() const override { return true; }
ENGINE_API virtual void FlushStreaming() override;
ENGINE_API virtual bool GenerateHLOD(ISourceControlHelper* SourceControlHelper, const IStreamingGenerationContext* StreamingGenerationContext, bool bCreateActorsOnly) const override;
ENGINE_API virtual bool IsValidGrid(FName GridName) const override;
ENGINE_API virtual void DrawPreview() const override;
ENGINE_API virtual URuntimeHashExternalStreamingObjectBase* StoreToExternalStreamingObject(UObject* StreamingObjectOuter, FName StreamingObjectName) override;
static ENGINE_API FString GetCellNameString(UWorld* InOuterWorld, FName InGridName, const FGridCellCoord& InCellGlobalCoord, const FDataLayersID& InDataLayerID, const FGuid& InContentBundleID, FString* OutInstanceSuffix = nullptr);
static ENGINE_API FGuid GetCellGuid(FName InGridName, int32 InCellSize, const FGridCellCoord& InCellGlobalCoord, const FDataLayersID& InDataLayerID, const FGuid& InContentBundleID);
ENGINE_API bool GetPreviewGrids() const;
ENGINE_API void SetPreviewGrids(bool bInPreviewGrids);
ENGINE_API int32 GetPreviewGridLevel() const;
ENGINE_API void SetPreviewGridLevel(int32 InPreviewGridLevel);
#endif
// streaming interface
u // 核心功能:遍历StreamingGrids,并且调用Func对每个Grid进行操作
ENGINE_API virtual void ForEachStreamingCells(TFunctionRef<bool(const UWorldPartitionRuntimeCell*)> Func) const override;
ENGINE_API virtual void ForEachStreamingCellsQuery(const FWorldPartitionStreamingQuerySource& QuerySource, TFunctionRef<bool(const UWorldPartitionRuntimeCell*)> Func, FWorldPartitionQueryCache* QueryCache = nullptr) const override;
ENGINE_API virtual void ForEachStreamingCellsSources(const TArray<FWorldPartitionStreamingSource>& Sources, TFunctionRef<bool(const UWorldPartitionRuntimeCell*, EStreamingSourceTargetState)> Func) const override;
ENGINE_API virtual uint32 ComputeUpdateStreamingHash() const override;
ENGINE_API virtual bool InjectExternalStreamingObject(URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject) override;
ENGINE_API virtual bool RemoveExternalStreamingObject(URuntimeHashExternalStreamingObjectBase* ExternalStreamingObject) override;
ENGINE_API uint32 GetNumGrids() const;
static ENGINE_API FString GetCellCoordString(const FGridCellCoord& InCellGlobalCoord);
ENGINE_API void ForEachStreamingGrid(TFunctionRef<void(const FSpatialHashStreamingGrid&)> Func) const;
protected:
// 获取StreamingGrid通过GridName
ENGINE_API const FSpatialHashStreamingGrid* GetStreamingGridByName(FName InGridName) const;
ENGINE_API void ForEachStreamingGrid(TFunctionRef<void(FSpatialHashStreamingGrid&)> Func);
ENGINE_API void ForEachStreamingGridBreakable(TFunctionRef<bool(const FSpatialHashStreamingGrid&)> Func) const;
ENGINE_API virtual EWorldPartitionStreamingPerformance GetStreamingPerformanceForCell(const UWorldPartitionRuntimeCell* Cell) const override;
#if WITH_EDITOR
ENGINE_API virtual bool GenerateStreaming(class UWorldPartitionStreamingPolicy* StreamingPolicy, const IStreamingGenerationContext* StreamingGenerationContext, TArray<FString>* OutPackagesToGenerate = nullptr) override;
ENGINE_API virtual void DumpStateLog(FHierarchicalLogArchive& Ar) const override;
#endif
private:
/** Console command used to change loading range for a given streaming grid */
static ENGINE_API class FAutoConsoleCommand OverrideLoadingRangeCommand;
#if WITH_EDITORONLY_DATA
// 编辑器模式下,用于存储StreamingGrid
UPROPERTY(EditAnywhere, Config, Category = RuntimeSettings)
TArray<FSpatialHashRuntimeGrid> Grids;
/** Whether to preview runtime grids. */
UPROPERTY(Transient)
bool bPreviewGrids;
UPROPERTY(Transient)
int32 PreviewGridLevel;
UPROPERTY(Transient)
mutable FWorldPartitionRuntimeSpatialHashGridPreviewer GridPreviewer;
#endif
/** Disable to help break the pattern caused by world partition promotion of actors to upper grid levels that are always aligned on child levels. */
UPROPERTY(EditAnywhere, Config, AdvancedDisplay, Category = RuntimeSettings)
EWorldPartitionCVarProjectDefaultOverride UseAlignedGridLevels;
/** Disable to avoid snapping higher levels cells to child cells. Only used when bUseAlignedGridLevels is false. */
UPROPERTY(EditAnywhere, Config, AdvancedDisplay, Category = RuntimeSettings)
EWorldPartitionCVarProjectDefaultOverride SnapNonAlignedGridLevelsToLowerLevels;
/** Enable to place actors smaller than a cell size into their corresponding cell using their location instead of their bounding box. */
UPROPERTY(EditAnywhere, Config, AdvancedDisplay, Category = RuntimeSettings)
EWorldPartitionCVarProjectDefaultOverride PlaceSmallActorsUsingLocation;
/** Enable to place partitioned actors into their corresponding cell using their location instead of their bounding box. */
UPROPERTY(EditAnywhere, Config, AdvancedDisplay, Category = RuntimeSettings)
EWorldPartitionCVarProjectDefaultOverride PlacePartitionActorsUsingLocation;
/** Whether this hash enables Z culling. */
UPROPERTY(EditAnywhere, Config, Category = RuntimeSettings)
bool bEnableZCulling;
protected:
/**
* Represents the streaming grids (PIE or Game)
*/
// 运行时模式下,用于存储StreamingGrid的容器
UPROPERTY(NonPIEDuplicateTransient)
TArray<FSpatialHashStreamingGrid> StreamingGrids;
// 名字与StreamingGrid的映射Map
mutable TMap<FName, const FSpatialHashStreamingGrid*> NameToGridMapping;
mutable bool bIsNameToGridMappingDirty;
private:
// 一组绘图接口,绘制StreamingGrid信息
ENGINE_API virtual bool Draw2D(FWorldPartitionDraw2DContext& DrawContext) const override;
ENGINE_API virtual void Draw3D(const TArray<FWorldPartitionStreamingSource>& Sources) const override;
ENGINE_API virtual bool ContainsRuntimeHash(const FString& Name) const override;
ENGINE_API virtual bool IsStreaming3D() const override;
ENGINE_API void GetAlwaysLoadedStreamingCells(const FSpatialHashStreamingGrid& StreamingGrid, TSet<const UWorldPartitionRuntimeCell*>& Cells) const;
ENGINE_API const TMap<FName, const FSpatialHashStreamingGrid*>& GetNameToGridMapping() const;
#if WITH_EDITOR
ENGINE_API bool CreateStreamingGrid(const FSpatialHashRuntimeGrid& RuntimeGrid, const FSquare2DGridHelper& PartionedActors, UWorldPartitionStreamingPolicy* StreamingPolicy, TArray<FString>* OutPackagesToGenerate = nullptr);
#endif
ENGINE_API TArray<const FSpatialHashStreamingGrid*> GetFilteredStreamingGrids() const;
friend class UWorldPartitionSubsystem;
};
UWorldPartitionRuntimeSpatialHash的功能是管理运行时模式下的StreamingGrid,以及运行时模式下的StreamingGrid的绘制、包含、和获取。以及在运行时模式下的遍历调用StreamingGrid的方法。
网格对象(Grid)
如果说UWorldPartitionRuntimeSpatialHash是世界分区的管理者,那么Grid就代表了每一个具体的分区。
我们看一下Grid结构,感叹于虚幻团队的细心,这个结构有两种:编辑器模式下用于调试的FSpatialHashRuntimeGrid结构和运行时模式下负责Grid实际功能的FSpatialHashStreamingGrid结构
用于编辑器下调试的FSpatialHashRuntimeGrid
/**
* Represents a runtime grid (editing)
*/
USTRUCT()
struct FSpatialHashRuntimeGrid
{
FSpatialHashRuntimeGrid()
#if WITH_EDITORONLY_DATA
: CellSize(12800)
, LoadingRange(25600)
, bBlockOnSlowStreaming(false)
, Origin(FVector2D::ZeroVector)
, Priority(0)
, DebugColor(FLinearColor::MakeRandomColor())
, bClientOnlyVisible(false)
, HLODLayer(nullptr)
#endif
{}
#if WITH_EDITORONLY_DATA
UPROPERTY(EditAnywhere, Category=Settings)
FName GridName;
UPROPERTY(EditAnywhere, Category=Settings)
int32 CellSize;
UPROPERTY(EditAnywhere, Category=Settings)
float LoadingRange;
/** Should streaming block in situations where cells aren't getting loaded fast enough. */
UPROPERTY(EditAnywhere, Category=Settings)
bool bBlockOnSlowStreaming;
UPROPERTY(EditAnywhere, Category=Settings)
FVector2D Origin;
UPROPERTY(EditAnywhere, Category=Settings)
int32 Priority;
UPROPERTY(EditAnywhere, Category=Settings, meta = (IgnoreForMemberInitializationTest))
FLinearColor DebugColor;
UPROPERTY()
bool bClientOnlyVisible;
UPROPERTY()
TObjectPtr<const UHLODLayer> HLODLayer;
bool operator == (const FSpatialHashRuntimeGrid& Other) const;
bool operator != (const FSpatialHashRuntimeGrid& Other) const;
#endif
};
FSpatialHashRuntimeGrid主要是在编辑模式下使用的,包括GridName、CellSize、LoadingRange、bBlockOnSlowStreaming、Origin、Priority、DebugColor、bClientOnlyVisible、HLODLayer等字段。专为编辑器模式使用的,而且在编辑器模式下是一个可以修改的Grid结构,可以实时修改参数,来快捷看到运行结果。
用于实际运行的FSpatialHashStreamingGrid
/**
* Represents a PIE/Game streaming grid
*/
USTRUCT()
struct FSpatialHashStreamingGrid
{
ENGINE_API FSpatialHashStreamingGrid();
ENGINE_API ~FSpatialHashStreamingGrid();
// Move constructor.
FSpatialHashStreamingGrid(const FSpatialHashStreamingGrid& Other) = delete;
FSpatialHashStreamingGrid(FSpatialHashStreamingGrid&& Other) = default;
// Move assignment.
FSpatialHashStreamingGrid& operator=(const FSpatialHashStreamingGrid& Other) = delete;
FSpatialHashStreamingGrid& operator=(FSpatialHashStreamingGrid&& Other) = default;
UPROPERTY()
FName GridName;
UPROPERTY()
FVector Origin;
UPROPERTY()
int32 CellSize;
UPROPERTY()
float LoadingRange;
UPROPERTY()
bool bBlockOnSlowStreaming;
UPROPERTY()
FLinearColor DebugColor;
UPROPERTY()
TArray<FSpatialHashStreamingGridLevel> GridLevels;
UPROPERTY()
FBox WorldBounds;
UPROPERTY()
bool bClientOnlyVisible;
UPROPERTY()
TObjectPtr<const UHLODLayer> HLODLayer;
float OverrideLoadingRange;
float GetLoadingRange() const
{
if (OverrideLoadingRange >= 0.f)
{
return OverrideLoadingRange;
}
return LoadingRange;
}
ENGINE_API void InjectExternalStreamingObjectGrid(const FSpatialHashStreamingGrid& InExternalObjectStreamingGrid) const;
ENGINE_API void RemoveExternalStreamingObjectGrid(const FSpatialHashStreamingGrid& InExternalObjectStreamingGrid) const;
ENGINE_API bool InsertGridCell(UWorldPartitionRuntimeCell* InGridCell, const FGridCellCoord& InGridCellCoords);
// Used by PIE/Game
ENGINE_API int64 GetCellSize(int32 Level) const;
ENGINE_API void GetCells(const FWorldPartitionStreamingQuerySource& QuerySource, TSet<const UWorldPartitionRuntimeCell*>& OutCells, bool bEnableZCulling, FWorldPartitionQueryCache* QueryCache = nullptr) const;
ENGINE_API void GetCells(const TArray<FWorldPartitionStreamingSource>& Sources, UWorldPartitionRuntimeHash::FStreamingSourceCells& OutActivateCells, UWorldPartitionRuntimeHash::FStreamingSourceCells& OutLoadCells, bool bEnableZCulling) const;
ENGINE_API void GetNonSpatiallyLoadedCells(TSet<const UWorldPartitionRuntimeCell*>& OutActivateCells, TSet<const UWorldPartitionRuntimeCell*>& OutLoadCells) const;
ENGINE_API void Draw2D(const class UWorldPartitionRuntimeSpatialHash* Owner, const FBox2D& Region2D, const FBox2D& GridScreenBounds, TFunctionRef<FVector2D(const FVector2D&, bool)> WorldToScreen, FWorldPartitionDraw2DContext& DrawContext) const;
ENGINE_API void Draw3D(const class UWorldPartitionRuntimeSpatialHash* Owner, const TArray<FWorldPartitionStreamingSource>& Sources, const FTransform& Transform) const;
ENGINE_API void ForEachRuntimeCell(TFunctionRef<bool(const UWorldPartitionRuntimeCell*)> Func) const;
ENGINE_API const FSquare2DGridHelper& GetGridHelper() const;
private:
ENGINE_API void ForEachRuntimeCell(const FGridCellCoord& Coords, TFunctionRef<void(const UWorldPartitionRuntimeCell*)> Func) const;
ENGINE_API void ForEachLayerCell(const FGridCellCoord& Coords, TFunctionRef<void(const FSpatialHashStreamingGridLayerCell*)> Func) const;
ENGINE_API void DrawStreamingSource2D(const FBox2D& GridScreenBounds, const FSphericalSector& Shape, TFunctionRef<FVector2D(const FVector2D&)> WorldToScreen, const FColor& Color, FWorldPartitionDraw2DContext& DrawContext) const;
ENGINE_API void DrawStreamingSource3D(UWorld* World, const FSphericalSector& Shape, const FTransform& Transform, const FColor& Color) const;
ENGINE_API void GetFilteredCellsForDebugDraw(const FSpatialHashStreamingGridLayerCell* LayerCell, TArray<const UWorldPartitionRuntimeCell*>& FilteredCells) const;
ENGINE_API EWorldPartitionRuntimeCellVisualizeMode GetStreamingCellVisualizeMode() const;
mutable FSquare2DGridHelper* GridHelper;
// Contains cells injected at runtime from content bundles
mutable TArray<FSpatialHashStreamingGridLevel> InjectedGridLevels;
};
FSpatialHashStreamingGrid是一个网格节点。一个世界分区是由很多的网格节点(Grid)组成的。这些网格节点(Grid)通过WorldPartitionRuntimeHash组织在一起,动态的加载和卸载、并且分布在不同的层级(Layer),最终构成了一个无缝运行的世界分区。
我们仔细看一下网格节点做了哪些事情:
1 每个网格都有GridName(网格名字)、Origin(原点)、size(尺寸)、LayerCount(层数)、WorldBounds(世界边界)、LoadingRange(加载范围)、DebugColor(调试颜色)等属性。正是因为每个Grid都有自己的世界边界,尺寸以及加载范围,所以每个Grid都可以是任意不规则的形状。不要看到字面上的Grid网格就以为是方格子。
2 每个网格都是一个多层结构,多层结构存储在GridLevels这个TArray中。这个容易理解,无论是2D游戏还是3D游戏,都是通过这种多层结构来实现的场景。
3 提供了网格操作常用的API接口。
最后
到这里,我们对虚幻世界分区的基本结构有了直观了解,后面会继续展开细节,并做一些测试。