본문 바로가기

언리얼러닝/마켓플레이스 코스

[C++ 게임 만들기 1부] 사이드 스크롤러 : LevelSpawner Class And Procedural Level Generation

이 자습서 시리즈의 2부 에서는 장애물 및 레벨 클래스를 만들고 해당 클래스에서 청사진을 만들었습니다.
튜토리얼의 이 부분에서는 절차적 레벨 생성이라는 방법을 사용하여 게임에서 레벨 부분을 스폰할 것입니다.

 

레벨 스포너 클래스

C++ 클래스 -> SideRunner에서 오른쪽 클릭 -> C++ 클래스를 클릭합니다클래스가 Actor 클래스를 상속하는지 확인하고 클래스 이름을 LevelSpawner로 지정한 다음 Create Class 버튼을 클릭합니다.

Visual Studio에서 LevelSpawner.h 파일을 열고 클래스 선언 위에 다음 코드 줄을 추가합니다.

class ABaseLevel;

이 자습서 시리즈의 이전 부분에서 생성한 레벨 부분에 대한 참조를 추가할 것이기 때문에 BaseLevel 클래스를 앞으로 선언합니다.

클래스 맨 아래에 다음 줄을 추가합니다.

public:
UFUNCTION()
	void SpawnLevel(bool IsFirst);
