프로그래밍/언리얼

언리얼 절차적 던전 생성

추향 2024. 11. 18. 18:26

던전 생성

언리얼에서 절차적으로 던전을 생성하기 위해 작성한 코드입니다.

작동은 하지만 미완성입니다.

간단한 설명.

1. Main Room을 생성하고, Arrow List(ChildrenArrow)를 가져옵니다.

2. 'ChildrenArrow'에서 랜덤으로 Arrow를 선택합니다.

3. 선택한 Arrow의 위치에 Room을 스폰합니다.

4 스폰된 방이 겹치는지 확인합니다:

4.1 충돌이 발생하면 방을 삭제하고 다른 Arrow로 시도합니다.

4.2 충돌이 발생하지 않으면 방을 유지합니다.

5. Room생성에 성공하면 Room의 Arrow를 'ChildrenArrow'에 추가합니다.

 

이렇게 Room Num을 만족하거나 Arrow없어질 때까지 3-5의 과정을 반복합니다.

실행

실행한 결과는 다음과 같습니다.

방은 두개만 넣어서 실행했습니다.

https://youtu.be/xYDS74XW1Aw

 

주요 코드.

Generator Room.h

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EscapeMapGenerator.generated.h"

class ARoom;
class ACorridor;

UCLASS()
class ESCAPE_API AEscapeMapGenerator : public AActor
{
	GENERATED_BODY()
public:	
    UPROPERTY(EditAnywhere, Category = "Dungeon Settings")
    int32 RoomNum = 30;
protected:
	AEscapeMapGenerator();
	virtual void BeginPlay() override;
    TArray<USceneComponent*> ChildrenArrow;
    void GenerateDungeon();
    void SpawnNextRoom(FVector SpawnLoc, FRotator SpawnRot, const TSubclassOf<ARoom>& InRoomClass);
    UPROPERTY(EditAnywhere, Category = "Dungeon Settings")
    TArray<ARoom*> Rooms;
    UPROPERTY(EditAnywhere, Category = "Dungeon Settings")
    TArray<ACorridor*> Corridors;
    UPROPERTY(EditAnywhere, Category = "Dungeon Settings")
	TSubclassOf<ARoom> MainRoomClass;
    UPROPERTY(EditAnywhere, Category = "Dungeon Settings")
    TArray<TSubclassOf<ARoom>> RoomClasses;
private:
    FTimerHandle RoomSpawnDelay;
    int32 CurrentRoomCount=0;
};

Generator Room.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "MapGen/EscapeMapGenerator.h"
//구성요소.
#include "MapGen/Room.h"
#include "MapGen/Corridor.h"

