이번 강좌에서는 블루프린트 없이 C++코드와 INI 파일만으로 클래스를 초기화하는 방법에 대해 알아보겠습니다.

이를 위해 이전 블루프린트 강좌에서 제작했던 캐릭터를 C++만 사용해 다시 제작해봅시다.

강좌 진행을 위해 마켓플레이스에서 Inifinity Blade Warriors 패키지를 다운받읍시다.
Weapon을 다운받았던 방식과 동일하게 Infinity로 검색하고 현재 프로젝트에 추가하면
InfinityBladeWarriors라는 폴더가 프로젝트에 추가됩니다. 

Infinity Blade Warriors 패키지

 

 임포트된 캐릭터

 

 

이제 에디터를 띄우고 [파일 > C++클래스추가] 버튼을 누른 후,
부모클래스를 Pawn을 선택해 플레이어가 조종할 수 있는 액터인 폰을 제작합시다.
이 폰의 이름은 ABPawn이라고 지읍시다.
그러면 아래와 같이 APawn을 상속받은 AABPawn라는 클래스가 생성됩니다.

UCLASS()
class ABC_API AABPawn : public APawn
{
	GENERATED_BODY()

public:
	// Sets default values for this pawn's properties
	AABPawn();

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

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

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};

 


폰의 컴포넌트

 

이 폰은 아래와 같이 총 다섯 개의 컴포넌트를 가지고 있습니다.
컴포넌트의 클래스 이름은 앞에 U자를 붙이면 얼추 맞습니다.  

  1. 캡슐 컴포넌트 : 인간형 폰의 충돌과 이동을 관리하는 루트 컴포넌트입니다. 
  2. 스켈레탈 메시 컴포넌트 : 인간형 폰의 비주얼을 담당하는 컴포넌트입니다.
  3. 화살 컴포넌트 : 편집을 위해 시선 방향을 가리키는 컴포넌트입니다. 
  4. 스프링암 컴포넌트 : 카메라 구도를 쉽게 세팅할 수 있게 제공하는 컴포넌트입니다.
  5. 카메라 컴포넌트 : 폰에 빙의한 플레이어의 화면을 렌더링할 카메라 컴포넌트입니다.  

아래는 이 컴포넌트를 선언하고 세팅한 코드입니다.  
저는 스켈레탈 메시 컴포넌트를 설정할 때,
네 번째에 위치한 Cardboard 캐릭터의 리소스 경로를 사용해 지정했습니다.
그리고 추가로 HP 설정을 위한 MaxHP, CurrentHP 두 변수도 함께 추가합시다. 

 

//AABPawn.h

public:
    UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Collision")
	class UCapsuleComponent* Body;

	UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Visual")
	class USkeletalMeshComponent* Mesh;

	UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Helper")
	class UArrowComponent* Arrow;


	UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Camera")
	class USpringArmComponent* SpringArm;

	UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Camera")
	class UCameraComponent* Camera;

	UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Stat")
	float MaxHP;

	UPROPERTY(BlueprintReadWrite, EditInstanceOnly, Category = "Stat")
	float CurrentHP;

 

//ABPawn.cpp

AABPawn::AABPawn()
{
	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;


	Body = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	RootComponent = Body;

	Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
	Mesh->SetupAttachment(Body);

	Arrow = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
	Arrow->SetupAttachment(Body);
	
	SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
	SpringArm->SetupAttachment(Body);
	
	Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
	Camera->SetupAttachment(SpringArm);

	
	Body->SetCapsuleSize(34.0f, 88.0f);
	Mesh->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -88.0f), FRotator(0.0f, -90.0f, 0.0f));

	static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_Warrior(TEXT(
		"SkeletalMesh'/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Shell.SK_CharM_Shell'"));

	if (SK_Warrior.Succeeded())
	{
		Mesh->SetSkeletalMesh(SK_Warrior.Object);
	}
	   	

	SpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));
	SpringArm->TargetArmLength = 650.0f;

	MaxHP = 100.0f;

}

// Called when the game starts or when spawned
void AABPawn::BeginPlay()
{
	Super::BeginPlay();
	CurrentHP = MaxHP;
}

 

 