UFUNCTION()
	void OnOverlapBegin(UPrimitiveComponent* OverlappedComp,
		AActor* OtherActor, UPrimitiveComponent* OtherComp,
		int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

함수 위의 UFUNCTION 키워드는 Unreal Engine 4(UE4) 리플렉션 시스템에서 인식하는 C++ 함수입니다. 예를 들어 이 함수가 클래스에서 사용할 C++ 함수임을 나타내기 위해 사용하는 키워드입니다.

함수 이름은 자명합니다. 이 함수를 사용하여 게임에서 레벨 부품을 생성할 것입니다.

OnOverlapBegin LevelSpawner와 다른 액터 간의 충돌을 감지하는 데 사용할 함수입니다.

앞으로 이동하여 위에서 함수를 선언한 줄 아래에 다음 줄을 추가합니다.

protected:
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level1;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level2;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level3;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level4;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level5;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level6;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level7;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level8;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level9;
UPROPERTY(EditAnywhere)
	TSubclassOf<ABaseLevel> Level10;
TArray<ABaseLevel*> LevelList;

UPROPERTY 선언의 매개변수로 EditAnywhere를 전달하고 있으며 이를 통해 LevelSpawner 클래스의 블루프린트 인스턴스에서 이 변수를 편집할 수 있습니다.

TSubclassOf를 사용하면 클래스의 변수, 예를 들어 우리가 만든 클래스의 유형인 변수를 선언할 수 있습니다. <> 사이에 클래스 이름을 전달하면 변수의 유형을 나타낼 것입니다. 이 경우에는 BaseLevel 클래스입니다.

예를 들어 Level1에서 Level10까지 모든 수준 변수에서 동일한 이름의 적절한 청사진을 첨부할 것입니다이러한 변수를 사용하여 게임에서 해당 레벨 부품을 생성할 것입니다.

LevelList 배열은 우리가 생성하는 모든 레벨 부분의 홀더 역할을 할 것입니다우리는 배열이 BaseLevel 유형이 될 것이라고 지정했으며 이 변수가 포인터이기 때문에 선언에 표시되는 *가 있고 포인터 변수를 선언할 때 표시하기 위해 *를 사용합니다.

마지막으로 방금 작성한 코드 줄 아래에 다음 줄을 추가합니다.

public:
    int RandomLevel;
    FVector SpawnLocation = FVector();
    FRotator SpawnRotation = FRotator();
    FActorSpawnParameters SpawnInfo = FActorSpawnParameters();

RandomLevel 변수를 사용하여 게임에서 스폰할 레벨 부분을 무작위화할 것입니다.

SpawnLocation SpawnRotation 변수는 스폰할 새 레벨 부분의 위치 및 회전 역할을 할 것이며 SpawnInfo는 새 액터를 스폰할 때 필요하므로 매개 변수로 전달해야 하지만 아무 작업도 수행하지 않습니다. 그것.

LevelSpawner.cpp 파일의 기능을 코딩하기 전에 완성된 LevelSpawner.h 파일을 참조용으로 남겨두겠습니다.

#pragma once

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

class ABaseLevel;

UCLASS()
class SIDERUNNER_API ALevelSpawner : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor's properties
	ALevelSpawner();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:
	// Called every frame
	virtual void Tick(float DeltaTime) override;

public:
	UFUNCTION()
		void SpawnLevel(bool IsFirst);
	UFUNCTION()
		void OnOverlapBegin(UPrimitiveComponent* OverlappedComp,
			AActor* OtherActor, UPrimitiveComponent* OtherComp,
			int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

protected:
	APawn* Player;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level1;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level2;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level3;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level4;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level5;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level6;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level7;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level8;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level9;
	UPROPERTY(EditAnywhere)
		TSubclassOf<ABaseLevel> Level10;
	TArray<ABaseLevel*> LevelList;

public:
	int RandomLevel;
	FVector SpawnLocation = FVector();
	FRotator SpawnRotation = FRotator();
	FActorSpawnParameters SpawnInfo = FActorSpawnParameters();
};

LevelSpawner.cpp 파일에 추가할 첫 번째 항목은 해당 파일에서 사용할 모든 클래스의 포함입니다파일의 첫 번째 #include 아래에 다음 줄을 추가합니다.

#include "BaseLevel.h"
#include "Engine.h"
#include "Components/BoxComponent.h"

레벨 부품 생성과 관련된 모든 마법은 SpawnLevel 함수 내에서 발생합니다해당 함수에 추가할 첫 번째 줄은 다음과 같습니다.

void ALevelSpawner::SpawnLevel(bool IsFirst)
{
	SpawnLocation = FVector(0.0f, 1000.0f, 0.0f);
	SpawnRotation = FRotator(0, 90, 0);
	if (!IsFirst)
	{
		ABaseLevel* LastLevel = LevelList.Last();
		SpawnLocation = LastLevel->GetSpawnLocation()->GetComponentTransform().GetTranslation();
	}
	RandomLevel = FMath::RandRange(1, 10);
	ABaseLevel* NewLevel = nullptr;
}

먼저 SpawnLocation SpawnRotation에 대한 값을 설정합니다. SpawnLocation 벡터의 Y축에 대한 매개변수로 1000을 설정한 이유가 궁금하다면 레벨 부분의 스케일을 10으로 설정했기 때문입니다. , 게임에서 레벨 부분의 길이는 1000단위 또는 cm가 됩니다. , 그리고 모든 수준 부분을 서로 옆에 배치하려면 1000단위씩 이동해야 합니다.

SpawnRotation Y 값도 마찬가지입니다카메라가 게임에 어떻게 배치되어 있는지, 레벨 부분을 Y축에서 90도 회전해야 제대로 보고 게임을 할 수 있습니다.

다음으로 IsFirst 매개변수를 사용하고 생성하는 레벨 부분이 첫 번째 레벨 부분이 아닌지 테스트합니다. IsFirst 변수 앞에 느낌표(!)를 사용합니다. , 값이 false이면 느낌표가 반대가 되고 이것이 true이고 값이 true이면 느낌표가 반대가 됩니다. 거짓입니다.

따라서 기본적으로 생성되는 레벨 부분이 첫 번째 레벨 부분이 아닌지 테스트하고 있습니다그 이유는 우리가 스폰하는 모든 레벨 부분을 LevelList 배열에 넣기 때문입니다. 이렇게 하면 현재 게임에 있는 레벨 부분을 추적할 수 있고 항상 마지막 레벨 부분에 액세스하여 해당 위치.항목을 가져올 수 있습니다다음 부품을 생성하려면 마지막 레벨 부품의 위치가 필요합니다.

배열의 Last 함수를 사용하여 배열에서 마지막 레벨 부분을 가져올 수 있습니다그런 다음 GetSpawnLocation 함수를 사용하여 마지막 부분의 위치에 액세스한 다음 GetComponentTransform을 호출하여 마지막 레벨 부분의 변환 구성 요소를 가져오고 마지막으로 GetTranslation 함수를 호출하여 마지막 레벨 부분의 위치를 ​​얻습니다.

다음으로 FMath RandRange 함수를 사용하여 1에서 10 사이의 임의의 숫자를 생성합니다. 이는 10개의 레벨 부분이 있고 RandRange 함수에서 반환된 숫자를 기반으로 게임에서 해당 레벨 부분을 생성하기 때문입니다.

NewLevel 변수가 있으므로 레벨에서 스폰되는 새 레벨 부분에 대한 참조를 저장할 수 있습니다새 레벨 부분에 대한 참조를 얻지 못하면 이를 LevelList 배열에 전달하고 저장할 수 없습니다.

NewLevel의 초기 값을 nullptr 또는 null 포인터로 설정하고 새 레벨 부분을 만들 때 NewLevel 변수에 저장합니다.

앞으로 NewLevel 변수를 만든 후 다음 코드 줄을 추가합니다.

if (RandomLevel == 1)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level1,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 2)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level2,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 3)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level3,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 4)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level4,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 5)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level5,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 6)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level6,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 7)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level7,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 8)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level8,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 9)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level9,
		SpawnLocation, SpawnRotation, SpawnInfo);
}
else if (RandomLevel == 10)
{
	NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level10,
		SpawnLocation, SpawnRotation, SpawnInfo);
}

