TECHARTNOMAD TECHARTFLOW.IO

TECH.ART.FLOW.IO

[번역] Unreal Engine 5 Open World Production

jplee 2024. 1. 11. 01:38

역자의 말.

이틀 전에 이어서 추가로 괜찮은 토픽이 있어서 공유 해 봅니다. 중국어는 글을 읽는 수준이 제가 좋지 않기 때문에 번역기의 도움을 받은 후 음성으로 다시 듣고 어색한 곳은 교정을 했기 때문에 조금 더 편히 보실 수 있을겁니다. ( 중국어 독학자라서 듣기는 회사에서 팀원들과 중국어로 소통하면서 지냈지만 중국어 문자는 많이 안습인 수준이거든요


월드파티션

UE5 에서 오픈 월드 맵을 제작할 때 꼭 알아야 할 파티셔닝 및 스트리밍 개념에 대한 몇 가지 필수 정보를 소개합니다! 꼭 보세요! 꼭 보세요! 꼭 봐야 할 몇 가지입니다!

 

 

버추얼 월드 제작

인터랙티브 인바이런먼트 및 레벨 디자인을 위한 툴과 기법에 대한 정보를 드립니다.

docs.unrealengine.com

 

 

【搬运】UnrealFest2023 UE5 building bigger建造更大的世界_哔哩哔哩_bilibili

https://www.youtube.com/@UnrealEngine 欢迎加入UP自建discord交流ue5的开放世界制作:discord.gg/d37HzSs7Ja(富强上网)

www.bilibili.com

이해하기 쉬운 설명 동영상도 있습니다.

 

【UE地形】世界分区(World Partition)是什么?怎么用?_哔哩哔哩_bilibili

8105 29 2023-09-08 16:57:42 未经作者授权,禁止转载

www.bilibili.com

 

간단히 말해, UE는 맵을 셀로 나누고 플레이어(스트리밍 소스)가 움직일 때 주변 셀을 스트리밍합니다:

위 그림에서는 4방향 파티션만 보이지만, 실제로 UE의 월드 파티션은 스트리밍에 스파스 쿼드트리 방식을 사용하며 그리드 레벨도 여러 개이므로 레벨0만 표시됩니다:

 

할당된 면적

월드 파티셔닝은 오브젝트를 둘러싸고 있는 박스를 기준으로 오브젝트가 속한 그리드 레벨 셀을 결정합니다.

예를 들어 월드 파티션에 다음과 같은 오브젝트가 있는 경우 빨간색 영역은 해당 오브젝트의 경계 상자의 가로 영역입니다.

월드 파티셔닝은 가장 낮은 그리드 레벨에서 시작하여 오브젝트를 해당 영역을 거의 포괄하는 셀로 분할합니다.

따라서 위의 오브젝트 분포의 최종 분할은 다음과 같습니다.

여기에서 파티션과 관련된 매개변수를 설정할 수 있습니다.

에디터 뷰에서 씬의 스트리밍 뷰를 보려면 메시 미리보기를 체크합니다.

접근 흐름

씬에 새로 생성된 액터는 기본적으로 스트리밍에 참여하며, 즉 언로드되며 액터의 디테일 패널에서 관련 조정을 할 수 있습니다.

  • 런타임 그리드 : 액터가 속한 월드 파티션 그리드로, None 이면 메인 그리드에 할당됩니다.
  • 이미 공간에 로드됨: 체크하면 액터가 월드 파티션 스트리밍에 참여하며, 체크 해제하면 액터가 항상 씬에 로드되고 스트리밍에 참여하지 않습니다.

월드 파티션에서의 스트리밍은 스트리밍 소스에 의해 트리거됩니다.

스트리밍 소스에 해당하는 C++ 인터페이스는 다음과 같습니다: IWorldPartitionStreamingSourceProvider

플레이어 컨트롤러는 기본 스트리밍 소스입니다.

이 시점에서 캐릭터가 맵의 오른쪽 아래 구석에 있는 경우, 위의 구분과 결합하면 전체 로딩 상황은 다음과 같이 보일 수 있습니다:

다른 액터도 스트리밍 소스로 작동할 수 있도록 WorldPartitionStreamingSourceComponent를 사용할 수도 있습니다!

뷰 디버깅

스트리밍 프로세스를 미리 볼 수 있는 공식 명령이 제공됩니다:

wp.Runtime.ToggleDrawRuntimeHash2D

wp.Runtime.ToggleDrawRuntimeHash3D

  • 미리보기 보기가 켜진 상태입니다.
  • wp.Runtime.ShowRuntimeSpatialHashGridLevel ${LevelIndex} 지시문을 사용하여 미리 보기의 수준을 조정합니다.
  • 셀을 로드하는 데 걸리는 시간을 확인하려면 wp.Runtime.ToggleDrawRuntimeCellsDetails 명령을 사용합니다.

  • 셀 스트리밍의 우선순위 히트맵을 보려면 wp.Runtime.ShowRuntimeSpatialHashCellStreamingPriority 1 명령을 사용합니다.

주요 구성

온라인
온라인 멀티플레이어에서는 서버가 기본적으로 스트리밍을 활성화하지 않으며, 장면 스트리밍은 플레이어의 로컬 동작일 뿐입니다.

서버에서 스트리밍은 다음 명령어를 사용하여 활성화할 수 있습니다.

스트리밍 최적화
월드 파티션은 기본적으로 스트리밍 최적화가 설정되어 있으며, 모든 프레임이 스트리밍 상태를 업데이트하는 것은 아니며, 다음 구성을 통해 최적화 전략을 조정할 수 있습니다:

  • wp.Runtime.UpdateStreaming.EnableOptimization 참: 기본값은 참, 스트리밍 최적화가 활성화됩니다.
  • wp.Runtime.UpdateStreaming.ForceUpdateFrameCount 0: 기본값은 0이며, 고정된 프레임 수를 설정하여 스트리밍 상태를 업데이트할 수 있습니다.
  • wp.Runtime.UpdateStreaming.LocationQuantization 400: 기본값은 400이며, 이동 거리가 400보다 크면 스트리밍 상태 업데이트를 트리거합니다.
  • wp.Runtime.UpdateStreaming.RotationQuantization 10: 기본값은 10이며, 회전 각도가 10보다 크면 스트리밍 상태 업데이트를 트리거합니다.

코드 흐름

개발자의 경우 월드 파티셔닝 프로세스에 대한 심층적인 이해는 전반적인 프로젝트 관리에 매우 중요합니다! 아티스트는 생략해도 됩니다.

여기서는 주요 단계 중 몇 가지를 골라 설명합니다.

UE 월드 파티션의 핵심 구조는 다음과 같습니다: UWorldPartition

AWorldSettings 에 저장되며 다음과 같이 액세스할 수 있습니다:

UWorldPartition* UWorld::GetWorldPartition() const
{
    AWorldSettings* WorldSettings = GetWorldSettings(/*bCheckStreamingPersistent*/false, /*bChecked*/false);
    return WorldSettings ? WorldSettings->GetWorldPartition() : nullptr;
}

액터의 월드 파티셔닝 지원은 주로 에디터의 더미 함수가 액터 베이스 클래스에 추가되어 월드 파티션에 대한 액터 설명을 생성하는 데 반영되었습니다:

class AActor: public UObject
#if WITH_EDITOR
    virtual TUniquePtr<class FWorldPartitionActorDesc> CreateClassActorDesc() const;
#endif
}

