KoreanFoodie's Study

언리얼 엔진 개념 간단 정리 2부 (이득우님 블로그 리뷰) 본문

Game Dev/Unreal C++ : Study

언리얼 엔진 개념 간단 정리 2부 (이득우님 블로그 리뷰)

GoldGiver 2022. 10. 5. 11:23

언리얼 엔진의 개념과 동작 원리 복습

이득우님의 블로그를 보며, 배운 내용을 간단히 정리해보려 한다! 언리얼을 처음 시작한다면, 이득우님이 쓰신 책인 "이득우의 언리얼 C++ 개임 개발의 정석" 을 꼭 읽어보는 것을 추천한다! 이 블로그에도 강좌 내용을 정리해 놓았지만, 실제로 읽어보는 것을 적극 추천한다.



8. 액터의 제작

액터는 월드에 배치될 수 있는, 월드 트랜스폼이 있는 언리얼 오브젝트라고 할 수 있다. 게임 컨텐츠의 설계는 액터에서부터 시작한다고 할 수 있다.

액터를 세팅할 때는 아래와 같이 애셋을 불러와 적합한 메시에 적용하면 된다. 자세한 구현은 이 글을 참고하자.

static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_BlackKnight(TEXT("SkeletalMesh'/Game/InfinityBladeWeapons/Weapons/Blade/Swords/Blade_BlackKnight/SK_Blade_BlackKnight.SK_Blade_BlackKnight'"));

Weapon->SetSkeletalMesh(SK_BlackKnight.Object);

아래 토막글들은 블로그의 내용을 그대로 발췌한 것이다.

먼저 ConstructorHelpers 클래스는 말 그대로 생성자(Constructor)에서만 사용이 가능하다. 즉 CDO 제작에만 사용된다는 이야이다. 만일 게임플레이 런타임에서 애셋을 로딩하기 위해서는 ConstructorHelper가 아닌 StaticLoadObject와 같은 다른 API를 사용해야 한다. 이렇게 애셋을 로딩 시점을 엔진 초기화 런타임과 게임플레이 런타임으로 구분하는 이유는, 안전을 위해서이다. 

일반적으로 컨텐츠에서 사용하는 애셋들은 엔진 초기화 시점에서 우리가 사용할 애셋이 확실히 존재하는지 검증하고 다음 단계를 진행하는 것이 안전하다. 만일 런타임에서 애셋을 로딩하게 된다면 예기치 않은 문제들이 발생할 수 있다. 예를 들어 우리가 제작한 무기를 게임 플레이 런타임에서 로딩한다면, 캐릭터가 등장하고 열심히 탐험하고 몬스터 공격을 위해 칼을 빼들 때야 비로서 로딩을 시작한다. 그런데 이 때 칼 애셋이 없는 경우에는 잘못해서 콘텐츠가 크래시가 일어날 수 있다. 

불러들이는 애셋의 경로는 특별한 이유가 없다면 로딩된 이후에 변경될 일이 없을테니, 사용할 애셋들은 ConstructorHelpers 클래스를 사용해 애셋의 유무를 미리 확인하고 로딩하는 것이 좋다.

ConstructorHelpers 클래스를 사용해 제작한 변수에는 static 키워드를 앞에 붙였다. 이 구문은 스태틱 키워드를 사용하지 않아도 동작에는 무방하지만 이렇게 스태틱 변수로 선언한 이유는 리소스는 공유해서 사용하는 자원이므로 모든 언리얼 오브젝트 인스턴스마다 애셋 정보를 로딩할 필요가 없기 때문이다. 그래서 ConstructorHelpers 로 변수를 선언할 때에는 관련 인스턴스들이 공유해서 쓰도록 로컬 스태틱 변수로 선언하는 것이 일반적이다.

 

 

9. C++ 에서 블루프린트의 확장

C++ 클래스를 상속받은 블루프린트로 만든 언리얼 오브젝트를 에디터에서 어떻게 편집할 수 있는지 간단히 알아보자.

언리얼 오브젝트가 블루프린트에서 상속받을 수 있는 클래스가 되려면 UCLASS 매크로 안에 다음과 같은 두 가지 타입을 넣어 주어야 한다.