이 모든 줄에서 RandomLevel 변수의 값을 테스트하고 있습니다값에 따라 적절한 레벨 부품을 생성합니다.

생성될 레벨 파트와 같은 새 액터를 저장하기 위해 NewLevel을 사용하고 있음을 알 수 있습니다.

GetWorld를 사용한 다음 세계의 SpawnActor 기능을 사용하여 새로운 레벨 부분을 만듭니다. <> 사이에 생성할 액터 유형(이 경우 BaseLevel)을 전달해야 하며 생성하려는 레벨의 복사본, 스폰 위치, 스폰 회전 및 스폰 정보를 매개변수로 전달해야 합니다.

사본은 레벨 부분 중 하나이며, 스폰 위치와 회전은 자명합니다. 액터가 스폰될 위치와 스폰된 액터의 회전을 결정하고 마지막으로 LevelSpawner.h 파일에 선언한 스폰 정보가 있고 매개변수로 필요하다고 언급했지만 자세히 사용하지는 않겠습니다.

SpawnActor 함수를 사용할 때 지정한 유형의 액터인 반환 값이 반환되어 NewLevel 변수에 저장됩니다.

다음 단계는 플레이어가 레벨 부분과 충돌할 때 감지할 수 있도록 NewLevel 변수의 트리거를 OnOverlapBegin 함수와 연결하는 것입니다.

RandomLevel 변수 테스트를 마친 후 다음 줄을 추가합니다.

if (NewLevel)
{
	if (NewLevel->GetTrigger())
	{
		NewLevel->GetTrigger()->OnComponentBeginOverlap.
			AddDynamic(this, &ALevelSpawner::OnOverlapBegin);
	}
}

먼저 NewLevel 변수가 있는지 테스트합니다. 더 정확히 말하면 NewLevel nullprt와 같지 않은지 테스트합니다. , 다음과 같이 작성할 수 있습니다.

if (NewLevel)
{
}

또는 다음과 같이 작성할 수 있습니다.

