PART 12 · 강의 2/8

오픈월드 맵 구축

World Partition과 HLOD를 활용한 대규모 월드 구성

01

World Partition 설정

대규모 월드의 동적 스트리밍

C++
// WorldPartitionManager.h #pragma once #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "WorldPartition/WorldPartition.h" #include "WorldPartitionManager.generated.h" UCLASS() class OPENWORLDRPG_API UWorldPartitionManager : public UWorldSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; // 스트리밍 소스 추가 (플레이어 위치 기반) UFUNCTION(BlueprintCallable, Category = "WorldPartition") void RegisterStreamingSource(AActor* Source); // 특정 영역 강제 로드 UFUNCTION(BlueprintCallable, Category = "WorldPartition") void ForceLoadRegion(const FBox& Region); // 스트리밍 거리 조절 UFUNCTION(BlueprintCallable, Category = "WorldPartition") void SetStreamingDistance(float Distance); protected: UPROPERTY() TObjectPtr<UWorldPartition> WorldPartition; float DefaultStreamingDistance = 50000.0f; }; // WorldPartitionManager.cpp void UWorldPartitionManager::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); UWorld* World = GetWorld(); if (World) { WorldPartition = World->GetWorldPartition(); if (WorldPartition) { UE_LOG(LogOpenWorld, Log, TEXT("World Partition 초기화: %s"), *World->GetMapName()); } } } void UWorldPartitionManager::ForceLoadRegion(const FBox& Region) { if (!WorldPartition) return; // 비동기 로드 요청 WorldPartition->LoadRegion(Region, FWorldPartitionStreamingQuerySource(), true); } void UWorldPartitionManager::SetStreamingDistance(float Distance) { DefaultStreamingDistance = Distance; // 콘솔 변수 설정 static IConsoleVariable* CVarStreamingDistance = IConsoleManager::Get().FindConsoleVariable( TEXT("wp.Runtime.RuntimeSpatialHashCellSize")); if (CVarStreamingDistance) { // 거리에 따른 셀 크기 조정 int32 CellSize = FMath::RoundToInt(Distance / 4.0f); CVarStreamingDistance->Set(CellSize); } }
02

PCG 기반 환경 생성

절차적 폴리지 및 환경 요소 배치

C++
// PCGEnvironmentGenerator.h #pragma once #include "CoreMinimal.h" #include "PCGSettings.h" #include "PCGEnvironmentGenerator.generated.h" /** * PCG 환경 생성 노드 * 바이옴 기반으로 다양한 환경 요소 배치 */ UENUM(BlueprintType) enum class EBiomeType : uint8 { Forest, Desert, Tundra, Grassland, Swamp, Mountain }; UCLASS(BlueprintType) class OPENWORLDRPG_API UPCGBiomeSettings : public UPCGSettings { GENERATED_BODY() public: // 바이옴 타입 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Biome") EBiomeType BiomeType = EBiomeType::Forest; // 나무 밀도 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Density", meta = (ClampMin = "0.0", ClampMax = "1.0")) float TreeDensity = 0.3f; // 바위 밀도 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Density") float RockDensity = 0.1f; // 풀/덤불 밀도 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Density") float GrassDensity = 0.8f; // 바이옴별 에셋 맵 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Assets") TMap<EBiomeType, FBiomeAssetSet> BiomeAssets; protected: virtual FPCGElementPtr CreateElement() const override; }; // PCGEnvironmentGenerator.cpp class FPCGBiomeElement : public FSimplePCGElement { protected: virtual bool ExecuteInternal(FPCGContext* Context) const override { const UPCGBiomeSettings* Settings = Context->GetInputSettings<UPCGBiomeSettings>(); // 입력 지형 데이터 TArray<FPCGTaggedData> Inputs = Context->InputData.GetInputs(); // 출력 채널별 포인트 생성 UPCGPointData* TreePoints = NewObject<UPCGPointData>(); UPCGPointData* RockPoints = NewObject<UPCGPointData>(); UPCGPointData* GrassPoints = NewObject<UPCGPointData>(); for (const FPCGTaggedData& Input : Inputs) { if (const UPCGSpatialData* SpatialData = Cast<UPCGSpatialData>(Input.Data)) { GenerateBiomePoints(SpatialData, Settings, TreePoints, RockPoints, GrassPoints); } } // 출력 데이터 추가 AddTaggedOutput(Context, TreePoints, FName("Trees")); AddTaggedOutput(Context, RockPoints, FName("Rocks")); AddTaggedOutput(Context, GrassPoints, FName("Grass")); return true; } private: void GenerateBiomePoints( const UPCGSpatialData* Terrain, const UPCGBiomeSettings* Settings, UPCGPointData* Trees, UPCGPointData* Rocks, UPCGPointData* Grass) const; };
03