#include "Kismet\KismetMathLibrary.h"
#include "Kismet/KismetSystemLibrary.h"
//Component
#include "Components/BoxComponent.h"
//Debug용 커스텀 코드.
#include "DrawDebugHelpers.h"
#include "CustomDebugHelper.h"
// Sets default values
AEscapeMapGenerator::AEscapeMapGenerator()
{
	PrimaryActorTick.bCanEverTick = false;
}
void AEscapeMapGenerator::BeginPlay()
{
	Super::BeginPlay();
	GenerateDungeon();
}
void AEscapeMapGenerator::GenerateDungeon()
{
	//Spawn MainRoom
	if (RoomNum>0) {
		SpawnNextRoom(FVector::ZeroVector, FRotator::ZeroRotator, MainRoomClass);
		CurrentRoomCount = 1;
		//Spawn Other Room
		GetWorld()->GetTimerManager().ClearTimer(RoomSpawnDelay);
		GetWorld()->GetTimerManager().SetTimer(RoomSpawnDelay, FTimerDelegate::CreateLambda([this]() {
			if (CurrentRoomCount < RoomNum && ChildrenArrow.Num() > 0) {
				int32 RandomClassIndex = FMath::RandRange(0, RoomClasses.Num() - 1);
				int32 RandomIndex = FMath::RandRange(0, ChildrenArrow.Num() - 1);
				USceneComponent* SelectArrow = ChildrenArrow[RandomIndex];
				ChildrenArrow.RemoveAt(RandomIndex); // 사용된 Arrow를 제거
				SpawnNextRoom(SelectArrow->GetComponentLocation(), SelectArrow->GetComponentRotation(), RoomClasses[RandomClassIndex]);
			}
			else {
				Debug::Print("End_Generator");
				Debug::Print(FString::FormatAsNumber(ChildrenArrow.Num()));

				GetWorld()->GetTimerManager().ClearTimer(RoomSpawnDelay);
			}
		}), 0.1f, true);
	}
}
void AEscapeMapGenerator::SpawnNextRoom(FVector SpawnLoc,FRotator SpawnRot,const TSubclassOf<ARoom>& InRoomClass)
{
	FActorSpawnParameters SpawnParams;
	SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
	//Spawn
	ARoom* RoomActor = GetWorld()->SpawnActor<ARoom>(InRoomClass, SpawnLoc, SpawnRot, SpawnParams);
	TArray<AActor*> OverlappingActors;
	FCollisionQueryParams QueryParams;
	QueryParams.AddIgnoredActor(RoomActor);  // RoomActor는 검사에서 제외됨
	RoomActor->BoxComponent->GetOverlappingActors(OverlappingActors, ARoom::StaticClass());
	OverlappingActors.Remove(RoomActor);
	if (OverlappingActors.Num() > 0)
	{
		Debug::DrawDebugBox(RoomActor->BoxComponent,true);
		RoomActor->Destroy(); return;
	}
	else
	{
		Debug::DrawDebugBox(RoomActor->BoxComponent);
		// 겹침이 없는 경우: Children 업데이트 및 후속 작업 수행
		CurrentRoomCount++;
		TArray<USceneComponent*> NewChildrenArrow;
		RoomActor->ArrowFolder->GetChildrenComponents(false, NewChildrenArrow);
		ChildrenArrow.Append(NewChildrenArrow); return;
	}
}

Room.h

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Room.generated.h"

class UBoxComponent;
class UArrowComponent;
UCLASS()
class ESCAPE_API ARoom : public AActor
{
	GENERATED_BODY()
public:	
	UPROPERTY(EditDefaultsOnly,BlueprintReadOnly)
	USceneComponent* ArrowFolder;
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	UBoxComponent* BoxComponent;
protected:
	ARoom();
	virtual void BeginPlay() override;
	UPROPERTY(EditDefaultsOnly)
	USceneComponent* GeometryFolder;
	USceneComponent* RootSceneComponent;
	UPROPERTY(EditDefaultsOnly)
	UArrowComponent* ActorArrowComponent;
};

Room.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "MapGen/Room.h"
#include "Components/BoxComponent.h"
#include "Components/ArrowComponent.h"
// Sets default values
ARoom::ARoom()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false;
	RootSceneComponent =CreateDefaultSubobject<USceneComponent>(TEXT("RootSceneComponent"));
	SetRootComponent(RootSceneComponent);
	ActorArrowComponent =CreateDefaultSubobject<UArrowComponent>(TEXT("ActorArrowComponent"));
	ActorArrowComponent->SetupAttachment(GetRootComponent());
	GeometryFolder=CreateDefaultSubobject<USceneComponent>(TEXT("GeometryFolder"));
	GeometryFolder->SetupAttachment(GetRootComponent());
	ArrowFolder =CreateDefaultSubobject<USceneComponent>(TEXT("ArrowFolder"));
	ArrowFolder->SetupAttachment(GetRootComponent());
	BoxComponent=CreateDefaultSubobject<UBoxComponent>(TEXT("BoxComponent"));
	BoxComponent->SetupAttachment(GetRootComponent());
}
void ARoom::BeginPlay()
{
	Super::BeginPlay();
}

Blueprint

이런식으로 Room을 구성합니다.

Arrow의 위치를 기반으로 Room을 생성하기 때문에 문이 위치한 지점에 Arrow를 배치합니다.

Room을 생성하면 0,0,0의 위치에서 생성되기 때문에 이것을 생각해서 Room을 구현합니다.

boxComponent를 이용해 충돌체크를 하기 때문에 방 크기에 맞춰 사용합니다.