이 두 줄의 코드는 모두 같은 것을 테스트하고 있습니다. 그런 다음 GetTrigger 함수를 호출하여 NewLevel 변수의 트리거가 있는지 테스트합니다.
이것은 기본적으로 GetTrigger 함수에 의해 반환되는 NewLevel 변수의 트리거가 nullptr과 같지 않은지 테스트하기 때문에 NewLevel 변수와 동일합니다.
이 두 if 문이 모두 참이면 NewLevel의 트리거를 얻고 해당 OnComponentBeginOverlap 함수를 사용하여 액터가 NewLevel 변수의 트리거와 충돌할 때 알림을 받을 OnOverlapBegin 함수인 함수 리스너를 추가합니다.
이 트리거는 BaseLevel.h 파일에서 선언한 것입니다.

// VARIABLE DECLARED IN BaseLevel.h
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "My Triggers")
		UBoxComponent* Trigger;
그리고 레벨 블루프린트에서 생성한 것:

마지막 단계는 LevelList 배열에 NewLevel 변수를 추가하는 것입니다. 따라서 OnComponentBeginOverlap 수신기를 NewLevel의 트리거에 추가한 후 다음 코드 줄을 추가합니다.

LevelList.Add(NewLevel);
if (LevelList.Num() > 5)
{
	LevelList.RemoveAt(0);
}

먼저 Add 함수를 사용하여 NewLevel을 배열에 추가합니다. 그런 다음 LevelList 배열 내부에 있는 요소의 수가 5보다 큰지 테스트합니다. Num 함수를 사용하여 이를 수행합니다.
이것이 사실이라면 RemoveAt 함수를 사용하여 해당 배열의 첫 번째 요소를 제거하고 매개 변수로 0을 전달합니다. RemoveAt 함수는 매개 변수로 지정한 인덱스에 있는 요소를 제거하고 해당 인덱스를 0으로 설정하기 때문입니다. .
이것이 SpawnLevel 함수의 최종 버전입니다.

void ALevelSpawner::SpawnLevel(bool IsFirst)
{

	SpawnLocation = FVector(0.0f, 1000.0f, 0.0f);
	SpawnRotation = FRotator(0, 90, 0);

	if (!IsFirst)
	{
		ABaseLevel* LastLevel = LevelList.Last();
		SpawnLocation = LastLevel->GetSpawnLocation()->GetComponentTransform().GetTranslation();
	}

	RandomLevel = FMath::RandRange(1, 10);
	ABaseLevel* NewLevel = nullptr;

	if (RandomLevel == 1)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level1,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 2)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level2,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 3)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level3,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 4)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level4,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 5)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level5,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 6)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level6,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 7)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level7,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 8)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level8,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 9)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level9,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	else if (RandomLevel == 10)
	{
		NewLevel = GetWorld()->SpawnActor<ABaseLevel>(Level10,
			SpawnLocation, SpawnRotation, SpawnInfo);
	}
	if (NewLevel)
	{
		if (NewLevel->GetTrigger())
		{
			NewLevel->GetTrigger()->OnComponentBeginOverlap.
				AddDynamic(this, &ALevelSpawner::OnOverlapBegin);
		}
	}
	LevelList.Add(NewLevel);
	if (LevelList.Num() > 5)
	{
		LevelList.RemoveAt(0);
	}
}

구현을 진행하면서 모든 레벨 부분에 있는 트리거 상자의 리스너로 사용하는 OnOverlapBegin 내에서 다음 코드 줄을 추가할 것입니다.

void ALevelSpawner::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, 
			UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, 
			const FHitResult& SweepResult)
{
	SpawnLevel(false);
}