완성된 ABPawn을 컴파일하고 레벨에 드래그해 배치해보면 아래와 같이 컴포넌트가 구성된 것을 확인할 수 있습니다. 

 

C++ 캐릭터 폰의 제작 결과 


INI 파일 설정기능



현재 MaxHP값의 초기값은 100.0인데, 보다시피 소스에 기본 값이 하드코딩 되어 있습니다.
이를 상속받은 블루프린트에서 변경할 수도 있겠지만,
현재 구현된 코드는 아무래도 확장성과 유연성이 떨어지는 방식이라고 할 수 있겠습니다.
하지만 이를 가만히 보고 있을 언리얼 엔진은 아니지요.

블루프린트나 별도의 에디터 작업이 없어도, C++ 클래스의 기본 값을 코드가 아닌
외부에서 유연하게 설정할 수 있도록 언리얼 엔진은 INI 파일 설정 기능을 제공하고 있습니다. 

원래 INI파일은 윈도우 관련 운영체제에서 시스템 구성요소의 설정을 위해서 고안된 파일 형식입니다.
INI 파일의 형식은 속성과 값, 그리고 이들을 포괄하는 섹션으로 구성되어 있습니다.

언리얼 엔진에서는 이 형식을 이렇게 사용합니다.

1. 섹션 : 현재 프로젝트에서 사용하는 C++ 클래스 식별자.

2. 속성 : C++ 클래스에서 INI를 사용하도록 지정한 UPROPERY 멤버 변수.

3. 값 : 속성에 지정할 값.

 

C++클래스 식별자는 현재 프로젝트에서 고유한 식별자로 구분되어 있으며 아래와 같은 형식을 가지게 됩니다.

{스크립트경로}/{모듈이름}.{클래스이름}

 

스크립트 경로는 항상 /Script로 시작되며,

모듈이름은 언리얼 오브젝트가 속한 모듈 이름,
클래스이름은 언리얼 오브젝트의 이름이 됩니다.
따라서 현재 우리가 제작한 ABPawn의 C++ 클래스 식별자는 "/Script/ArenaBattle.ABPawn"이 됩니다. 

언리얼 엔진에서의 INI의 실제 사례를 확인하기 위해 프로젝트 폴더의 Config 폴더에 가봅시다. 

Config 폴더에 있는 DefaultEngine.ini 파일을 메모장으로 열어보면
이미 아래와 같은 설정이 들어 있는 것을 확인할 수 있습니다. 

//Config 폴더의 DefaultEngine.ini 파일
[/Script/EngineSettings.GameMapsSettings]
GameInstanceClass=/Script/ABC.ABCGameInstance

 

내용을 잘 살펴보면 우리가 지난 강좌에서
프로젝트 설정에서 지정한 게임 인스턴스 역할을 하는 클래스가 GameInstanceClass라는 프로퍼티에 설정되어 있음을 유추할 수 있을 겁니다.

현재 이 값이 설정된 섹션을 보면 /Script/EngineSettings.GameMapsSettings로 되어 있습니다.  

이는 EngineSettings라는 모듈에 있는 GameMapSettings라는 C++ 언리얼 오브젝트 클래스의 GameInstanceClass 변수의 값을 /Script/ArenaBattle.ABGameInstance로 지정하겠다라는 의미가 되겠습니다.

 

정말 소스코드에서 이런 언리얼 오브젝트와 변수가 있는지 다같이 찾아봅시다.

비주얼 스튜디오의 언리얼 소스 프로젝트인 UE4 프로젝트를 열고 Source의 Runtime 폴더로 갑시다.
이 폴더는 언리얼 엔진에서 에디터/게임이 사용하는 기본 모듈들을 포함하고 있습니다.

Runtime의 하단에 있는 폴더를 잘 살펴보면 이전에 WebService 모듈 구조를 만들면서 설명드린 언리얼 엔진의 모듈 규칙을 정확히 준수하고 있음을 확인할 수 있습니다.

여기서 EngineSettings라는 폴더를 살펴보면 눈에 익숙한 Build.cs 파일이 보일겁니다.
따라서 이 폴더는 EngineSettings라는 모듈임을 유추할 수 있습니다.

