KoreanFoodie's Study

이득우 언리얼 C++ 13 : 프로젝트의 설정과 무한 맵의 제작 본문

Game Dev/Unreal C++ : Tutorial

이득우 언리얼 C++ 13 : 프로젝트의 설정과 무한 맵의 제작

GoldGiver 2022. 3. 23. 00:10

이득우님의 "이득우의 언리얼 C++ 게임 개발의 정석" 책을 따라가며 실습한 내용을 정리한 포스팅입니다. 실습에 필요한 자료들은 이 링크에서, 제가 작업한 예제 소스 완성본은 여기에서 찾아보실 수 있습니다. (저는 언리얼 4.27.2 버전 기준으로 작업하였습니다)

프로젝트의 정리와 모듈 추가 

이전에 작업한 C++ 파일들은 전부 ArenaBattle 이라는 폴더 하나에 몰려 있었다. 모듈화 및 정리를 위해 폴더를 정리하자. 헤더파일은 Public 폴더에서, CPP 파일은 Private 폴더에서 보관하도록 만든다.

 

다음과 같이 구성한다. uproject 파일을 우클릭한 후 메뉴에서 Generate Visual Studio project files 을 클릭하면 솔루션 탐색기의 폴더 구조가 바뀐다. 마찬가지로 에디터의 콘텐츠 브라우저에서도 변경 사항이 업데이트 된다.

 

언리얼 엔진은 주 게임 모듈 (Primary Game Module) 을 사용해 게임 프로젝트의 로직을 관리한다. 우리가 작업한 코드는 ArenaBattle 라는 주 게임 모듈에서 관리하고 있었는데, 새 모듈을 추가해 보자.

추가 모듈 제작을 위해서는 다음의 요소들이 필요하다.

  • 모듈 폴더와 빌드 설정 파일 : 모듈 폴더와 모듈명으로 된 Build.cs 파일
  • 모듈의 정의 파일 : 모듈명으로 된 .cpp 파일 

실습에서는 Resource->Chapter13 폴더의 ArenaBattleSetting 폴더를 Source 폴더에 복사한 후, uproject 에서 비주얼 스튜디오 프로젝트를 재생성해 만들었다.

 

구조를 다음과 같이 바꾼다. 이때, Target.cs 파일에 모듈 설정을 추가해 주어야 한다.

 

ArenaBattle.Target.cs

...
ExtraModuleNames.AddRange( new string[] { "ArenaBattle", "ArenaBattleSetting" } );~
...

 

ArenaBattleSetting.Target.cs

...
ExtraModuleNames.AddRange( new string[] { "ArenaBattle", "ArenaBattleSetting" } );
...

 

빌드가 완료되면 Binaries 폴더에 새로운 파일이 생성되는지 확인한다.

 

ArenaBattleSetting DLL 이 잘 생성되면, 이제 로딩을 하도록 명령한다. uproject 파일에 새로운 모듈에 대한 정보를 기입해야 한다. ArenaBattleSetting 모듈을 먼저 실행하도록 LoadingPhase 값을 "PreDefault" 로 주고, ArenaBattle 이 ArenaBattleSetting 모듈에 의존성을 가지도록 설정한다. 그럼 ArenaBattleSetting 모듈은 항상 ArenaBattle 모듈보다 먼저 언리얼 에디터 프로세스에 올라간다.

{
	"FileVersion": 3,
	"EngineAssociation": "{A94345B7-42CD-BDAA-4D6E-AB8EB697EF1E}",
	"Category": "",
	"Description": "",
	"Modules": [
		{
			"Name": "ArenaBattleSetting",
			"Type": "Runtime",
			"LoadingPhase": "PreDefault"
		},
		{
			"Name": "ArenaBattle",
			"Type": "Runtime",
			"LoadingPhase": "Default",
			"AdditionalDependencies": [
				"Engine",
				"UMG",
				"AIModule",
				"ArenaBattleSetting"
			]
		}
	]
}

 