UCLASS(BlueprintType, Blueprintable ...

class ENGINE_API AActor : public UObject

{

따라서 우리가 만든 블루프린트가 AActor 를 상속받는 경우, 키워드를 따로 선언하지 않아도 된다(이미 위의 키워드가 선언되어 있음).

멤버 변수의 권한은 UPROPERTY( ... ) 로 설정할 수 있다. 각 값들의 용도는 다음과 같다(Edit 과 Visible 이 앞에 붙는다).

예시는 다음과 같다.

UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
class USkeletalMeshComponent* Weapon;

UPROPERTY(BlueprintReadWrite, EditInstanceOnly)
float BaseDamage;

추가로, 카테고리와 meta 속성 들을 추가할 수 있다.

private:
    UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Stat", meta=(AllowPrivateAccess="true"))
    float BaseDamage;

AllowPrivateAccess 를 넣으면 C++ 코드에서의 캡슐화와 에디터에서의 접근이 동시에 가능해진다!

 

 

10.  INI 설정과 런타임 애셋 로딩

이번 항목은 거의 모든 내용을 그대로 가져왔다.

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

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

  1. 섹션 : 현재 프로젝트에서 사용하는 C++ 클래스 식별자.
  2. 속성 : C++ 클래스에서 INI를 사용하도록 지정한 UPROPERY 멤버 변수.
  3. 값 : 속성에 지정할 값.

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

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

스크립트 경로는 항상 /Script로 시작되며, 모듈이름은 언리얼 오브젝트가 속한 모듈 이름, 클래스이름은 언리얼 오브젝트의 이름이 된다. 예를 들어, ABPawn의 C++ 클래스 식별자는 "/Script/ArenaBattle.ABPawn"이 된다.

 

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

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

[/Script/EngineSettings.GameMapsSettings]

GameInstanceClass=/Script/ArenaBattle.ABGameInstance

내용을 잘 살펴보면 우리가 지난 강좌에서 프로젝트 설정에서 지정한 게임 인스턴스 역할을 하는 클래스가 GameInstanceClass라는 프로퍼티에 설정되어 있음을 유추할 수 있다(게임 인스턴스 클래스 설정임). 현재 이 값이 설정된 섹션을 보면 /Script/EngineSettings.GameMapsSettings로 되어 있다.

이는 EngineSettings라는 모듈에 있는 GameMapSettings라는 C++ 언리얼 오브젝트 클래스의 GameInstanceClass 변수의 값을 /Script/ArenaBattle.ABGameInstance로 지정하겠다라는 의미가 된다. 정말 소스코드에서 이런 언리얼 오브젝트와 변수가 있는지 다같이 찾아보자.

/* GameMapSettings.h */

UCLASS(config=Engine, defaultconfig)
class ENGINESETTINGS_API UGameMapsSettings
    : public UObject
{
    GENERATED_UCLASS_BODY()

   /// 중략

    /** The class to use when instantiating the transient GameInstance class */
    UPROPERTY(config, noclear, EditAnywhere, Category=GameInstance, meta=(MetaClass="GameInstance"))
    FStringClassReference GameInstanceClass;

언리얼 엔진에서는 INI를 여러개 제공하고 있으며 엔진 기능에 관련된 INI는 DefaultEngine.ini 파일로, 게임 로직에 관련된 INI는 DefaultGame.ini 파일을 통해 관리하도록 구성을 제공하고 있다. 우리는 엔진 로직이 아니고 게임에 관련된 세팅이기 때문에 DefaultGame.ini 파일을 사용하도록 예제를 진행해 보겠다.

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

[/Script/ArenaBattle.ABPawn]

MaxHP=1000.0

ABPawn.h 는 다음과 같이 변경하면 된다.

UCLASS(config=Game)

class ARENABATTLE_API AABPawn : public APawn
{
    GENERATED_BODY()

public:

	/// 중략 

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

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

이번에는 게임플레이 런타임에서 랜덤하게 다른 캐릭터 애셋을 하나 골라서 로딩하도록 코드를 업데이트해 보자. 이전 강좌에서 애셋을 로딩하기 위해 사용한 생성자 코드의 ConstructorHelpers 클래스 용도는 게임 시작 전 단계에서 애셋이 정확히 있는지 검증하기 위한 목적이 있다고 설명했다. 하지만 시작전에 검증되어야 할 필수 애셋이 있는 반면, 있던 없던 스트리밍 방식으로 천천히 로딩되도 큰 상관이 없는 애셋도 있다. 전자를 하드 레퍼런싱(Hard Referencing), 후자를 소프트 레퍼런싱(Soft Referencing)이라고 하는데, 이번 강좌에서는 소프트 레퍼런싱 방식으로 로딩해보자. 

소프트 레퍼런싱 방식을 사용하려면 애셋의 정보를 가져올 때에는 FStringAssetReference 구조체를, 애셋의 클래스 정보를 가져올 때에는 FStringClassReference 구조체에 경로 정보를 지정해주면 된다. 우리는 18종류의 캐릭터 중에서 하나를 랜덤하게 생성해야 하기 때문에, 언리얼의 배열인 TArray를 사용해 멤버 변수를 아래와 같이 선언해주자.

private:
    UPROPERTY(config)
    TArray<FStringAssetReference> CharacterAssets;

그리고 DefaultGame.ini 파일을 아래와 같이 업데이트하자. 언리얼 엔진에서 TArray 배열을 사용하는 경우에는 +/- 기호를 사용해 배열값을 추가, 삭제하는 것이 가능하다.

[/Script/ArenaBattle.ABPawn]
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 
... 나머지 + 로 시작하는 애셋 16개

이제 게임플레이 로직에서 CharacterAssets에 들어간 변수 중 하나를 랜덤으로 선택해 로딩하도록 지정하자. 언리얼 엔진에서 런타임에서 애셋을 로딩하기 위해서는 FStreamableManager라는 구조체를 언리얼 오브젝트에서 선언하고 이를 통해 로딩하도록 고안되어 있다. 게임 인스턴스 클래스에서 이를 지정해 모든 액터들이 이를 사용할 수 있도록 지정하겠다.

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

/* ABGameInstance.h */
UPROPERTY()
FStreamableManager AssetLoader;

/* ABPawn.cpp */
void AABPawn::BeginPlay()
{
    Super::BeginPlay();
    
    int32 NewIndex = FMath::RandRange(0, CharacterAssets.Num() - 1);
    UABGameInstance* ABGameInstance = Cast<UABGameInstance>(GetGameInstance());

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

 

 

11. 언리얼 C++ 델리게이트

C# 에서 사용했던 델리게이트와 비슷하다. 문법과 사용법만 간단히 짚고 넘어가도록 하자. 자세한 내용은 이전에 블로그에 정리한 글을 참고하자.

 

 

12. 시리얼라이제이션(Serialization)

자세한 내용은 이전에 블로그에 정리한 글을 참고하자.

 

 

13. 언리얼 스마트 포인터 삼총사와 메모리 관리

이 역시 자세한 내용은 이전에 블로그에 정리한 글을 참고하자.

Comments