HLOD 시스템

원거리 렌더링 최적화

C++
// HLODManager.h #pragma once #include "CoreMinimal.h" #include "Subsystems/WorldSubsystem.h" #include "HLODManager.generated.h" UCLASS() class OPENWORLDRPG_API UHLODManager : public UWorldSubsystem { GENERATED_BODY() public: // HLOD 레이어별 전환 거리 설정 UFUNCTION(BlueprintCallable, Category = "HLOD") void SetHLODTransitionDistance(int32 Layer, float Distance); // HLOD 품질 조절 UFUNCTION(BlueprintCallable, Category = "HLOD") void SetHLODQuality(EHLODQuality Quality); // 동적 HLOD 활성화 UFUNCTION(BlueprintCallable, Category = "HLOD") void SetDynamicHLODEnabled(bool bEnabled); protected: // HLOD 레이어별 거리 TMap<int32, float> LayerDistances; };
04

지형 커스터마이제이션과 레벨 스트리밍

런타임 지형 수정 및 Data Layer 기반 콘텐츠 관리

오픈월드 RPG에서는 Landscape 지형을 런타임에 동적으로 수정하거나, Data Layer를 활용하여 콘텐츠를 논리적으로 분리해야 합니다. 이를 통해 퀘스트 진행에 따른 지형 변화나 시간대별 콘텐츠 전환 등을 구현할 수 있습니다.

