PART 3 - 강의 7/7

PCG Framework

Procedural Content Generation을 통한 대규모 월드 자동 생성

01

PCG 개요

절차적 콘텐츠 생성 프레임워크의 이해

PCG(Procedural Content Generation) Framework는 UE5에서 도입된 노드 기반 절차적 생성 시스템입니다. 대규모 월드의 폴리지, 구조물, 환경 요소를 규칙 기반으로 자동 배치할 수 있습니다.

PCG Graph

노드 기반 그래프로 생성 규칙 정의. 재사용 가능한 서브그래프 지원.

PCG Component

레벨에 배치하여 PCG Graph 실행. 볼륨 기반 영역 정의.

PCG Settings

생성 파라미터 노출. 인스턴스별 커스터마이징 가능.

Partition Grid

World Partition과 통합. 그리드 기반 분산 실행.

02

PCG Graph 기본 구조

핵심 노드와 데이터 플로우

기본 PCG Graph 구조 // 일반적인 폴리지 배치 그래프 구조 [Surface Sampler] // 랜드스케이프에서 포인트 샘플링 | v [Density Filter] // 밀도 기반 필터링 | v [Slope Filter] // 경사도 기반 필터링 | v [Transform Randomizer] // 위치, 회전, 스케일 랜덤화 | v [Static Mesh Spawner] // 최종 메시 스폰 // 각 노드는 Point Data를 입력받아 변환 후 출력

주요 노드 카테고리

카테고리 주요 노드 용도
Input Surface Sampler, Landscape Data, Volume Sampler 포인트 데이터 생성
Filter Density, Bounds, Slope, Distance 포인트 필터링
Transform Transform, Copy Points, Project 포인트 변환
Output Static Mesh Spawner, Actor Spawner 최종 오브젝트 생성
03

C++ PCG 커스텀 노드

프로그래밍으로 PCG 기능 확장

CustomPCGNode.h #pragma once #include "CoreMinimal.h" #include "PCGSettings.h" #include "CustomPCGNode.generated.h" /** * 커스텀 PCG 필터 노드 * 특정 조건에 따라 포인트 필터링 */ UCLASS(BlueprintType, ClassGroup = (Procedural)) class MYGAME_API UCustomPCGFilterSettings : public UPCGSettings { GENERATED_BODY() public: UCustomPCGFilterSettings(); // 노드 이름 virtual FName GetDefaultNodeName() const override { return FName(TEXT("Custom Filter")); } // 노드 타이틀 #if WITH_EDITOR virtual FName GetNodeName() const override { return FName(TEXT("Custom Filter")); } #endif protected: virtual TArray<FPCGPinProperties> InputPinProperties() const override; virtual TArray<FPCGPinProperties> OutputPinProperties() const override; virtual FPCGElementPtr CreateElement() const override; public: // 필터 파라미터 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings", meta = (PCG_Overridable)) float MinHeight = 0.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings", meta = (PCG_Overridable)) float MaxHeight = 1000.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings", meta = (PCG_Overridable)) bool bInvertFilter = false; }; /** * PCG Element - 실제 실행 로직 */ class FCustomPCGFilterElement : public FSimplePCGElement { protected: virtual bool ExecuteInternal(FPCGContext* Context) const override; };
CustomPCGNode.cpp #include "CustomPCGNode.h" #include "PCGContext.h" #include "Data/PCGPointData.h" #include "Helpers/PCGHelpers.h" UCustomPCGFilterSettings::UCustomPCGFilterSettings() { bUseSeed = false; } TArray<FPCGPinProperties> UCustomPCGFilterSettings::InputPinProperties() const { TArray<FPCGPinProperties> Properties; Properties.Emplace(PCGPinConstants::DefaultInputLabel, EPCGDataType::Point); return Properties; } TArray<FPCGPinProperties> UCustomPCGFilterSettings::OutputPinProperties() const { TArray<FPCGPinProperties> Properties; Properties.Emplace(PCGPinConstants::DefaultOutputLabel, EPCGDataType::Point); Properties.Emplace(TEXT("Filtered Out"), EPCGDataType::Point); return Properties; } FPCGElementPtr UCustomPCGFilterSettings::CreateElement() const { return MakeShared<FCustomPCGFilterElement>(); } bool FCustomPCGFilterElement::ExecuteInternal(FPCGContext* Context) const { const UCustomPCGFilterSettings* Settings = Context->GetInputSettings<UCustomPCGFilterSettings>(); if (!Settings) { return false; } // 입력 데이터 가져오기 TArray<FPCGTaggedData> Inputs = Context->InputData.GetInputs(); TArray<FPCGTaggedData>& Outputs = Context->OutputData.TaggedData; for (const FPCGTaggedData& Input : Inputs) { const UPCGPointData* PointData = Cast<UPCGPointData>(Input.Data); if (!PointData) { continue; } // 출력 데이터 생성 UPCGPointData* PassedData = NewObject<UPCGPointData>(); UPCGPointData* FilteredData = NewObject<UPCGPointData>(); PassedData->InitializeFromData(PointData); FilteredData->InitializeFromData(PointData); TArray<FPCGPoint>& PassedPoints = PassedData->GetMutablePoints(); TArray<FPCGPoint>& FilteredPoints = FilteredData->GetMutablePoints(); PassedPoints.Reset(); FilteredPoints.Reset(); const TArray<FPCGPoint>& SourcePoints = PointData->GetPoints(); for (const FPCGPoint& Point : SourcePoints) { float Height = Point.Transform.GetLocation().Z; bool bPassFilter = (Height >= Settings->MinHeight && Height <= Settings->MaxHeight); if (Settings->bInvertFilter) { bPassFilter = !bPassFilter; } if (bPassFilter) { PassedPoints.Add(Point); } else { FilteredPoints.Add(Point); } } // 출력에 추가 FPCGTaggedData& PassedOutput = Outputs.Emplace_GetRef(); PassedOutput.Data = PassedData; PassedOutput.Pin = PCGPinConstants::DefaultOutputLabel; FPCGTaggedData& FilteredOutput = Outputs.Emplace_GetRef(); FilteredOutput.Data = FilteredData; FilteredOutput.Pin = TEXT("Filtered Out"); } return true; }
04