이러한 구조를 채우기 위해 서브클래스로 재작성할 수 있습니다:

class FWorldPartitionActorDesc
{
    // Persistent
    FGuid                           Guid;
    FTopLevelAssetPath              BaseClass;
    FTopLevelAssetPath              NativeClass;
    FName                           ActorPackage;
    FSoftObjectPath                 ActorPath;
    FName                           ActorLabel;
    FVector                         BoundsLocation;
    FVector                         BoundsExtent;
    FName                           RuntimeGrid;
    bool                            bIsSpatiallyLoaded;
    bool                            bActorIsEditorOnly;
    bool                            bActorIsRuntimeOnly;
    bool                            bActorIsHLODRelevant;
    bool                            bIsUsingDataLayerAsset; // Used to know if DataLayers array represents DataLayers Asset paths or the FNames of the deprecated version of Data Layers
    FName                           HLODLayer;
    TArray<FName>                   DataLayers;
    TArray<FGuid>                   References;
    TArray<FName>                   Tags;
    FPropertyPairsMap               Properties;
    FName                           FolderPath;
    FGuid                           FolderGuid;
    FGuid                           ParentActor; // Used to validate settings against parent (to warn on layer/placement compatibility issues)
    FGuid                           ContentBundleGuid;

    // Transient
    mutable uint32                  SoftRefCount;
    mutable uint32                  HardRefCount;
    UClass*                         ActorNativeClass;
    mutable TWeakObjectPtr<AActor>  ActorPtr;
    UActorDescContainer*            Container;
    TArray<FName>                   DataLayerInstanceNames;
    bool                            bIsForcedNonSpatiallyLoaded;
};