에디터를 로딩하면 아직 모듈 목록에 ArenaBattleSetting 이 보이지 않는다. 이는 해당 모듈에 속한 언리얼 오브젝트가 없기 때문이다. Object 를 부모로 하는 ABCharacterSetting 클래스를 생성해 넣어준다.

 

 

INI 설정과 애셋의 지연 로딩

언리얼 엔진은 언리얼 오브젝트의 기본값을 유연하게 관리하도록 외부 INI 파일에서 기본 속성 값을 지정하는 기능을 제공한다. INI 파일을 사용해 ABCharacterSetting 의 기본값을 설정해 보자.

애셋은 경로 정보만 알면 프로그램에서 이를 참조해 로딩할 수 있다. 이 애셋 경로 정보를 보관하기 위해 언리얼은 FSoftObjectPath 라는 클래스를 제공한다.

기본값을 INI 파일에서 불러들이려면 UCLASS 매크로에 config 키워드를 추가해 불러들일 INI 파일의 이름을 지정하고, 불러들일 PROPERTY 속성에는 config 키워드를 선언해야 한다. 이렇게 선언하면 언리얼 엔진은 언리얼 오브젝트를 초기화할 때 해당 속성의 값을 INI 파일에서 읽어 설정한다.

 

ABCharacterSetting.h

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ABCharacterSetting.generated.h"

/**
 * 
 */
UCLASS(config=ArenaBattle)
class ARENABATTLESETTING_API UABCharacterSetting : public UObject
{
	GENERATED_BODY()
	
public:
	UABCharacterSetting();

	UPROPERTY(config)
	TArray<FSoftObjectPath> CharacterAssets;
};

 

ABCharcterSetting.cpp

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

#include "ABCharacterSetting.h"

UABCharacterSetting::UABCharacterSetting()
{

}

UCLASS 매크로 내 config 키워드에 있는 ArenaBattle 이라는 설정으로 인해, 언리얼 엔진은 초기화 단계에서 Config 폴더에 위치한 DefaultArenaBattle.ini 파일을 읽어들여 ABCharacterSetting 의 CharacterAssets 값을 설정한다. 

 

DefaultArenaBattle.ini

[/Script/ArenaBattleSetting.ABCharacterSetting]
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Barbarous.SK_CharM_Barbarous 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/sk_CharM_Base.sk_CharM_Base 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Bladed.SK_CharM_Bladed 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Forge.SK_CharM_Forge 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_FrostGiant.SK_CharM_FrostGiant 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Golden.SK_CharM_Golden 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Natural.SK_CharM_Natural 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Pit.SK_CharM_Pit 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Ragged0.SK_CharM_Ragged0 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_RaggedElite.SK_CharM_RaggedElite 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Ram.SK_CharM_Ram 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Robo.SK_CharM_Robo 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Shell.SK_CharM_Shell 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_solid.SK_CharM_solid 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Standard.SK_CharM_Standard 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Tusk.SK_CharM_Tusk 
+CharacterAssets=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Warrior.SK_CharM_Warrior

config 폴더의 구성

 

언리얼 엔진이 초기화되면 엔진 구동에 필요한 모듈이 순차적으로 로딩된다. 모듈이 로딩되면서 모듈은 자신에게 속한 모든 언리얼 오브젝트의 기본값을 지정해 생성해내는데, 이를 클래스 기본 객체 (Class Default Object) 라고 한다. 그래서 엔진이 초기화되면 모든 언리얼 오브젝트 클래스 기본 객체가 메모리에 올라간 상태가 된다.

이렇게 메모리에 올라간 클래스 기본 객체는 GetDefault 함수를 사용해 가져올 수 있다. (엔진이 종료할 때까지 상주함)

ABCharacterSetting 언리얼 오브젝트의 클래스 기본 객체는 엔진 초기화 단계에서 생성자를 거쳐 INI 에서 설정한 값이 할당되므로, ArenaBattleSetting 모듈 이후에 로딩되는 ArenaBattle 모듈에서 GetDefault 함수를 사용하면 INI 에서 지정한 애셋의 목록 정보를 얻어 올 수 있다. 이를 위해, 먼저 ArenaBattle.Build.cs 에 ArenaBatteSetting 을 Dependency 로 추가한다.

 

