PCG Framework
Procedural Content Generation을 통한 대규모 월드 자동 생성
PCG 개요
절차적 콘텐츠 생성 프레임워크의 이해
PCG(Procedural Content Generation) Framework는 UE5에서 도입된 노드 기반 절차적 생성 시스템입니다. 대규모 월드의 폴리지, 구조물, 환경 요소를 규칙 기반으로 자동 배치할 수 있습니다.
PCG Graph
노드 기반 그래프로 생성 규칙 정의. 재사용 가능한 서브그래프 지원.
PCG Component
레벨에 배치하여 PCG Graph 실행. 볼륨 기반 영역 정의.
PCG Settings
생성 파라미터 노출. 인스턴스별 커스터마이징 가능.
Partition Grid
World Partition과 통합. 그리드 기반 분산 실행.
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 | 최종 오브젝트 생성 |
C++ PCG 커스텀 노드
프로그래밍으로 PCG 기능 확장
#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;
};
#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;
}
World Partition 통합
PCG와 스트리밍의 연동
PCG는 World Partition의 그리드 시스템과 통합되어 대규모 월드에서 효율적으로 작동합니다.
// 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 // 외부 파일로 저장
에디터 시간 생성은 결과를 저장하여 로드 시간을 줄입니다. 런타임 생성은 시드 기반으로 동적 변화를 구현할 때 유용합니다.
실전 예제: 숲 생성
다층 폴리지 시스템 구현
// 다층 숲 생성 그래프
=== 나무 레이어 ===
[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 사용
#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();
}
핵심 요약
- PCG Framework는 노드 기반 절차적 콘텐츠 생성 시스템
- Point Data 기반으로 샘플링, 필터링, 변환, 스폰 파이프라인 구성
- C++ 커스텀 노드로 프로젝트 특화 기능 확장 가능
- World Partition 통합으로 대규모 월드에서 효율적 실행
- 시드 기반으로 재현 가능한 결과와 변형 생성
도전 과제
배운 내용을 직접 실습해보세요
PCG(Procedural Content Generation) 플러그인을 활성화하고, PCG Graph를 생성하세요. Surface Sampler와 Static Mesh Spawner 노드를 연결하여 랜드스케이프에 나무/바위를 자동 배치하세요.
PCG 그래프에서 Density Filter, Distance Filter를 사용하여 나무가 길/건물과 겹치지 않도록 규칙을 설정하세요. Biome 데이터에 따라 식생 유형을 자동 변경하는 로직을 구현하세요.
UPCGSettings를 상속하여 RPG 오픈월드에 특화된 커스텀 PCG 노드를 C++로 구현하세요. 예를 들어, 거점 주변에 NPC 집을 자동 배치하거나, 던전 입구를 산악 지형에 자동 생성하는 노드를 만드세요.