엔진에 덮어쓰기가 꽤 많이 있습니다:

에디터에서 월드 파티션 맵에 액터를 새로 만들면 FWorldPartitionActorDesc 가 자동 생성됩니다:

에디터에서 생성된 모든 액터 디스크립트는 UWorldPartition의 액터 디스크립트 컨테이너에 저장됩니다:

class UWorldPartition final 
    : public UObject
    , public FActorDescContainerCollection
    , public IWorldPartitionCookPackageGenerator
{

    UPROPERTY(Transient)
    TObjectPtr<UActorDescContainer> ActorDescContainer;     //ActorDesc를 저장하는 컨테이너로, **MainContainer**라고도 합니다.

    UPROPERTY()
    TObjectPtr<UWorldPartitionRuntimeHash> RuntimeHash;     //액터 커스터마이징을 위한 파티셔닝 전략

    UPROPERTY(Transient)
    TObjectPtr<UWorld> World;
};

ActorDescContainer는 임시 변수이며 직렬화에는 참여하지 않지만 패키징 중에 저장됩니다:

bool UWorldPartition::GatherPackagesToCook(IWorldPartitionCookPackageContext& CookContext)
{
    TArray<FString> PackagesToCook;
    if (GenerateContainerStreaming(ActorDescContainer, &PackagesToCook))
    {
        FString PackageName = GetPackage()->GetName();
        for (const FString& PackageToCook : PackagesToCook)
        {
            CookContext.AddLevelStreamingPackageToGenerate(this, PackageName, PackageToCook);
        }
        return true;
    }
    return false;
}

할당된 면적

여기서 UWorldPartition의 액터 파티셔닝 전략은 에디터가 시작될 때마다 맵을 다시 파티셔닝하고, 파티셔닝된 데이터는 패키징할 때 구워지는 RuntimeHash로 구현됩니다:

bool UWorldPartition::PrepareGeneratorPackageForCook(IWorldPartitionCookPackageContext& CookContext, TArray<UPackage*>& OutModifiedPackages)
{
    check(RuntimeHash);
    return RuntimeHash->PrepareGeneratorPackageForCook(OutModifiedPackages);
}

bool UWorldPartition::PopulateGeneratorPackageForCook(IWorldPartitionCookPackageContext& CookContext, const TArray<FWorldPartitionCookPackage*>& InPackagesToCook, TArray<UPackage*>& OutModifiedPackages)
{
    check(RuntimeHash);
    return RuntimeHash->PopulateGeneratorPackageForCook(InPackagesToCook, OutModifiedPackages);
}

bool UWorldPartition::PopulateGeneratedPackageForCook(IWorldPartitionCookPackageContext& CookContext, const FWorldPartitionCookPackage& InPackagesToCook, TArray<UPackage*>& OutModifiedPackages)
{
    check(RuntimeHash);
    return RuntimeHash->PopulateGeneratedPackageForCook(InPackagesToCook, OutModifiedPackages);
}

UWorldPartitionRuntimeCell* UWorldPartition::GetCellForPackage(const FWorldPartitionCookPackage& PackageToCook) const
{
    check(RuntimeHash);
    return RuntimeHash->GetCellForPackage(PackageToCook);
}

에디터에서 오픈 월드 맵을 시작할 때 UWorldPartition::OnBeginPlay가 호출됩니다:

void UWorldPartition::OnBeginPlay()
{
    TArray<FString> OutGeneratedStreamingPackageNames;
    GenerateStreaming((bIsPIE || IsRunningGame()) ? &OutGeneratedStreamingPackageNames : nullptr);

    // 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);
    }

    RuntimeHash->OnBeginPlay();
}

여기서 생성 스트리밍 함수는 UWorldPartition::GenerateContainerStreaming 으로 추가 호출되어, 메인 컨테이너에서 모든 컨테이너와 액터 디스크를 재귀적으로 검색합니다.

씬에 배치된 레벨 인스턴스의 특수한 경우인 FLevelInstanceActorDesc와 같이 일부 액터 디스크로는 자체 컨테이너를 보유하며, 주요 코드는 다음과 같이 정의됩니다:

class FLevelInstanceActorDesc : public FWorldPartitionActorDesc
{
public:
    FLevelInstanceActorDesc();
    virtual ~FLevelInstanceActorDesc() override;

    virtual bool IsContainerInstance() const override;
    virtual bool GetContainerInstance(const UActorDescContainer*& OutLevelContainer, 
                                      FTransform& OutLevelTransform, 
                                      EContainerClusterMode& OutClusterMode) const override;
protected:
    FTransform LevelInstanceTransform;
    ELevelInstanceRuntimeBehavior DesiredRuntimeBehavior;
    TWeakObjectPtr<UActorDescContainer> LevelInstanceContainer;  //레벨 인스턴스의 액터 디스컨타이
private:
    void RegisterContainerInstance(UWorld* InWorld);
    void UnregisterContainerInstance();
};

모든 컨테이너와 액터 디스크를 수집한 후에 호출됩니다:

bool UWorldPartitionRuntimeSpatialHash::GenerateStreaming(UWorldPartitionStreamingPolicy* StreamingPolicy,
                                                          const IStreamingGenerationContext* StreamingGenerationContext,
                                                          TArray<FString>* OutPackagesToGenerate);

이 기능의 주요 흐름은 다음과 같습니다:

  • 씬의 모든 그리드 구성을 수집합니다(그리드 구성을 포함하는 것은 월드 세팅뿐만 아니라, 현재 HLOD에 주로 사용되는 ASpatialHashRuntimeGridInfo 와 같은 일부 액터도 그리드 구성을 제공합니다).
  • 모든 액터 디스크를 반복처리한 다음 액터 디스크로의 런타임 그리드 환경설정에 따라 해당 그리드의 FActorSetInstance 로 나눕니다.
  • 각 그리드의 FActorSetInstance 에 대해 호출됩니다:
FSquare2DGridHelper GetPartitionedActors(const FBox& WorldBounds, 
                                         const FSpatialHashRuntimeGrid& Grid, 
                                         const TArray<const FActorSetInstance*>& ActorSetInstances)

를 사용하여 그리드의 모든 액터를 특정 셀로 나눕니다.

 

접근 흐름

스트리밍의 로직은 주로 여기에 있습니다:

주요 이슈

월드 파티션을 사용하면 원활하게 로딩되는 대규모 월드를 구현할 수 있지만 스트리밍의 특성상 많은 제약이 따르기 때문에 개발자는 전문 엔진 팀을 갖춘 보다 성숙한 프로덕션 팀을 구성하고, 설계 시 월드 파티션의 특성을 고려해야 하며, 월드 파티션 스트리밍 전략을 감독할 전문 인력이 필요한 경우가 많습니다.

국경을 넘나들다

이전 객체 분할 및 스트리밍 관찰:

몇 가지 "이상한" 현상이 관찰될 수 있습니다.

  • 오브젝트 E의 둘러싸는 상자가 월드 파티션의 축에 위치하기 때문에 레벨 3으로 분류되어 영원히 로드되지만 둘러싸는 상자 영역은 분명히 매우 작습니다.

이 문제는 매우 쉽게 발생할 수 있습니다:'

해결책

엔진은 이러한 현상을 완화하는 데 도움이 되는 여러 가지 명령을 제공합니다:

  • wp.Runtime.RuntimeSpatialHashUseAlignedGridLevels: 그리드 레벨의 상향 정렬을 켤지 여부입니다.
  • wp.Runtime.RuntimeSpatialHashSnapNonAlignedGridLevelsToLowerLevels: 정렬되지 않은 개체를 분할하여 그리드 레벨을 낮춥니다.
  • wp.Runtime.RuntimeSpatialHashPlaceSmallActorsUsingLocation: 셀 크기보다 작은 둘러싸는 상자가 있는 객체는 위치를 사용하여 직접 분할합니다.
  • Runtime.RuntimeSpatialHashPlacePartitionActorsUsingLocation: 둘러싸는 박스가 셀 크기보다 작은 분할된 오브젝트(초목, 물, 지형)는 위치를 사용하여 직접 분할합니다.

이에 대한 설명은 여기에서 확인할 수 있습니다:

 

Tech Note: World Partition Spatially Loaded Actors are Always Loaded

This article was written by Ryan Bickell Description: World Partition Actors can be “promoted” to higher level cells (cells that cover a larger area than the set Grid Cell Size) of the grid hierarchy depending on their location or bounds and the curren

forums.unrealengine.com