ArenaBattle.Build.cs

PrivateDependencyModuleNames.AddRange(new string[] { "ArenaBattleSetting" });

 

이제 GetDefault 함수를 사용해 애셋 목록을 읽어들인 후 하나씩 로그에 출력한다.

 

ABCharacter.cpp

...
#include "ABCharacterSetting.h"

AABCharacter::AABCharacter()
{

    ...
    auto DefaultSetting = GetDefault<UABCharacterSetting>();
    if (DefaultSetting->CharacterAssets.Num() > 0)
    {
        for (auto CharacterAsset : DefaultSetting->CharacterAssets)
        {
            ABLOG(Warning, TEXT("Character Asset : %s"), *CharacterAsset.ToString());
        }
    }
}

출력 로그를 확인하면 주석처리를 하고, NPC 가 생성될 때 랜덤하게 목록 중 하나를 골라 캐릭터 애셋을 로딩하도록 기능을 변경해 보자.

언리얼 엔진은 게임 진행 중에도 비동기 방식으로 애셋을 로딩하도록 FStreamableManager 클래스를 제공한다. 이 매니저 클래스는 프로젝트에서 하나만 활성화하는 것이 좋기 때문에 우리 프로젝트에서 유일한 인스턴스로 동작하는 ABGameInstance 클래스에서 이를 멤버 변수로 선언한다.

FStreamableManager 에서 비동기 방식으로 애셋을 로딩하는 명령은 AsyncLoad 다. 해당 함수에 FStreamableDelegate 형식의 델리게이트를 넘겨줄 경우, 애셋이 로딩을 완료하면 해당 델리게이트에 연결된 함수를 호출해준다. FStreamableDelegate 형식으로 델리게이트 멤버를 선언하고 넘겨줄 수 있지만 델리게이트에서 제공하는 CreateObject 명령을 사용해 즉석으로 델리게이트를 생성함으로써 함수와 연동시킨 후 넘겨주는 방식이 간편하다.

 

ABGameInstance.h

...
#include "Engine/StreamableManager.h"

...

UCLASS ARENABATTLE_API UABGameInstance : public UGameInstance
{
    ...
    
public:
    
    ...
    
    FStreamableManager StreamableManager;

    ...
};

 

ABCharacter.h

...

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
    ...
    
private:
    
    void OnAssetLoadCompleted();
    
    ...
    
private:
    
    ...
    
    FSoftObjectPath CharacterAssetToLoad = FSoftObjectPath(nullptr);
    TSharedPtr<struct FStreamableHandle> AssetStreamingHandle;
};

 

ABCharacter.cpp

...
#include "ABGameInstance.h"

// Called when the game starts or when spawned
void AABCharacter::BeginPlay()
{
	Super::BeginPlay();
	
	if (!IsPlayerControlled())
	{
		auto DefaultSetting = GetDefault<UABCharacterSetting>();
		int32 RandIndex = FMath::RandRange(0, DefaultSetting->CharacterAssets.Num() - 1);
		CharacterAssetToLoad = DefaultSetting->CharacterAssets[RandIndex];

		auto ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
		if (nullptr != ABGameInstance)
		{
			AssetStreamingHandle = ABGameInstance->StreamableManager.RequestAsyncLoad(CharacterAssetToLoad, FStreamableDelegate::CreateUObject(this, &AABCharacter::OnAssetLoadCompleted));
		}
	}

	auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
	if (nullptr != CharacterWidget)
	{
		CharacterWidget->BindCharacterStat(CharacterStat);
	}
}

...

void AABCharacter::OnAssetLoadCompleted()
{
	USkeletalMesh* AssetLoaded = Cast<USkeletalMesh>(AssetStreamingHandle->GetLoadedAsset());
	AssetStreamingHandle.Reset();
	if (nullptr != AssetLoaded)
	{
		GetMesh()->SetSkeletalMesh(AssetLoaded);
	}
}

플레이를 누르면, NPC 에 한해 캐릭터 메시가 랜덤으로 로딩되는 것을 볼 수 있다.

 