C++
// TerrainModifier.h - 런타임 지형 수정 시스템 #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "LandscapeProxy.h" #include "TerrainModifier.generated.h" UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class OPENWORLDRPG_API UTerrainModifier : public UActorComponent { GENERATED_BODY() public: // 지형 높이 수정 (크레이터, 언덕 생성 등) UFUNCTION(BlueprintCallable, Category = "Terrain") void ModifyTerrainHeight(FVector WorldLocation, float Radius, float HeightDelta, UCurveFloat* FalloffCurve); // 런타임 레이어 웨이트 페인팅 UFUNCTION(BlueprintCallable, Category = "Terrain") void PaintTerrainLayer(FVector WorldLocation, float Radius, FName LayerName, float Weight); // Data Layer 활성화/비활성화 UFUNCTION(BlueprintCallable, Category = "DataLayer") void SetDataLayerActive(FName LayerName, bool bActive); protected: // 지형 수정 이력 (되돌리기 지원) UPROPERTY() TArray<FTerrainModification> ModificationHistory; // Landscape 컴포넌트 캐싱 TMap<FIntPoint, TWeakObjectPtr<ULandscapeComponent>> CachedComponents; }; // TerrainModifier.cpp void UTerrainModifier::ModifyTerrainHeight(FVector WorldLocation, float Radius, float HeightDelta, UCurveFloat* FalloffCurve) { UWorld* World = GetWorld(); if (!World) return; // 영향받는 Landscape 프록시 찾기 for (TActorIterator<ALandscapeProxy> It(World); It; ++It) { ALandscapeProxy* Landscape = *It; // 월드 좌표를 랜드스케이프 로컬 좌표로 변환 FVector LocalPos = Landscape->GetTransform() .InverseTransformPosition(WorldLocation); // 영향 범위 내 높이 데이터 수정 TArray<uint16> HeightData; int32 MinX, MinY, MaxX, MaxY; if (Landscape->GetHeightDataInRegion( LocalPos, Radius, HeightData, MinX, MinY, MaxX, MaxY)) { for (int32 Y = MinY; Y <= MaxY; ++Y) { for (int32 X = MinX; X <= MaxX; ++X) { float Dist = FVector2D::Distance( FVector2D(X, Y), FVector2D(LocalPos.X, LocalPos.Y)); float Falloff = FalloffCurve ? FalloffCurve->GetFloatValue(Dist / Radius) : FMath::Clamp(1.0f - Dist / Radius, 0.0f, 1.0f); int32 Idx = (Y - MinY) * (MaxX - MinX + 1) + (X - MinX); HeightData[Idx] += static_cast<uint16>( HeightDelta * Falloff); } } // 수정된 높이 데이터 적용 Landscape->SetHeightData(MinX, MinY, MaxX, MaxY, HeightData.GetData()); } } } void UTerrainModifier::SetDataLayerActive( FName LayerName, bool bActive) { UWorld* World = GetWorld(); if (!World) return; // Data Layer Manager에서 레이어 검색 if (UDataLayerManager* DLManager = UDataLayerManager::GetDataLayerManager(World)) { if (UDataLayerInstance* Layer = DLManager->GetDataLayerInstance(LayerName)) { EDataLayerRuntimeState NewState = bActive ? EDataLayerRuntimeState::Activated : EDataLayerRuntimeState::Unloaded; Layer->SetRuntimeState(NewState); UE_LOG(LogOpenWorld, Log, TEXT("Data Layer '%s' -> %s"), *LayerName.ToString(), bActive ? TEXT("Activated") : TEXT("Unloaded")); } } }
Data Layer 활용 패턴

Data Layer를 사용하면 같은 월드 공간에 주간/야간 콘텐츠, 퀘스트 진행 전/후 상태, 시즌 이벤트 콘텐츠 등을 레이어로 분리하여 런타임에 전환할 수 있습니다. World Partition의 셀 단위 스트리밍과 결합하면 메모리를 효율적으로 관리하면서 풍부한 콘텐츠 변화를 제공할 수 있습니다.

런타임 지형 수정 시 주의사항

Landscape의 런타임 수정은 성능 비용이 높습니다. 수정 영역이 크면 GPU 텍스처 업데이트, 콜리전 리빌드, 네비메시 재생성이 필요합니다. 가능하면 작은 영역에 한정하고, LandscapeEditLayers를 사용하여 비파괴적 편집을 적용하세요.

SUMMARY

핵심 요약

  • World Partition으로 대규모 월드 동적 스트리밍
  • PCG로 바이옴 기반 절차적 환경 생성
  • HLOD로 원거리 렌더링 최적화
  • Data Layer로 콘텐츠를 논리적으로 분리하여 런타임 전환
  • TerrainModifier로 퀘스트 진행에 따른 지형 변화 구현
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: World Partition 오픈월드 맵 생성

World Partition을 활성화한 대규모 맵을 생성하고, 랜드스케이프를 배치하세요. Cell Size를 설정하고, Loading Range를 조정하여 스트리밍 동작을 확인하세요.

실습 2: 환경 에셋 배치

Nanite 환경 메시(바위, 나무, 건물)를 배치하고, HLOD 레이어를 구성하세요. PCG로 자동 식생 배치를 설정하고, Data Layer로 콘텐츠 레이어를 분리하세요.

심화 과제: 월드 스트리밍 최적화

완성된 오픈월드를 돌아다니면서 스트리밍 성능을 프로파일링하세요. 팝인, 히칭, 메모리 스파이크를 식별하고, Cell Size, Loading Range, HLOD 설정을 조정하여 최적화하세요.