Chapter 13. 프로젝트의 설정과 무한 맵의 제작

13.1 프로젝트 정리와 모듈 추가
Prologue)
- 소스코드를 효과적으로 관리할 수 있도록 프로젝트 구조 변경
- 게임 설정 관련 데이터를 별도의 모듈로 분리
- 게임의 기본 데이터를 INI 파일로 관리하는 방법
- 레벨의 요소를 섹션이라는 단위로 개편.
- 무한으로 증가하는 레벨을 설계
Note) 언리얼 소스코드 구조
- 언리얼 소스 코드 구조를 살펴보면, 언리얼 오브젝트에 관련된 헤더들은
Classes 폴더를 사용하고 외부에 공개하는 선언 파일은 Public 폴더에,
외부에 공개하지 않는 정의 파일은 Private 폴더에서 보관하고 있음.
- 이와 같이 프로젝트의 폴더 구조를 변경해보자.
Source > ArenaBattle 폴더로 이동한 후 Public, Private 폴더를 생성.
.h 파일은 Public 폴더로, .cpp 파일은 Private 폴더로 이동시킴.
- .vs / Binaries / Intermediate / ArenaBattle.sln 파일은 삭제시키고
다시 한 번 Generate Visual Studio project files를 진행.
컴파일 하면 콘텐츠 브라우저의 C++ 클래스 폴더 구성도 바뀜.
Note) 프로젝트의 로직 관리
- 언리얼 엔진은 주 게임 모듈(Primary Game Module)을 사용해서
게임 프로젝트의 로직을 관리함.
우리가 작업한 코드는 GWGhost라는 주 게임 모듈에서 관리하고 있음.
- 그런데 주 게임 모듈 외에 다른 모듈을 게임 프로젝트에 추가하고
로직을 분리해 관리할 수도 있음. 게임 세팅을 위한 별도의 게임 모듈을 생성해서
두 개의 모듈로 게임 프로젝트를 구성해보고자 함.
- 언리얼 에디터는 C++ 프로젝트를 생성할 때 주 게임 모듈을 자동으로 생성해주지만,
추가 모듈을 생성하는 기능은 제공하지 않음. 그래서 새로운 모듈을 추가하려면
언리얼 빌드 규칙을 이해하고, 이에 따라 폴더와 파일을 생성해야 함.
Note) 추가 모듈 제작을 위한 요소
- 모듈 폴더와 빌드 설정 파일
모듈 폴더와 모듈 명으로된 Build.cs 파일
- 모듈의 정의 파일
모듈명으로 된 .h 파일과 .cpp 파일
Note) 추가 모듈 제작
Source > 새폴더 "GhostWar5Setting"
Source > GhostWar5Setting > 새파일 "GhostWar5Setting.Build.cs"
GhostWar5Setting > 새폴더 "Public", "Private"
GhostWar5Setting > Public > 새파일 "GhostWar5Setting.h"
GhostWar5Setting > Private > 새파일 "GhostWar5Setting.cpp"
모두 작성 했다면, 다시 한번 Generate Visual Studio project files 진행.
<hide/>
// GhostWar5Setting.Build.cs
using UnrealBuildTool;
public class GhostWar5Setting : ModuleRules
{
public GhostWar5Setting(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG" });
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
<hide/>
// GhostWar5Setting.h
#pragma once
<hide/>
// GhostWar5Setting.cpp
#include "GhostWar5Setting.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE( FDefaultModuleImpl, GhostWar5Setting );
Note) 새로운 모듈의 컴파일 등록
- 비쥬얼 스튜디오에서 추가한 모듈을 빌드하도록
GhostWar5.Target.cs 파일과 GhostWar5Editor.Target.cs 파일을 수정.
- Target.cs 파일들은 각각 게임 빌드와 에디터 빌드 설정을 지정해줌.
모듈을 추가하면 빌드시 새로운 모듈을 컴파일 함.
빌드가 완료되면 Binaries 폴더에 새로운 파일이 생성됨.
<hide/>
// GhostWar5Setting.Build.cs
using UnrealBuildTool;
using System.Collections.Generic;
public class GhostWar5Target : TargetRules
{
public GhostWar5Target( TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V2;
ExtraModuleNames.AddRange( new string[] { "GhostWar5", "GhostWar5Setting" } );
}
}
<hide/>
// GhostWar5Editor.Target.cs
using UnrealBuildTool;
using System.Collections.Generic;
public class GhostWar5EditorTarget : TargetRules
{
public GhostWar5EditorTarget( TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
DefaultBuildSettings = BuildSettingsVersion.V2;
ExtraModuleNames.AddRange( new string[] { "GhostWar5", "GhostWar5Setting" } );
}
}
Note) 언리얼 에디터가 새로운 DLL 파일도 로딩하도록 명령하는 방법
- 이를 위해 uproject 파일에 새로운 모듈에 대한 정보를 기입해야 함.
모듈 정보를 기입할 때 새로운 GhostWarSetting 모듈을 다른 모듈보다 먼저
로딩하도록 LoadingPhase 값을 "PreDefault"로 설정하고 ArenaBattle 모듈이
GhostWarSetting 모듈에 대한 의존성을 가지도록 설정함.
- 그러면 GhostWarSetting 모듈은 이제 항상 GhostWar 모듈보다 먼저
언리얼 에디터 프로세스에 올라감.
<hide/>
// GhostWar5.uproject
{
"FileVersion": 3,
"EngineAssociation": "5.0",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "GhostWar5Setting",
"Type": "Runtime",
"LoadingPhase": "PreDefault",
"AdditionalDependencies": [
"Engine",
]
},
{
"Name": "GhostWar5",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
"Engine",
"UMG",
"AIModule"
]
}
],
"Plugins": [
{
"Name": "ModelingToolsEditorMode",
"Enabled": true,
"TargetAllowList": [
"Editor"
]
},
{
"Name": "VisualStudioTools",
"Enabled": true,
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/362651520df94e4fa65492dbcba44ae2",
"SupportedTargetPlatforms": [
"Win64"
]
}
]
}
Note) 콘텐츠 브라우저의 C++ 클래스에 새 모듈 보이게 설정하기
- GhostWarSetting 모듈에 속한 언리얼 오브젝트가 하나도 없기 때문에 안보임.
파일 > 새로운 C++ 클래스 > 모든 클래스 표시 > Object 부모 클래스 > "GWCharacterSetting"
- 이름 칸 바로 옆 목록에 클래스가 들어갈 모듈을 지정할 수 있음.
이 목록에서 신규로 제작한 GhostWarSetting 모듈을 발견할 수 있음.
- 주 모듈인 GhostWar 대신 GhostWarSetting을 선택하고 클래스를 생성.
다만, 언리얼 엔진 4.19와 4.20 버전에서는 GhostWarSetting 모듈에 GWCharacterSetting을
추가하면 재생성된 비주얼 스튜디오 프로젝트 설정이 꼬이는 문제가 발생함.
이를 해결하기 위해서는 에디터를 종료하고 프로젝트의 루트 폴더로 이동한 후
Generate Visual Studio project files를 진행.
- 이제 다시 한번 에디터에서 컴파일을 진하면 콘텐츠 브라우저에
GhostWarSetting 폴더가 생성됨. 그 안에 GWCharacterSetting 오브젝트가 들어있음.
13.2 INI 설정과 애셋 지연 로딩
Note) INI 파일을 사용해 GWCharacterSetting의 기본값 설정 방법
- 생성자 코드에서 캐릭터 애셋을 코드로 지정할 수 있음.
다만 애셋이 변경되면 코드를 다시 작성하고 컴파일해야 해서 번거로움.
- 새로운 모듈에 추가한 GWCharacterSetting에 캐릭터 애셋의 목록을 보관하고자 함.
언리얼 엔진은 언리얼 오브젝트의 기본값을 유연하게 관리하도록
외부 INI 파일에서 기본 멤버 값을 지정하는 기능을 제공함.
- 애셋은 경로 정보만 알면 로딩할 수 있음. 이 애셋 경로 정보를 보관하기 위해
언리얼 엔진은 FSoftObjectPath라는 클래스를 제공함.
- 언리얼 오브젝트가 기본 값을 INI 파일에서 불러들이려면
UCLASS 매크로에 config 키워드를 추가해 여기에 불러들일 INI 파일의 이름을 지정함.
대입될 멤버의 PROPERTY 속성에는 config 키워드를 선언해야 함.
- 이렇게 선언하면 언리얼 엔진은 언리얼 오브젝트를 초기화할 때
해당 속성의 값을 INI 파일에서 읽어서 설정함.
- 프로젝트 폴더 > Config > 새 파일 "DefaultGhostWar5.ini"
<hide/>
// GWCharacterSetting.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "GWCharacterSetting.generated.h"
UCLASS(config=GhostWar5) // 1. 언리얼 엔진이 초기화 단계에서 Config 폴더에 위치한 DefaultGhostWar.ini 파일을 읽어들임.
class GHOSTWAR5SETTING_API UGWCharacterSetting : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(config) // 2. 읽어들인 DefaultGhostWar.ini 파일로 CharacterAssets 멤버의 기본값을 초기화함.
TArray<FSoftObjectPath> CharacterMeshPaths;
};
<hide/>
// DefaultGhostWar5.ini
[/Script/GhostWar5Setting.GWCharacterSetting]
+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
Def) 클래스 기본 개체(Class Default Object)
- 언리얼 엔진이 초기화되면 엔진 구동에 필요한 모듈이 순차적으로 로딩됨.
- 모듈이 로딩되면서 모듈은 자신에게 속한 모든 언리얼 오브젝트의 기본값을
지정하고 생성해냄. 이를 클래스 기본 개체라고 함. 그래서 엔진이 초기화되면
모든 언리얼 오브젝트 클래스 기본 개체가 메모리에 올라간 상태임.
- 이렇게 메모리에 올라간 클래스 기본 개체는 GetDefault() 함수를 사용해 가져올 수 있음.
클래스 기본 개체는 엔진이 종료될 때까지 상주하기 때문에 언제든지 사용해도 됨.
- GWCharacterSetting 언리얼 오브젝트의 클래스 기본 개체는 엔진 초기화 단계에서
생성자를 거쳐 .ini에서 설정한 값이 할당되므로 GhostWarSetting 모듈 이후에
로딩되는 GhostWar 모듈에서 GetDefault() 함수를 사용하면
.ini 파일에서 지정한 애셋의 목록 정보를 얻어올 수 있음.