다시 EngineSettings 폴더를 뒤져보면 일반적으로 언리얼 오브젝트 선언을 담아두는 Classes 폴더안에 GameMapSettings라는 언리얼 오브젝트가 있습니다.

 

GameMapSettings 언리얼 오브젝트의 확인


config=Engine

config 설정을 DefaultEngine.ini 파일에서 읽어온다

 

이제 GameMapSettings.h 헤더파일을 열어서 UGameMapSettings 클래스 선언을 살펴봅시다.

클래스에 멤버 변수로 GameInstanceClass라는 변수가 UPROPERTY 매크로와 함께 정의된 것이 보일겁니다.
자세히보면 UPROPERTY에는 config라는 키워드가 선언되어 있습니다.

이는 언리얼 오브젝트 선언에서 지정한 INI 설정 파일에서 기본 값을 읽어오겠다는 의미입니다. 

다시 올라가 언리얼 오브젝트 클래스 선언을 보면 상단 UCLASS 매크로에 config=Engine이라고 지정되어 있습니다.

이는 이 언리얼 오브젝트의 config 설정을 DefaultEngine.ini 파일에서 읽어온다는 의미가 되겠습니다.
( 참고로 언리얼 엔진 설치 폴더 내 Config 폴더에는 BaseEngine.ini 파일이 있으며, 여기에 모든 기본값이 세팅되어 있습니다. 이 기본 값을 오버라이드(override)하려면 DefaultEngine.ini 내 동일한 섹션에 필드와 값을 지정해주면 기본 값을 덮어쓰게 됩니다. )

아래 코드에서 관련된 부분을 노란색으로 색칠했으니 확인해보시면 언리얼 엔진이 INI 파일을 어떻게 활용하는지 대략 이해가 되실겁니다. 

 

GameMapSettings.h

 


INI 구성 및 읽어오기

언리얼 엔진에서는 INI를 여러개 제공하고 있으며,

엔진 기능에 관련된 INIDefaultEngine.ini 파일로,
게임 로직에 관련된 INIDefaultGame.ini 파일을 통해 관리하도록 구성을 제공하고 있습니다.

우리는 엔진 로직이 아니고 게임에 관련된 세팅이기 때문에 DefaultGame.ini 파일을 사용하도록 예제를 진행해봅시다. 

메모장으로 DefaultGame.ini 파일을 생성하고 아래와 같이 입력합시다. 

//DefaultGame.ini
[/Script/ArenaBattle.ABPawn]
MaxHP=1000.0

 

 

이제 언리얼 오브젝트가 INI 파일에서 설정 값을 읽어올 수 있도록 합시다.
ABPawn.h를 열고 설정을 아래와 같이 변경해봅시다.

 

ABPawn.h

 

이렇게 소스 코드를 변경하면, 언리얼 에디터를 닫고 컴파일을 한 후에 다시 띄웁시다.
언리얼 에디터는 로딩시에 처음 INI파일을 초기화하기 때문에, 에디터를 한번 닫고 다시 로딩하는 것이 확실합니다. 

이제 다시 에디터를 로딩하고 ABPawn을 레벨에 드래그한 후, 플레이버튼을 누릅시다.
그러면 BeginPlay 함수 로직에 의해서 우리가 INI에 세팅했던,
MaxHP 값이 CurrentHP에 대입되 결국 인스턴스에서만 보이는 CurrentHP 값이 1000이 되었음을 확인할 수 있습니다.

 

게임실행시에 보임

 


레퍼런싱

 

이 예제에서 알 수 있듯이 CDO 생성 시점에서는 INI 값이 적용되지 않습니다.
BeginPlay 함수와 같이 게임플레이 런타임에서야 비로소 INI 값은 효력을 발휘하게 되지요. 

이번에는 게임플레이 런타임에서 랜덤하게 다른 캐릭터 애셋을 하나 골라서 로딩하도록 코드를 업데이트해봅시다.