World Partition 통합

PCG와 스트리밍의 연동

PCG는 World Partition의 그리드 시스템과 통합되어 대규모 월드에서 효율적으로 작동합니다.

PCG Settings // PCG Component 설정에서 Partition 활성화 PCG Component > Details > Generation Generation Trigger: Generate At Runtime // 또는 Generate On Load Partition Grid Size: 25600 // World Partition Cell Size와 일치 Is Partitioned: true // 파티션 활성화 // Partition Grid Actor // World Partition 셀과 동기화되어 로드/언로드 // 각 셀은 독립적으로 PCG 실행 // 생성 결과 저장 Hierarchical Generation: Enabled // 계층적 생성 Generated Resources: Save to External // 외부 파일로 저장
런타임 vs 에디터 생성

에디터 시간 생성은 결과를 저장하여 로드 시간을 줄입니다. 런타임 생성은 시드 기반으로 동적 변화를 구현할 때 유용합니다.

05

실전 예제: 숲 생성

다층 폴리지 시스템 구현

Forest PCG Graph 구조 // 다층 숲 생성 그래프 === 나무 레이어 === [Landscape Data] | [Surface Sampler] // 밀도: 0.01 | [Slope Filter] // 최대 45도 | [Height Filter] // 0 ~ 2000m | [Distance to Distance] // 나무 간격 최소 500cm | [Weighted Random] // 나무 종류 가중치 선택 | [Transform Points] - Scale: 0.8 ~ 1.2 - Rotation Z: 0 ~ 360 | [Static Mesh Spawner] // 나무 메시들 === 관목 레이어 === [Get Attribute (Tree Points)] | [Surface Sampler] // 나무 주변 샘플링 | [Distance Filter] // 나무로부터 200~500cm | [Density Filter] // 높은 밀도 | [Static Mesh Spawner] // 관목 메시들 === 풀 레이어 === [Landscape Data] | [Surface Sampler] // 매우 높은 밀도 | [Landscape Layer Weight] // Grass 레이어만 | [Static Mesh Spawner] // ISM 사용
C++ PCG 런타임 실행 #include "PCGComponent.h" #include "PCGGraph.h" #include "PCGSubsystem.h" // PCG 그래프 런타임 실행 void AMyWorldManager::ExecutePCGForRegion(FBox Region) { if (UPCGSubsystem* PCGSubsystem = GetWorld()->GetSubsystem<UPCGSubsystem>()) { // PCG 컴포넌트 찾기 TArray<UPCGComponent*> PCGComponents; // ... 컴포넌트 수집 for (UPCGComponent* Comp : PCGComponents) { if (Comp && Comp->GetBounds().Intersect(Region)) { // 그래프 실행 요청 Comp->Generate(); } } } } // 시드 변경으로 다른 결과 생성 void AMyWorldManager::RegenerateWithSeed(UPCGComponent* Component, int32 NewSeed) { if (!Component) return; // 시드 설정 Component->Seed = NewSeed; // 기존 결과 정리 및 재생성 Component->Cleanup(); Component->Generate(); }
SUMMARY

핵심 요약

  • PCG Framework는 노드 기반 절차적 콘텐츠 생성 시스템
  • Point Data 기반으로 샘플링, 필터링, 변환, 스폰 파이프라인 구성
  • C++ 커스텀 노드로 프로젝트 특화 기능 확장 가능
  • World Partition 통합으로 대규모 월드에서 효율적 실행
  • 시드 기반으로 재현 가능한 결과와 변형 생성
PRACTICE

도전 과제

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

실습 1: PCG 그래프 기본 생성

PCG(Procedural Content Generation) 플러그인을 활성화하고, PCG Graph를 생성하세요. Surface Sampler와 Static Mesh Spawner 노드를 연결하여 랜드스케이프에 나무/바위를 자동 배치하세요.

실습 2: PCG 규칙 기반 배치

PCG 그래프에서 Density Filter, Distance Filter를 사용하여 나무가 길/건물과 겹치지 않도록 규칙을 설정하세요. Biome 데이터에 따라 식생 유형을 자동 변경하는 로직을 구현하세요.

심화 과제: C++에서 PCG 커스텀 노드 구현

UPCGSettings를 상속하여 RPG 오픈월드에 특화된 커스텀 PCG 노드를 C++로 구현하세요. 예를 들어, 거점 주변에 NPC 집을 자동 배치하거나, 던전 입구를 산악 지형에 자동 생성하는 노드를 만드세요.