게임 인스턴스는 유일한 인스턴스를 가지는 싱글톤(Singleton) 처럼 동작한다. 언리얼의 프로젝트 설정의 "일반 세팅"의 Default Classes 섹션의 고급 섹션에서 이를 지정할 수 있다.

 

위의 예제에서 사용한 StreamableManager 은 사실 엔진 모듈 내에 존재하는 UAssetManager 라는 오브젝트에 이미 선언돼 있다. Engine/AssetManager.h 파일을 포함한 후 UAssetManager::GetStreamableManager() 함수를 대신 사용해도 무방하다.

 

 

무한 맵의 생성

 

섹션 액터가 해야 할 일은 다음과 같다.

  • 섹션의 배경과 네 방향으로 캐릭터의 입장을 통제하는 문을 제공한다.
  • 플레이어가 섹션에 진입하면 모든 문을 닫는다.
  • 문을 닫고 일정 시간 후에 섹션 중앙에 NPC를 생성한다.
  • 문을 닫고 일정 시간 후에 아이템 상자를 섹션 내 랜덤한 위치에 생성한다.
  • 생성한 NPC가 죽으면 모든 문을 개방한다.
  • 통과한 문으로 이어지는 새로운 섹션을 생성한다.

 

섹션 액터 제작을 위해 Actor 를 부모클래스로 한 ABSection 이라는 클래스를 ArenaBattle 모듈에 생성한다.

액터에 스태틱메시 컴포넌트를 선언하고 이를 루트로 설정한 후 SM_SQUARE 애셋을 지정한다. 이제 코드를 보자.

 

ABSection.h

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

#pragma once

#include "ArenaBattle.h"
#include "GameFramework/Actor.h"
#include "ABSection.generated.h"

UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AABSection();
	virtual void OnConstruction(const FTransform& Transform) override;

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

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

private:
	enum class ESectionState : uint8
	{
		READY = 0,
		BATTLE,
		COMPLETE
	};

	void SetState(ESectionState NewState);
	ESectionState CurrentState = ESectionState::READY;

	void OperateGates(bool bOpen = true);

private:
	UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
	TArray<UStaticMeshComponent*> GateMeshes;

	UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrivateAccess = true))
	TArray<UBoxComponent*> GateTriggers;

	UPROPERTY(VisibleAnywhere, Category = Mesh, Meta = (AllowPrivateAccess = true))
	UStaticMeshComponent* Mesh;

	UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrivateAccess = true))
	UBoxComponent* Trigger;

	UPROPERTY(EditAnywhere, Category = State, Meta = (AllowPrivateAccess = true))
	bool bNoBattle;

	UFUNCTION()
	void OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult);

	UFUNCTION()
	void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	void OnNPCSpawn();

private:
	UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
	float EnemySpawnTime;

	UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
	float ItemBoxSpawnTime;

	FTimerHandle SpawnNPCTimerHandle = { };
	FTimerHandle SpawnItemBoxTimerHandle = { };
};

액터에는 에디터와 연동되는 OnConstruction이라는 특별한 함수가 설계돼 있다. 이 함수를 생성하고, 여기서 액터와 컴포넌트 속성을 설정하면 작업 중인 레벨에서도 미리 결과를 확인할 수 있다.

ESectionState 는 각각 전투 대기, 전투중, 전투 완료의 상태를 표현하기 위한 Enum 이다. 추후 OperateGates 를 통해 상태에 맞게 문을 열고 닫을 것이다.

메시 컴포넌트들은 각각 섹션 그 자체와 4개의 문들을 담을 것이다. 트리거는 해당 영역에 접근했을 때 ESectionState 를 변경하는데 사용될 것이다.

OnTriggerBeginOverlap, OnGateTriggerBeginOverlap 은 실제 트리거 영역에 캐릭터가 들어온 것을 체크한다.

OnNPCSpawn 을 통해 적을 생성하고, 아이템 박스 생성 스폰 등에 필요한 값들과 핸들이 아래에 선언되어 있다.

 

ABSection.cpp

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


#include "ABSection.h"
#include "ABCharacter.h"
#include "ABItemBox.h"