트리거 상자와 충돌하고 플레이어 액터와 충돌할 때마다 SpawnLevel 함수를 호출하고 false를 전달하여 새 레벨 부분을 만들고 레벨의 마지막 레벨 부분 위치 뒤에 스폰합니다. 우리는 LevelList 배열에서 가져오고 이것이 어떻게 작동하는지 이미 설명했습니다.
마지막 단계는 게임이 시작될 때 BeginPlay 기능에서 발생하는 초기 레벨 부분을 만드는 것입니다. BeginPlay 내부에 다음 줄을 추가합니다.

void ALevelSpawner::BeginPlay()
{
	Super::BeginPlay();

	SpawnLevel(true);
	SpawnLevel(false);
	SpawnLevel(false);
	SpawnLevel(false);
}

내가 처음 호출할 때 SpawnLevel 함수의 매개 변수로 true를 전달하고 있음을 알 수 있습니다. 이는 게임의 첫 번째 레벨 부분인 초기 레벨 부분을 스폰해야 하고, 이후 다른 레벨 부분을 생성할 때 SpawnLevel 함수에 false를 전달하기 때문입니다.
계속 진행하기 전에 Visual Studio 또는 Unreal Engine 편집기에서 클래스를 컴파일해야 합니다.

레벨 스포너 청사진

이제 LevelSpawner 클래스를 완성했으므로 여기에서 청사진을 만들 수 있습니다.
Content -> Blueprints 폴더 안에서 우클릭 -> Blueprint Class를 클릭합니다. LevelSpawner 클래스를 상속하는지 확인하십시오.

청사진의 이름을 BP_LevelSpawner로 지정하고 편집기에서 엽니다. Components 탭에서 BP_LevelSpawner(self) 최상위 상위 항목을 클릭하고 Details 탭에서 레벨 부품을 연결해야 하는 빈 필드를 찾습니다.

이러한 수준 부분을 선언할 때 선언한 모든 수준 부분에 대해 UPROPERTY에 EditAnywhere 매개 변수를 추가했기 때문에 이러한 수준 부분을 연결할 수 있습니다.
EditAnywhere 매개변수를 사용하면 해당 클래스의 블루프린트 인스턴스에서 변수를 편집할 수 있습니다.

UPROPERTY(EditAnywhere)
TSubclassOf<ABaseLevel> Level1;

없음이라고 표시된 드롭다운 목록을 클릭하고 검색 표시줄 수준 청사진에서 찾을 수 있습니다.

모든 레벨 파트 변수에 대해 올바른 레벨 파트 청사진을 연결했는지 확인하십시오.

BP_LevelSpawner에 대한 변경 사항을 컴파일하고 저장합니다이제 적절한 필드에 모든 레벨 부품을 연결했으므로 레벨에서 BP_LevelSpawner 블루프린트를 드래그하고 게임을 테스트할 수 있습니다.

 

게임을 실행하자마자 레벨 부분이 생성되는 것을 볼 수 있었습니다.

아마도 두 가지 문제를 알아차렸을 것입니다. 하나는 우리가 스파이크를 만질 때 아무 일도 일어나지 않는다는 것입니다. 이것은 우리가 기능의 해당 부분을 아직 코딩하지 않았기 때문에 정상입니다.

두 번째 문제는 게임에 더 들어갈수록 레벨 생성이 중단된다는 것입니다문제는 모든 레벨의 일부인 Trigger Box 구성 요소를 건너뛰었다는 것입니다플레이어 액터는 Trigger Box를 통과해야 하며 그런 다음 새 레벨 부분을 스폰하는 코드가 실행됩니다.

이 문제를 해결하려면 BP_Level1 청사진 내부로 이동하여 Components 탭에서 Trigger Box를 선택하고 Transform 속성에서 Z Location Z Scale을 변경합니다.

이렇게 하면 Trigger Box 구성 요소가 더 커지고 위치가 위쪽으로 이동하므로 이제 플레이어 액터가 이 구성 요소를 뛰어 넘을 수 없으며 이전처럼 새 레벨 부품을 생성하지 않는 문제가 발생하지 않습니다. .