하지만 이는 좋은 해결책이 아니며, 이러한 문제를 피하기 위해 UE는 개발팀이 여기에서 RuntimeHashClass 를 대체하여 시나리오의 필요에 따라 커스터마이징할 수 있도록 했습니다:

참조 가능:

Engine\Source\Runtime\Engine\Private\WorldPartition\RuntimeSpatialHash\RuntimeSpatialHashGridHelper.cpp

UE5.3에서는 위의 구성이 에디터에 공식적으로 노출되어 월드 파티션의 좌표 원점을 조정할 수 있으며, 대부분의 요구 사항을 충족합니다:

로드 우선순위

월드 파티셔닝 스트리밍으로 인해 씬의 일부 움직이는 오브젝트에서 이상 현상이 발생하는데, 그 중 가장 흔한 것이 중력입니다:

위 그림에서 문제는 다음과 같습니다:

  • 큐브 중 일부가 셀 중 하나로 분할되고 분할 알고리즘의 한계로 인해 땅이 다른 셀로 분할되고 땅이 위치한 셀이 하중 범위 내에 있지 않기 때문에 물체가 땅 바닥으로 직접 떨어지게됩니다.

이 문제를 유발할 수 있는 다른 상황도 있습니다:

  • 월드 파티션 스트리밍은 액터 단위가 아닌 셀 단위로 이루어지며, 셀이 스트리밍 소스의 로딩 범위 내에 있으면 해당 셀의 모든 액터를 비동기적으로 로딩하고, 로딩 순서는 보장되지 않으며, 큐브가 먼저 로드된 후 지면이 로드되면 이 때 피직스 틱이 실행되면 오브젝트도 지면으로 떨어지게 됩니다. (씬에 스트레스가 가해지면 이런 일이 발생할 가능성이 더 높습니다.

처방전

이 문제를 해결하기 위한 주요 아이디어는 다음과 같습니다:

  • 중력을 받는 오브젝트가 위치한 월드 파티션 메시의 로딩 범위를 더 넓게 설정합니다.
  • 중력 오브젝트를 중력을 받는 오브젝트보다 로딩 우선순위가 낮은 월드 파티션 메시로 이동합니다.

데이터 지속성

월드 파티셔닝에 관련된 액터는 데이터 퍼시스턴트가 아니므로 로드 후 오브젝트를 변경하면 언로드할 때 변경 사항이 저장되지 않습니다.

예를 들어 월드 파티션에 떠다니는 큐브 그룹이 있는데, 로드 후 중력 때문에 땅으로 떨어지고, 언로드한 후 다시 로드하면 큐브가 이전 상태를 유지한 채 땅에 떨어지는 것을 볼 수 있습니다:

콘솔 명령어 gc.ForceCollectGarbageEveryFrame 1을 켜면 다시 살펴보세요:

이 시점에서 큐브가 다시 로드되고 큐브가 시작 위치로 돌아갑니다.

첫 번째 예는 우리가 물체 수정을 저장할 수 있다는 착각을 일으키게 하는데, 이는 로딩 범위를 벗어난 셀이 즉시 풀리지 않고 Unloaded Still Around 상태이기 때문입니다. 이렇게 하면 다음 로딩 시 즉시 재사용할 수 있고, 실제로 셀을 언로드할 수 있는 시기는 다음 GC입니다.

솔루션

이 문제는 스트리밍의 특성으로 인해 발생합니다:

  • 스트리밍은 에셋을 메모리에 로드하고, 스트리밍은 에셋의 수정 사항을 저장하지 않고 에셋을 언로드(리소스 파괴)합니다.

그렇다면 유출 시 에셋 수정 사항을 저장해야 하나요?

  • 영구적으로 수정하려는 경우, 즉 게임을 다시 연 후에 수정하려는 경우가 아니라면 이전 수정 사항은 여전히 존재하며 복원할 수 없습니다.

일반적으로 이러한 수정 사항은 특정 범위(수명 주기) 내에서만 유지되며, 예를 들어 게임에 재접속하거나 레벨에 재진입하고 아카이브를 다시 열면 손실됩니다.

이제 스트리밍이 데이터 손실로 이어지지 않기를 바랄 뿐입니다.

그 정도면 충분합니다:

  • 데이터의 이 부분이 스트리밍에 참여하지 않도록, 즉 데이터가 액터가 아닌 합리적인 범위에 저장되도록 하면 됩니다.

UE5.3에서는 개발자가 이 문제를 해결하는 데 도움이 될 수 있는 실험단계 플러그인 LevelStreamingPersistence를 제공합니다.

핵심 구조는 다음과 같습니다:

class ULevelStreamingPersistenceManager : public UWorldSubsystem
{
    GENERATED_BODY()

    // Serialization
    bool SerializeTo(TArray<uint8>& OutPayload);
    bool InitializeFrom(const TArray<uint8>& InPayload);

    // Sets property value and creates the entry if necessary, returns true on success.
    template <typename ClassType, typename PropertyType>
    bool SetPropertyValue(const FString& InObjectPathName, const FName InPropertyName, const PropertyType& InPropertyValue);

    // Sets the property value on existing entries, returns true on success.
    template <typename PropertyType>
    bool TrySetPropertyValue(const FString& InObjectPathName, const FName InPropertyName, const PropertyType& InPropertyValue);

    // Gets the property value if found, returns true on success.
    template<typename PropertyType>
    bool GetPropertyValue(const FString& InObjectPathName, const FName InPropertyName, PropertyType& OutPropertyValue) const;

    // Sets the property value converted from the provided string value on existing entries, returns true on success.
    bool TrySetPropertyValueFromString(const FString& InObjectPathName, const FName InPropertyName, const FString& InPropertyValue);

    // Gets the property value and converts it to a string if found, returns true on success.
    bool GetPropertyValueAsString(const FString& InObjectPathName, const FName InPropertyName, FString& OutPropertyValue);
};

런타임 생성

월드 파티셔닝은 에디터에서 이루어지며 런타임에 생성(스폰)된 오브젝트는 월드 파티셔닝의 흐름에 참여하지 않는데, 다음과 같은 시나리오를 상상해 보세요:

  • 중력이 있는 공이 씬에 스폰되어 바닥에 떨어지고, 캐릭터가 바닥이 스트리밍되는 영역에서 멀어지면 공이 바닥에 떨어집니다.

이는 분명히 우리가 원하는 것이 아니며, 일반적으로 캐릭터 이외의 씬에 있는 모든 상주 오브젝트는 처음에 맵 레벨에 배치되어야 합니다.

하지만 때로는 특별한 요구 사항이 존재합니다:

  • 일부 씬 오브젝트에는 런타임 이벤트 종속성이 있을 수 있으며 런타임에 스폰되어야 합니다.

해결방안

  • 오브젝트의 위치가 고정되어 있는 경우, 플레이스홀더용 액터를 씬에 미리 배치하여 런타임에 구성할 수 있습니다.
  • 오브젝트가 특정 영역에 있는 경우 해당 영역에 트리거 볼륨을 배치하여 트리거 로직을 통해 오브젝트의 라이프사이클을 제어할 수 있습니다. 트리거 영역의 크기가 월드 파티션의 로딩 범위보다 큰 경우 트리거 볼륨은 로딩 우선 순위가 높은 메시 안에 배치하고 플로우 소스 컴포넌트를 추가해야 합니다.

캐릭터 순간이동

월드 파티션에서 캐릭터를 텔레포트(순간이동)할 때, 스트리밍에 일정 시간이 걸리기 때문에 캐릭터를 목표 위치로 바로 이동시키면 주변 오브젝트의 로딩이 아직 끝나지 않아 중력에 의해 캐릭터가 바로 땅에 떨어질 가능성이 높습니다.

해결 방법
이 문제를 해결하려면 대상 영역을 미리 로드하고 로딩이 완료될 때까지 기다려야 합니다.

  • 옵션 1: 캐릭터 틱을 끄고, 캐릭터를 타깃으로 순간이동하고, 캐릭터 자체를 스트리밍 소스로 사용하여 주변 영역의 스트리밍을 트리거하고, 폴링(FTSTicker) 대상 영역의 로딩이 완료되면 틱을 켜고, 매트릭스의 매스 프레임워크에도 해당 참조가 있습니다:

  • 옵션 2: 대상 위치에 스트리밍 소스 컴포넌트가 있는 액터를 스폰하고, 스트리밍을 켜고, 로딩이 완료되었는지 폴링한 다음, 완료되면 액터를 전송합니다:

One File Per Actor

OFPA를 사용하면 팀원들이 같은 맵의 여러 액터를 공동 편집할 수 있습니다.

기존 맵은 모든 액터를 월드 에셋 패키지에 저장하지만, UE의 오픈 월드 맵은 기본적으로 OFPA(One File Per Actor)가 활성화되어 있어 각 액터에 파일이 하나씩 있습니다.

이 파일은 프로젝트 Content 디렉터리의 ExternalActorsExternalObjects 폴더의 해당 맵 경로 아래에 저장되며, 아래 그림은 OpenWorld라는 이름의 맵에 있는 모든 액터를 나타냅니다:

씬 개요에서 액터의 실제 파일 경로를 가져올 수 있습니다:

일반적으로 모든 액터는 ExternalActors 에 저장되는 반면, ExternalObjects 에는 다양한 유형의 세팅, 아웃라인 그룹화 등 일부 (액터가 아닌) 오브젝트가 저장됩니다.

HLOD

월드 파티셔닝 스트리밍을 사용하면 스트리밍 소스의 주변 영역에만 집중하여 씬 전체에 걸친 성능 부담을 줄일 수 있습니다:

이 스트리밍은 확실히 게임 로직에 최적화되어 있지만, 시나리오의 경우 스트리밍 때문에 원거리 씬을 언로드하고 싶지 않았고, HLOD는 이 문제를 해결하기 위해 설계되었습니다.

HLOD 는 월드 파티션에 원거리 뷰를 표시하는 데 사용되며, 오픈 월드 맵에서는 노멀 맵(UE4)의 HLOD 와는 다르게 사용됩니다:

 

월드 파티션 - 계층형 레벨 오브 디테일

월드 파티션 월드에서 계층형 레벨 오브 디테일을 사용하는 방법입니다.

docs.unrealengine.com

공식 문서에도 HLOD에 대한 자세한 설명이 부족하고, 월드 파티션에서 HLOD를 사용하는 의도를 이해하지 못하는 경우 HLOD를 악용하기 쉽습니다:

  • 셀의 모든 액터를 저정밀 셀 모델 프록시에 병합하면 셀이 스트리밍 소스의 로딩 범위를 벗어날 때 이 간단한 모델을 사용하여 원거리 뷰를 표시하면서 렌더링 소비를 줄이고(저정밀 모델과 텍스처 사용), 오클루전 컬링 계산을 최적화(액터 수 감소)할 수 있습니다.

여기에서 전체 맵의 기본(디폴트) HLOD 레이어 구성을 설정할 수 있으며, 모든 액터에 적용됩니다:

다음과 같은 자산 배분 패널이 있습니다:

  • 레이어 유형: HLOD를 생성하는 데 사용할 전략
    • 인스턴스화: 해당 모델의 가장 낮은 레벨 LOD를 사용합니다(나나이트 모델인 경우 해당 LOD를 사용하여 새 나나이트 데이터를 생성합니다).
    • 병합: 파티션의 스태틱 메시 바디를 병합합니다.
    • 단순화: 파티션의 메시를 병합하고 병합된 메시를 기반으로 메시 단순화를 수행하며, 잎이 많은 나무와 같이 복잡한 지오메트리를 가진 오브젝트에 더 적합합니다.
    • 근사화: 파티션의 메쉬를 병합하고 병합된 메쉬 위에 메쉬 근사화를 수행하며, 건물이나 돌과 같이 둥근 지오메트리를 가진 개체에 더 적합합니다.
    • 사용자 지정: HLOD 생성 전략을 사용자 지정합니다.
  • 공간적으로 로드됨: 생성된 HLOD에 스트리밍을 활성화해야 하는지 여부를 선택하며, 활성화하면 이 HLOD 레이어에 대해 HLOD 그리드가 생성되고 생성된 HLODActor가 이 그리드에 배치됩니다.
    • 셀 크기: HLOD 그리드의 셀 크기(가급적 메인 그리드 셀 크기의 정수 배수)를 구성합니다.
    • 로드 범위: HLOD 그리드의 로드 범위를 구성하며, 값은 메인 그리드 셀 크기의 정수 배수여야 합니다.
    • 상위 레이어: 생성된 HLOD 단순형에 대해 상위 HLOD 레이어를 사용하여 다시 처리하면 일반적으로 더 큰 하중 범위를 갖는 더 컴팩트한 HLOD 단순형을 생성할 수 있습니다.

데이터 레이어

데이터 레이어는 오픈 월드용 UE 5의 기능으로, 아웃라인 폴더와 비슷하게 사용됩니다:

하지만 폴더와 달리 두 가지 주요 기능도 제공합니다:

  • 기본적으로 클래스의 레이어를 로드하지 않고도 편집기에서 맵을 열 수 있습니다.
  • 관련 인터페이스를 통해 런타임에 레이어를 로드 및 언로드할 수 있습니다.

데이터 레이어를 사용해야 하는 이유는 무엇인가요?

  • 데이터 레이어는 씬 오브젝트를 카테고리에 따라 레이어로, 영역에 따라 블록으로 분류하는 데 사용할 수 있습니다. 이러한 분류는 에디터에서 전체 씬을 불러오는 부담을 덜어주고, 여러 사람이 편집할 때 각자가 관심 있는 영역을 선택적으로 불러올 수 있어 대규모 씬 관리 및 여러 사람의 협업에 매우 중요하며, 제대로 처리하지 않으면 씬 제작 과정에서 혼란의 시작이 될 수 있습니다.
  • 에디터에서 분류하면 이후 씬 최적화를 위한 효과적인 기반을 제공할 수 있습니다.
  • 데이터 레이어는 월드 파티션의 흐름에 참여하는 하위 레벨입니다.

여기에서 데이터 레이어 패널을 열 수 있습니다:

마우스 오른쪽 버튼을 클릭하여 이 맵의 데이터 레이어를 만듭니다:

 

이렇게 하면 모든 맵에 공통으로 적용되는 데이터 레이어 에셋이 생성됩니다.

여기에서 액터가 속한 액터를 수동으로 설정할 수 있습니다:

그렇게 될 수도 있습니다:

또한 데이터 레이어의 현재 컨텍스트를 설정하면 새 액터가 현재 데이터 레이어에 자동으로 추가됩니다:

현재 데이터 레이어를 제거하거나 정리하려면 이 버튼을 누릅니다:

레벨의 예

레벨 인스턴싱은 UE 5에서 사용할 수 있으며, 월드 파티션 편집 경험을 개선하고 간소화하도록 설계되었습니다.

 

레벨 인스턴싱

레벨 인스턴싱 및 프로젝트에서 레벨 인스턴싱을 사용하는 방법을 소개합니다.

docs.unrealengine.com

레벨 인스턴스의 목적은 액터 세트를 그룹화하여 월드 전체에 레벨 인스턴스를 배치하고 재사용할 수 있도록 하는 것이지만, 패키지화할 때 메인 레벨의 월드 파티션에 임베드할 수 있고 UE5 데이터 레이어 사용이 아직 대중화되지 않았기 때문에 UE4에 익숙한 많은 개발팀에서는 오픈 월드 맵의 서브 레벨로 사용하는 것을 선호합니다.

레벨 인스턴스에서 액터의 런타임 그리드를 설정할 수 없던 문제도 5.3에서 수정되었습니다:

레벨 인스턴스의 액터에는 메인 월드 전용 옵션이 추가로 있는데, 체크하면 이 액터는 레벨 인스턴스 맵이 열렸을 때만 로드되며, 다른 맵에서 레벨 인스턴스로 로드할 때는 로드되지 않습니다:

오픈 월드 시나리오의 성능 최적화는 유지 관리해야 할 자산이 많고 인력으로 유지 관리하기가 어렵기 때문에 개별 자산의 최적화 개념과는 다릅니다.
따라서 가능한 한 거시적 수준에서 통합 관리 전략을 모색하고 더 큰 성능 균형을 위해 일부 작은 최적화 요구 사항을 포기하고 시나리오의 자산을 분류하고 분리하는 작업을 잘 수행해야하며 이는 매우 중요하며 성능 최적화의 중요한 기반이됩니다.
R&D 팀은 다양한 분류에 따라 그에 맞는 LOD 그룹, 텍스처 그룹, HLOD 생성 전략을 수립할 수 있습니다.
또한 전문화가 꼭 필요한 일부 에셋의 경우 전문화(화이트리스트)를 위한 기반도 유지해야 합니다.
씬 제작 프로세스의 전제는 각 참여자가 씬 제작 사양을 엄격하게 준수하는 것이므로 R&D 팀은 씬 사양을 게시하고 반복하기 위한 공개 문서 저장소를 설정하고 가능한 한 수동 검증보다는 도구에 의존하여 사람의 개입으로 인한 오류 가능성을 크게 줄이고, 관련 인력의 수동 작업량을 늘려야 하는 경우 가장 간소화된 데이터와 프로세스를 최대한 평가하는 것도 필요합니다.
관련 인력의 수작업을 늘려야 하는 경우, 가능한 한 가장 간소화된 데이터와 프로세스를 평가하는 것도 필요합니다.