// Sets default values
AABSection::AABSection()
{
 	// 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;

	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
	RootComponent = Mesh;

	FString AssetPath = TEXT("/Game/Book/StaticMesh/SM_SQUARE.SM_SQUARE");
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_SQUARE(*AssetPath);
	if (SM_SQUARE.Succeeded())
	{
		Mesh->SetStaticMesh(SM_SQUARE.Object);
	}
	else
	{
		ABLOG(Error, TEXT("Failed to load staticmesh asset. : %s"), *AssetPath);
	}

	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
	Trigger->SetBoxExtent(FVector(775.0f, 775.0f, 300.0f));
	Trigger->SetupAttachment(RootComponent);
	Trigger->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
	Trigger->SetCollisionProfileName(TEXT("ABTrigger"));

	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnTriggerBeginOverlap);


	FString GateAssetPath = TEXT("/Game/Book/StaticMesh/SM_GATE.SM_GATE");
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_GATE(*GateAssetPath);
	if (!SM_GATE.Succeeded())
	{
		ABLOG(Error, TEXT("Failed to load staticmesh asset. : %s"), *GateAssetPath);
	}

	static FName GateSockets[] = { { TEXT("+XGate") } , { TEXT("-XGate") } , { TEXT("+YGate") } , { TEXT("-YGate") } };
	for (FName GateSocket : GateSockets)
	{
		ABCHECK(Mesh->DoesSocketExist(GateSocket));
		UStaticMeshComponent* NewGate = CreateDefaultSubobject<UStaticMeshComponent>(*GateSocket.ToString());
		NewGate->SetStaticMesh(SM_GATE.Object);
		NewGate->SetupAttachment(RootComponent, GateSocket);
		NewGate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
		GateMeshes.Add(NewGate);

		UBoxComponent* NewGateTrigger = CreateDefaultSubobject<UBoxComponent>(*GateSocket.ToString().Append(TEXT("Trigger")));
		NewGateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.0f));
		NewGateTrigger->SetupAttachment(RootComponent, GateSocket);
		NewGateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
		NewGateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
		GateTriggers.Add(NewGateTrigger);

		NewGateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnGateTriggerBeginOverlap);
		NewGateTrigger->ComponentTags.Add(GateSocket);
	}

	bNoBattle = false;

	EnemySpawnTime = 2.0f;
	ItemBoxSpawnTime = 5.0f;
}

// Called when the game starts or when spawned
void AABSection::BeginPlay()
{
	Super::BeginPlay();
	
	SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}

// Called every frame
void AABSection::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

void AABSection::SetState(ESectionState NewState)
{
	switch (NewState)
	{
	case ESectionState::READY:
	{
		Trigger->SetCollisionProfileName(TEXT("ABTrigger"));
		for (UBoxComponent* GateTrigger : GateTriggers)
		{
			GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
		}
		
		OperateGates(true);
		break;
	}
	case ESectionState::BATTLE:
	{
		Trigger->SetCollisionProfileName(TEXT("NoCollision"));
		for (UBoxComponent* GateTrigger : GateTriggers)
		{
			GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
		}
		
		OperateGates(false);

		GetWorld()->GetTimerManager().SetTimer(SpawnNPCTimerHandle, FTimerDelegate::CreateUObject(this, &AABSection::OnNPCSpawn), EnemySpawnTime, false);

		GetWorld()->GetTimerManager().SetTimer(SpawnItemBoxTimerHandle, FTimerDelegate::CreateLambda([this]() -> void {
			FVector2D RandXY = FMath::RandPointInCircle(600.0f);
			GetWorld()->SpawnActor<AABItemBox>(GetActorLocation() + FVector(RandXY, 30.0f), FRotator::ZeroRotator);
			}), ItemBoxSpawnTime, false);

		break;
	}
	case ESectionState::COMPLETE:
	{
		Trigger->SetCollisionProfileName(TEXT("NoCollision"));
		for (UBoxComponent* GateTrigger : GateTriggers)
		{
			GateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
		}

		OperateGates(true);
		break;
	}
	}
	CurrentState = NewState;
}