Note) GhostWar5 모듈의 GWCharacter과 GhostWar5Setting 모듈의 GWCharacterSetting
- 캐릭터 애셋 목록을 얻어오기 위한 코드를 작성해보고자 함.
GhostWar5 모듈의 빌드 규칙을 지정하는 GhostWar5.Build.cs 파일에서
GhostWar5Setting 모듈을 사용하도록 참조할 모듈 목록을 추가해야 함.
- 구현부가 모여 있는 Private 폴더에서만 GhostWar5Setting 모듈을 사용할 예정이므로
PrivateDependencyModule 항목에 이를 추가함.
- GetDefault() 함수를 사용해서 애셋 목록을 읽어들인 후 하나씩 로그에 출력
소스 코드를 모두 작성 후, 결과 파일과 폴더들 재생성.
애셋 경로를 잘 가져오는 것을 확인하면 해당 코드를 제거.
<hide/>
// GhostWar5.Build.cs
using UnrealBuildTool;
public class GhostWar5 : ModuleRules
{
public GhostWar5(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "NavigationSystem", "AIModule", "GameplayTasks" });
PrivateDependencyModuleNames.AddRange(new string[] { "GhostWar5Setting" });
}
}
<hide/>
// GWCharacter.cpp
...
#include "GWCharacterSetting.h"
AGWCharacter::AGWCharacter()
...
{
...
const UGWCharacterSetting* DefaultSetting = GetDefault<UGWCharacterSetting>();
if (0 < DefaultSetting->CharacterMeshPaths.Num())
{
for (FSoftObjectPath CharacterAsset : DefaultSetting->CharacterMeshPaths)
{
UE_LOG(LogTemp, Warning, TEXT("%s has been loaded."), *(CharacterAsset.ToString()));
}
}
}
...
Note) NPC가 생성될 때 랜덤하게 목록 중 하나를 골라 캐릭터 애셋을 로딩
- 게임 진행 중에도 비동기 방식으로 애셋을 로딩하도록
FStreamableManager라는 클래스를 제공됨. 이 매니저 클래스는
프로젝트에서 하나만 활성하는 것이 좋기 때문에 우리 프로젝트에서
유일한 개체로 동작하는 GWGameInstance 클래스에서 이를 멤버 변수로 선언.
- FStreamableManager에서 비동기 방식으로 애셋을 로딩하는 함수는 AsyncLoad()임.
해당 함수에 FStreamableDelegate 형식의 델리게이트를 넘겨줄 경우,
애셋 로딩이 완료되면 해당 델리게이트에 연결된 함수를 호출해줌.
- FStreamableDelegate 형식으로 델리게이트 멤버를 선언하고 넘겨줄 수 있지만
델리게이트에서 제공하는 CreateUObject() 함수를 사용해서
델리게이트를 생성함으로써 함수와 연동시킨 후 넘겨주는 방식이 간편함.
이를 사용해 애셋을 비동기로 로딩하는 로직을 구현하고자 함.
- 플레이 버튼을 눌러 결과를 확인. NPC에 한해 캐릭터 메시가 랜덤으로 로딩됨.
<hide/>
// GWGameInstance.h
#pragma once
...
#include "Engine/StreamableManager.h"
#include "GWGameInstance.generated.h"
...
UCLASS()
class GHOSTWAR5_API UGWGameInstance : public UGameInstance
{
...
public:
FStreamableManager StreamableManager;
private:
...
};
<hide/>
// GWGameInstance.cpp
#include "GWGameInstance.h"
UGWGameInstance::UGWGameInstance()
: StreamableManager()
{
...
}
...
<hide/>
// GWCharacter.h
...
class GHOSTWAR5_API AGWCharacter : public ACharacter
{
...
private:
...
void OnAssetLoadCompleted();
private:
...
FSoftObjectPath CurrentCharacterAsset;
TSharedPtr<struct FStreamableHandle> AssetStreamingHandle;
};
<hide/>
// GWCharacter.cpp
...
#include "GWGameInstance.h"
AGWCharacter::AGWCharacter()
...
, CurrentCharacterAsset(nullptr)
{
...
}
...
void AGWCharacter::BeginPlay()
{
...
const UGWCharacterSetting* DefaultCharacterSetting = GetDefault<UGWCharacterSetting>();
int32 RandIndex = FMath::RandRange(0, DefaultCharacterSetting->CharacterAssets.Num() - 1);
CurrentCharacterAsset = DefaultCharacterSetting->CharacterAssets[RandIndex];
UGWGameInstance* GameInstance = Cast<UGWGameInstance>(GetGameInstance());
if (nullptr != GameInstance)
{
AssetStreamingHandle = GameInstance->StreamableManager.RequestAsyncLoad(
CurrentCharacterAsset,
FStreamableDelegate::CreateUObject(this, &AGWCharacter::OnAssetLoadCompleted)
);
}
}
...
void AGWCharacter::OnAssetLoadCompleted()
{
AssetStreamingHandle->ReleaseHandle();
TSoftObjectPtr<USkeletalMesh> LoadedAssetPath(CurrentCharacterAsset);
if (true == LoadedAssetPath.IsValid())
{
GetMesh()->SetSkeletalMesh(LoadedAssetPath.Get());
}
}
Note) 예제에서 사용하는 FStreamableManager는 사실 엔진 모듈 내에 존재하는
UAssetManager라는 오브젝트에 이미 선언되어 있음.
Engine/AssetManager.h 헤더 파일을 포함한 후
UAssetManager::GetStreamableManager() 함수를 대신 사용해도 무방함.
Note) 언리얼 오브젝트를 싱글톤으로 동작하게끔 지정하는 방법
언리얼 오브젝트를 싱글톤으로 동작하게끔 지정할 수 있음.
툴바 > 세팅 > 프로젝트 세팅 > 일반 세팅 >
Default Classes > 역삼각형 클릭 > 게임 싱글톤 클래스에서 지정 가능.
13.3 무한 맵의 생성
Note) 무한 맵 스테이지를 제작에 고려사항들
이번에는 레벨을 섹션이라는 단위로 나누고 하나의 섹션을 클리어하면
새로운 섹션이 등장하는 무한 맵 스테이지를 제작하고자 함
섹션 액터가 해야 할 일은 다음과 같음.
- 섹션의 배경과 네 방향으로 캐릭터 입장을 통제하는 문을 제공함.
- 플레이어가 섹션에 진입하면 모든 문을 닫음.
- 문을 닫고 일정 시간 후에 섹션 중앙에서 NPC를 생성함.
- 문을 닫고 일정 시간 후에 아이템 상자를 섹션 내 랜덤한 위치에 생성
- 생성한 NPC가 죽으면 모든 문을 개방
- 통과한 문으로 이어지는 새로운 섹션을 생성
- 새로운 C++ 클래스 > Actor 부모 클래스 > "ABSection"
액터에 스태틱 메시 컴포넌트를 선언하고 이를 루트로 설정한 후
SM_SQUARE 애셋을 지정
<hide/>
// GWSection.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GWSection.generated.h"
UCLASS()
class GHOSTWAR5_API AGWSection : public AActor
{
GENERATED_BODY()
public:
AGWSection();
private:
UPROPERTY(VisibleAnywhere, Category=Mesh, Meta=(AllowPrivateAccess=true))
UStaticMeshComponent* StaticMesh;
};
<hide/>
// GWSection.cpp
#include "GWSection.h"
AGWSection::AGWSection()
{
PrimaryActorTick.bCanEverTick = false;
StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
RootComponent = StaticMesh;
FString AssetPath = TEXT("StaticMesh'/Game/StaticMesh/SM_SQUARE.SM_SQUARE'");
static ConstructorHelpers::FObjectFinder<UStaticMesh> SMSquare(*AssetPath);
if (true == SMSquare.Succeeded())
{
StaticMesh->SetStaticMesh(SMSquare.Object);
}
}
Note) 소켓에 포탈 부착하기
- SM_SQUARE 애셋은 방향별로 출입문과 섹션을 이어붙일 수 있게 소켓이 부착되어 있음.
SM_SQUARE 스태틱 에셋 더블클릭 > 소켓 매니저
- 배경의 각 출입구에 철문을 부착하고자 함. 철문마다 스태틱메시 컴포넌트를 제작하고
이를 소켓에 부착함. 제공하는 철문 애셋은 피봇이 왼쪽에 있으므로 부착하는 최종위치는
소켓 위치로부터 Y축으로 -80.5만큼 이동한 지점이 됨.
- 소켓 목록을 제작하고 이를 사용해 철문을 각각 부착해보자. 각각의 철문은 동일한
기능을 가지므로 TArray로 묶어 관리함.
<hide/>
// GWSection.h
...
class GHOSTWAR5_API AGWSection : public AActor
{
...
private:
...
UPROPERTY(VisibleAnywhere, Category=Mesh, Meta=(AllowPrivateAccess=true))
TArray<UStaticMeshComponent*> GateMeshes;
};
<hide/>
// GWSection.cpp
#include "GWSection.h"
AGWSection::AGWSection()
{
...
FString GateAssetPath = TEXT("StaticMesh'/Game/StaticMesh/SM_GATE.SM_GATE'");
static ConstructorHelpers::FObjectFinder<UStaticMesh> SMGate(*GateAssetPath);
if (true == SMGate.Succeeded())
{
static FName GateSockets[] = { {TEXT("+XGate")}, {TEXT("-XGate")}, {TEXT("+YGate")}, {TEXT("-YGate")} };
for (FName GateSocket : GateSockets)
{
ensure(true == StaticMesh->DoesSocketExist(GateSocket));
UStaticMeshComponent* NewGate = CreateDefaultSubobject<UStaticMeshComponent>(*(GateSocket.ToString()));
NewGate->SetStaticMesh(SMGate.Object);
NewGate->SetupAttachment(RootComponent, GateSocket);
NewGate->SetRelativeLocation(FVector(0.f, -80.5f, 0.f));
GateMeshes.Add(NewGate);
}
}
}
Note) 플레이어의 입장 감지 및 섹션 클리어 후 출구 선택 구현
- 프로젝트 세팅 > 콜리전 > Preset 섹션 > 새 프로파일 "TUTrigger"
TUCharacter만을 감지하는 콜리전 프리셋을 추가.
이 콜리전 프리셋은 플레이어의 입장을 감지하고, 섹션을 클리어 후 출구를 선택할 때 사용.
- 해당 프리셋을 사용하는 Box 컴포넌트를 생성하고 섹션의 중앙과 각 철문 영역에 부착함.