이전 강좌에서 애셋을 로딩하기 위해 사용한 생성자 코드의 ConstructorHelpers 클래스 용도는
게임 시작 전 단계에서 애셋이 정확히 있는지 검증하기 위한 목적이 있다고 설명드렸습니다.

하지만     시작전에 검증되어야 할 필수 애셋이 있는 반면,
있던 없던 스트리밍 방식으로 천천히 로딩되도 큰 상관이 없는 애셋도 있습니다.  

전자를 하드 레퍼런싱(Hard Referencing),  
후자를 소프트 레퍼런싱(Soft Referencing)이라고 하는데,

이번 강좌에서는 소프트 레퍼런싱 방식으로 로딩해봅시다. 

소프트 레퍼런싱 방식을 사용하려면
애셋의 정보를 가져올 때에는           FStringAssetReference 구조체를,
애셋의 클래스 정보를 가져올 때에는 FStringClassReference 구조체에 경로 정보를 지정해주면 됩니다.

우리는 18종류의 캐릭터 중에서 하나를 랜덤하게 생성해야 하기 때문에,
언리얼의 배열인 TArray를 사용해 멤버 변수를 아래와 같이 선언해줍시다.

 

//ABPawn.h
{
private:
	UPROPERTY(config)
	TArray<FStringAssetReference> CharacterAssets;
}    

 

그리고 DefaultGame.ini 파일을 아래와 같이 업데이트합시다.
언리얼 엔진에서 TArray 배열을 사용하는 경우에는 +/- 기호를 사용해 배열값을 추가, 삭제하는 것이 가능합니다.
( 참고로 애셋 경로 사용시 작은따옴표 바깥쪽 부분은 생략해도 무방합니다. )

//DefaultGame.ini
MaxHP=1000.0
+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

 

이제 게임플레이 로직에서 CharacterAssets에 들어간 변수 중 하나를 랜덤으로 선택해 로딩하도록 지정합시다.

언리얼 엔진에서 런타임에서 애셋을 로딩하기 위해서FStreamableManager라는 구조체를
언리얼 오브젝트에서 선언하고 이를 통해 로딩하도록 고안되어 있습니다.

저는 게임 인스턴스 클래스에서 이를 지정해, 모든 액터들이 이를 사용할 수 있도록 지정하겠습니다. 

아래 코드는 수정된 게임 인스턴스의 선언입니다.
뭔가 설명이 복잡해보여도 사실 단순히 멤버 변수만 하나 추가해주면 됩니다.

 

//ABCGameInstance.h
class ABC_API UABCGameInstance : public UGameInstance
{
	GENERATED_BODY()
	

public:
	UABCGameInstance();
	virtual void Init() override;

	UPROPERTY()
		class UWebConnection* WebConnection;

	UPROPERTY()
		class UWebConnection* WebConnectionNew;

	UPOPERTY()//  4.2 버전에서는 타입오류가 나오므로, 제거 하는 게 좋다
		FStreamableManager AssetLoader;

};

 

//ABPawn.cpp
#include "ABCGameInstance.h"

void AABPawn::BeginPlay()
{
	Super::BeginPlay();
	CurrentHP = MaxHP;
	int32 NewIndex = FMath::RandRange(0, CharacterAssets.Num() - 1);

	UABCGameInstance* ABCGameInstance = Cast<UABCGameInstance>(GetGameInstance());

	if (ABCGameInstance)
	{
		TAssetPtr<USkeletalMesh> NewCharacter = Cast<USkeletalMesh>(ABCGameInstance->AssetLoader.SynchronousLoad(CharacterAssets[NewIndex]));
		if (NewCharacter)
		{
			Mesh->SetSkeletalMesh(NewCharacter.Get());
		}
	}
}

 

플레이 버튼을 누르면 아래와 같이 시작할 때마다 랜덤하게 다른 캐릭터들로 변신하는 것을 볼 수 있습니다. 

실행시 마다 에샛이 다름

 

이번 강좌에는 동기화방식을 사용해서 애셋이 로딩이 완료될 때 까지 게임 플레이 로직이 멈춥니다.  

비동기방식으로 애셋을 로딩하는 방법은 Delegate 를 학습한 후에 차후에 보강하겠습니다.

 

+ Recent posts