void AABSection::OperateGates(bool bOpen)
{
	for (UStaticMeshComponent* Gate : GateMeshes)
	{
		Gate->SetRelativeRotation(bOpen ? FRotator(0.0f, -90.0f, 0.0f) : FRotator::ZeroRotator);
	}
}

void AABSection::OnConstruction(const FTransform& Transform)
{
	Super::OnConstruction(Transform);
	SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}

void AABSection::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (CurrentState == ESectionState::READY)
	{
		ABLOG(Warning, TEXT("OnTriggerBeginOverlap : Ready -> Battle "));
		SetState(ESectionState::BATTLE);
	}
}

UFUNCTION()
void AABSection::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	ABCHECK(OverlappedComponent->ComponentTags.Num() == 1);

	FName ComponentTag = OverlappedComponent->ComponentTags[0];
	FName SocketName = FName(*ComponentTag.ToString().Left(2));
	if (!Mesh->DoesSocketExist(SocketName)) {
		ABLOG(Warning, TEXT("SocketName : '%s' does not exist!"), (*SocketName.ToString()));
		return;
	}

	FVector NewLocation = Mesh->GetSocketLocation(SocketName);

	TArray<FOverlapResult> OverlapResults;
	FCollisionQueryParams CollisionQueryParam(NAME_None, false, this);
	FCollisionObjectQueryParams ObjectQueryParam(FCollisionObjectQueryParams::InitType::AllObjects);
	bool bResult = GetWorld()->OverlapMultiByObjectType(
		OverlapResults,
		NewLocation,
		FQuat::Identity,
		ObjectQueryParam,
		FCollisionShape::MakeSphere(775.0f),
		CollisionQueryParam
	);

	if (!bResult)
	{
		auto NewSection = GetWorld()->SpawnActor<AABSection>(NewLocation, FRotator::ZeroRotator);
	}
	else
	{
		ABLOG(Warning, TEXT("New section area is not empty."));
	}
}

void AABSection::OnNPCSpawn()
{
	GetWorld()->SpawnActor<AABCharacter>(GetActorLocation() + FVector::UpVector * 88.0f, FRotator::ZeroRotator);
}

생성자에서는 SM_SQUARE, SM_GATE 등의 메시들과 트리거들을 생성한다. 예제 애셋의 SM_SQUARE 에는 8개의 소켓이 지정되어 있다.

4 개는 근접 애셋이 붙는 용도, 4개는 게이트 용도이다.

 

BeginPlay 에서는 State 를 초기화하고, SetState 에서는 State 에 맞게 트리거를 설정해준다.

이때 ABTrigger 라는 콜리전 프리셋을 사용한다.

콜리전 프리셋을 생성해 ABCharacter 와 겹침, 나머지는 무시 반응을 하도록 설정한다.

Timer 를 설정해 특정 시간 후에 적/아이템 박스가 스폰되도록 만들었다.

 

OperateGates 는 문의 회전 방향을 바꿔 문을 여닫게 만들고, OnConstruction 을 이용해 에디터에서 실행 전에 액터의 변동 사항이 보이도록 만든다.

State 를 False 로 주니 문이 열린 상태로 시작한다!

 

OnTriggerBeginOverlap 은 새로운 섹션에 들어왔을 때 READY 에서 BATTLE 로 스테이트를 변경해 준다.

OnGateTriggerBeginOverlap 은 게이트에 겹침이 감지되었을 때 해당 위치에 액터가 없을 경우 새로운 Section 을 Spawn 해 준다. 이때 어떤 문에 있는 컴포넌트인지 구분할 수 있도록 컴포넌트에 소켓 이름으로 태그(Tag) 를 설정한다.

위의 함수들은 델리게이트를 이용해 호출해준다. 

문에 접근하면 트리거가 발생해 새 섹션이 생성된다.

 

NPC 가 플레이어를 쫓아올 수 있도록 프로젝트 세팅의 네비게이션 메시 설정에서 Runtime Generation 속성의 값을 Dynamic 으로 변경해준다.

이제 아이템 박스 생성, 적 스폰, 추격 후 공격이 전부 잘 작동한다!

Comments