<hide/>
// TUSection.h
...
class TESTUE4_API ATUSection : public AActor
{
...
private:
...
UPROPERTY(VisibleAnywhere, Category=Trigger, Meta=(AllowPrivateAccess=true))
class UBoxComponent* Trigger;
UPROPERTY(VisibleAnywhere, Category=Trigger, Meta=(AllowPrivateAccess=true))
TArray<class UBoxComponent*> GateTriggers;
};
// TUSection.cpp
...
ATUSection::ATUSection()
{
...
Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
Trigger->SetBoxExtent(FVector(775.f, 775.f, 300.f));
Trigger->SetupAttachment(RootComponent);
Trigger->SetRelativeLocation(FVector(0.f, 0.f, 250.f));
Trigger->SetCollisionProfileName(TEXT("TUTrigger"));
...
if (true == SM_GATE.Succeeded())
{
static FName GateSockets[] = { { TEXT("+XGate")}, { TEXT("-XGate")} , { TEXT("+YGate")} , { TEXT("-YGate")} };
for (FName GateSocket : GateSockets)
{
...
UBoxComponent* NewGateTrigger = CreateDefaultSubobject<UBoxComponent>(*GateSocket.ToString().Append(TEXT("Trigger")));
NewGateTrigger->SetBoxExtent(FVector(100.f, 100.f, 300.f));
NewGateTrigger->SetupAttachment(RootComponent, GateSocket);
NewGateTrigger->SetRelativeLocation(FVector(70.f, 0.f, 250.f));
NewGateTrigger->SetCollisionProfileName(TEXT("TUTrigger"));
GateTriggers.Add(NewGateTrigger);
}
}
}
...
Note) 섹션 액터의 스테이트 기획
- 준비 스테이트
액터의 시작 스테이트. 문을 열어 놓고 대기하다가 중앙의 박스 트리거로
플레이어의 진입이 감지되면 전투 스테이트로 이동.
- 전투 스테이트
문을 닫고 일정 시간이 지나면 NPC를 소환함. 그리고 일정 시간이 지나면
랜덤한 위치에 아이템 상자도 생성함. 소환한 NPC가 죽으면 완료 스테이트로 이동.
- 완료 스테이트
닫힌 문을 연다. 각 문마다 배치한 트리거 게이트로 플레이어를 감지하면
이동한 문의 방향으로 새로운 섹션을 소환함.
캐릭터가 시작하는 섹션으로도 활용. 구분하기 위해 NoBattle 멤버 추가.
<hide/>
// TUSection.h
...
class TESTUE4_API ATUSection : public AActor
{
GENERATED_BODY()
enum class ESectionState : uint8
{
READY = 0,
BATTLE = 1,
COMPLETE = 2,
END
};
public:
...
private:
void SetState(ESectionState NewState);
void OperateGates(bool bOpen = true);
private:
...
ESectionState CurrentState;
UPROPERTY(EditAnywhere, Category=State, Meta=(AllowPrivateAccess=true))
bool bNoBattle;
};
<hide/>
// TUSection.cpp
...
ATUSection::ATUSection()
: Body(nullptr)
, GateMeshes{}
, Trigger(nullptr)
, GateTriggers{}
, CurrentState(ESectionState::READY)
, bNoBattle(false)
{
...
}
void ATUSection::BeginPlay()
{
Super::BeginPlay();
SetState(true == bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}
void ATUSection::SetState(ESectionState NewState)
{
switch (NewState)
{
case ESectionState::READY:
{
Trigger->SetCollisionProfileName(TEXT("TUTrigger"));
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);
break;
}
case ESectionState::COMPLETE:
{
Trigger->SetCollisionProfileName(TEXT("NoCollision"));
for (UBoxComponent* GateTrigger : GateTriggers)
{
GateTrigger->SetCollisionProfileName(TEXT("TUTrigger"));
}
OperateGates(true);
break;
}
default:
break;
}
}
void ATUSection::OperateGates(bool bOpen)
{
for (UStaticMeshComponent* Gate : GateMeshes)
{
Gate->SetRelativeRotation(true == bOpen ? FRotator(0.f, -90.f, 0.f) : FRotator::ZeroRotator);
}
}
Note) OnConstruction() 함수
- 섹션 액터는 완료 스테이트에서 시작하기 때문에 플레이를 누르면 모든 문이 열림.
하지만 제작 단계에서 완료 스테이트의 상황으로 모든 문이 열리도록 제작자에게
보여진다면 더욱 편리할듯.
- 액터에는 에디터와 연동되는 OnConstruction()이라는 함수가 있음.
에디터 작업에서 선택한 액터의 멤버나 트랜스폼 정보가 변경될 때
이 OnConstruction() 함수가 실행됨. OnConstruction() 함수를 미리 생성하고 여기서
액터와 컴포넌트의 속성을 설정하면 작업 중인 레벨에서도 미리 결과를 확인할 수 있다. [???]
<hide/>
// ABSection.h
...
UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
GENERATED_BODY()
public:
AABSection();
virtual void OnConstruction(const FTransform& Transform) override;
protected:
...
};
<hide/>
// TUSection.cpp
...
void ATUSection::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
SetState(true == bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}
...
Note) 박스 컴포넌트의 감지 기능
- 완료 스테이트에서는 각 철문에 있는 트리거가 활성화되고,
플레이어가 감지되면 해당 철문의 방향으로
새로운 섹션 액터를 생성하는 로직이 필요함.
- 그런데 게임 진행 상황에 따라 해당 위치에 이미 섹션 액터가 생성되어 있을 수 있음.
이를 미리 확인하는 로직이 필요함. 물리 엔진 기능을 사용해서 해당 위치에 액터가
있다면 생성을 건너 뛰도록 함.
- 새롭게 생성된 섹션 액터는 준비 스테이트에서부터 시작하는데,
가운데 트리거 영역을 활성화해서 플레이어가 들어오는지 감지해야 함.
플레이어를 감지하면 바로 전투 스테이트로 전환하고
플레이어가 빠져나가지 못하게 문을 닫음.
- 이번에는 각 박스 컴포넌트의 감지 기능을 사용해서 위 내용을 구현해보고자 함.
박스 컴포넌트의 OnComponentBeginOverlap 델리게이트에
바인딩 시킬 함수를 생성하고 이를 연결함.
해당 델리게이트는 다이내믹 델리게이트이므로,
함수 선언에 UFUNCTION을 지정해야 함.
- 총 네개의 문에 각각 네 개의 박스 컴포넌트가 있지만 이들의 기능은 동일함.
그래서 모든 문의 설치된 박스 컴포넌트의 델리게이트에 하나의 멤버 함수를 연결.
이때 감지된 박스 컴포넌트가 어떤 문에 있는 컴포넌트인지 구분할 수 있도록
컴포넌트에 소켓 이름으로 태그를 설정하고, 이를 사용해서 해당 방향에 띄울
다음 섹션액터까지 구현해보자.
<hide/>
// TUSection.h
...
class TESTUE4_API ATUSection : public AActor
{
...
public:
...
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);
protected:
...
};
<hide/>
// TUSection.cpp
...
ATUSection::ATUSection()
...
{
...
Trigger->SetCollisionProfileName(TEXT("TUTrigger"));
Trigger->OnComponentBeginOverlap.AddDynamic(this, &ATUSection::OnTriggerBeginOverlap);
...
if (true == SM_GATE.Succeeded())
{
static FName GateSockets[] = { { TEXT("+XGate")}, { TEXT("-XGate")} , { TEXT("+YGate")} , { TEXT("-YGate")} };
for (FName GateSocket : GateSockets)
{
...
NewGateTrigger->OnComponentBeginOverlap.AddDynamic(this, &ATUSection::OnGateTriggerBeginOverlap);
GateTriggers.Add(NewGateTrigger);
}
}
}
...
void ATUSection::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (ESectionState::READY == CurrentState)
{
SetState(ESectionState::BATTLE);
}
}
void ATUSection::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
check(1 == OverlappedComponent->ComponentTags.Num());
FName ComponentTag = OverlappedComponent->ComponentTags[0];
FName SocketName = FName(*ComponentTag.ToString().Left(2));
if (false == Body->DoesSocketExist(SocketName))
{
return;
}
FVector NewLocation = Body->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.f),
CollisionQueryParam
);
if (false == bResult)
{
auto NewSection = GetWorld()->SpawnActor<ATUSection>(NewLocation, FRotator::ZeroRotator);
}
}
13.4 네비게이션 매시 시스템 활용
Note) 새로운 섹션에서 내비게이션 매시 영역
- NPC와 아이템 상자가 생성될 시간을 지정할 속성을 추가하고
타이머 기능을 사용해 일정 시간 이후에 이들을 생성함.
- 해당 코드를 실행하면 이제 새롭게 생성된 섹션에 플레이어가 진입하고나서
2초 후에 가운데 위치에서 NPC가 생성되고
5초 후엔 NPC 반경 6미터 내의 랜덤한 위치에 아이템 상자가 생성됨.
<hide/>
// ABSection.h
...
UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
...
void OnNPCSpawn();
UPROPERTY(EditAnywhere, Category=Spawn, Meta=(AllowPrivateAccess=true))
float EnemySpawnTime;
UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
float ItemBoxSpawnTime;
FTimerHandle SpawnNPCTimerHandle = {};
FTimerHandle SpawnItemBoxTimerHandle = {};
};
<hide/>
// TUSection.cpp
...
#include "../TestUE4.h"
#include "TUCharacter.h"
#include "TUItemBox.h"
ATUSection::ATUSection()
...
, EnemySpawnTime(2.f)
, ItemBoxSpawnTime(5.f)
, SpawnNPCTimerHandle{}
, SpawnItemBoxTimerHandle{}
{
...
}
...
void ATUSection::SetState(ESectionState NewState)
{
switch (NewState)
{
case ESectionState::READY:
{
...
}
case ESectionState::BATTLE:
{
...
GetWorld()->GetTimerManager().SetTimer(
SpawnNPCTimerHandle, FTimerDelegate::CreateUObject(this, &ATUSection::OnNPCSpawn),
EnemySpawnTime, false);
GetWorld()->GetTimerManager().SetTimer(
SpawnItemBoxTimerHandle,
FTimerDelegate::CreateLambda([this]() -> void {
FVector2D RandXY = FMath::RandPointInCircle(600.f);
GetWorld()->SpawnActor<ATUItemBox>(GetActorLocation() + FVector(RandXY, 30.f),
FRotator::ZeroRotator);
}), ItemBoxSpawnTime, false);
break;
}
case ESectionState::COMPLETE:
{
...
}
default:
break;
}
}
...
void ATUSection::OnNPCSpawn()
{
GetWorld()->SpawnActor<ATUCharacter>(GetActorLocation() + FVector::UpVector * 88.f, FRotator::ZeroRotator);
}
...
Note) 새롭게 생성된 섹션에도 내비게이션 메시 자동생성
- NPC 캐릭터는 새로운 섹션에서 네비게이션 메시 영역이
설정되지 않았기 때문에 움직이지 못하고 생성된 위치에 가만히 서있음.
새로 생성된 섹션 영역에도 내비게이션 메시가 만들어져야 NPC가
이를 활용해 플레이로 이동할 수 있음.
- 게임 실행 중에도 동적으로 네비게이션 메시를 생성하도록 프로젝트 설정을 해줘야함.
프로젝트 세팅 > 네비게이션 메시 설정 > Runtime Generation 속성 Dynamic으로 변경.
새롭게 생성된 섹션에도 내비게이션 메시가 실시간으로 만들어져서 적용됨.
이 설정만 변경하면 새롭게 생성된 섹션 액터에도
네비게이션 시스템이 만들어져서 NPC는 플레이어를 쫒아옴.