1. 퍼시스턴트 레벨과 서브(스트리밍) 레벨로 나뉜다.
- 퍼시스턴트 레벨은 월드에 포함되는 고정 레벨
- 서브 레벨은 필요에 따라 붙였다 뗏다 할 수 있는 레벨

2. 퍼시스턴트 레벨에 여러 개의 서브 레벨을 부착할 수 있다.
- 필요에 따라 초기 로딩하도록 설정이 가능.
- 서브 레벨의 원점 트랜스폼을 조절하면 다양하게 활용하는 것이 가능


 

1. 퍼시스턴트 레벨 :  월드에는 최소 하나의 지정되는 레벨
2. 스트리밍 레벨 : 월드에 무관하게 추가/삭제가 가능하도록 설계된 레벨

언리얼에서 게임 컨텐츠 구조는 
ABGameInstance와 WebConnection간의 관계와 동일하게 부모-자식 관계가 확장된 구조를 따릅니다. 

언리얼에서 모든 게임 컨텐츠의 기초은 월드에서부터 시작됩니다.

월드에는 최소 하나의 레벨이 지정되는데, 이를 퍼시스턴트 레벨(Persistent Level)이라고 합니다.
이 퍼시스턴트 레벨에는 추가로 월드의 초기 설정 값들을 지정한 월드세팅 언리얼 오브젝트가 있습니다. 

기본 생성된 월드에는 레벨을 실시간으로 추가할 수 있는데,
월드에 무관하게 추가/삭제가 가능하도록 설계된 레벨 스트리밍 레벨(Streaming Level)이라고 합니다.

이러한 레벨은 월드에서 독립적으로 행동하는 단위 오브젝트인 액터(Actor)의 묶음으로 구성되어 있습니다.

이 액터들은 다시 컴포넌트들로 구성되어 있지요.

지금까지 설명드린 월드의 계층 구성은 아래와 같이 정리할 수 있습니다. 

 

월드의 계층 구조


이렇게 생성된 계층 구조는 설계에 따라 
기본서브오브젝트 방식으로 관리할지, 런타임에서 실시간으로 생성해 관리할지 방식이 나뉘어지게 됩니다. 

설명을 위해 월드 내에서 게임의 룰을 관장하는 액터인 게임모드를 가지고 예시를 들어보겠습니다. 
FPS 게임을 제작할 때 레벨 디자이너가 레벨을 공들여서 제작했습니다. 

이렇게 제작된 레벨에 플레이어들이 접속해 플레이를 할 때에는 
데스매치(DeathMatch)룰을 적용할지, 
깃발 뺏기(Capture the Flag) 룰을 적용할지를
방장이 정하도록 유연하게 적용할 수 있게 만드는 것이 좋은 디자인일 겁니다.  
(레벨마다 변경이 될수 있으므로, 다이나믹인게 좋다?)

이 상황에서 월드세팅 오브젝트가 
퍼시스턴트레벨 오브젝트에 스태틱 방식인 기본서브오브젝트로 포함되어 버리면, 월드세팅마다 레벨이 하나씩 만들어져야 합니다.
이러면 중복된 맵으로 인해 용량이 낭비되겠지요.
그래서 월드에서 월드세팅과 게임 모드는 후자인 다이나믹 방식으로 로딩되게 설계되어 있습니다. 

반면에 액터 설계자가 지정한 컴포넌트의 조합에 따라 월드에서 스스로 동작하도록 설계되어 있습니다. 
그래서 액터는 스태틱하게 컴포넌트와 함께 생성하고 다 같이 소멸하는 것이 효과적입니다.

그래서 액터와 컴포넌트는 전자인 스태틱하게 로딩하도록 설계되어 있습니다.

아래는 이를 정리한 도식입니다. 

 

월드내 오브젝트 생성 방식의 구분 ( 파란색이 다이나믹 방식, 붉은색이 스태틱 방식 )

'Unreal > Concept' 카테고리의 다른 글

Actor, Pawn  (0) 2019.05.29
UObject , AActor  (0) 2019.05.29
레퍼런싱 - 작성 중  (0) 2019.05.29
언리얼 INI 파일 설정 기능  (0) 2019.05.29
BlueprintImplementableEvent, BlueprintNativeEvent  (0) 2019.05.28

이번 강좌에서는 블루프린트 없이 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 를 학습한 후에 차후에 보강하겠습니다.

 

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

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

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


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

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

 

//ABPawn.h
{
private:
	UPROPERTY(config)
	TArray<FStringAssetReference> CharacterAssets;
}    
//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

 

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

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

 

//ABCGameInstance.h
class ABC_API UABCGameInstance : public UGameInstance
{
public:

	UPOPERTY()
		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());
		}
	}
}

 

 

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

 


 

하드 레퍼런싱

'Unreal > Concept' 카테고리의 다른 글

UObject , AActor  (0) 2019.05.29
Level (레벨)  (0) 2019.05.29
언리얼 INI 파일 설정 기능  (0) 2019.05.29
BlueprintImplementableEvent, BlueprintNativeEvent  (0) 2019.05.28
ConstructorHelpers + 애셋 로딩 시점관련  (0) 2019.05.28

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

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

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

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

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

 

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

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

 

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

 


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

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

 

DefaultGame.ini

 


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

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

 

Character.h

 

 

ini HP값을 적용시킴

'Unreal > Concept' 카테고리의 다른 글

Level (레벨)  (0) 2019.05.29
레퍼런싱 - 작성 중  (0) 2019.05.29
BlueprintImplementableEvent, BlueprintNativeEvent  (0) 2019.05.28
ConstructorHelpers + 애셋 로딩 시점관련  (0) 2019.05.28
액터 검색 및 순회 방법  (0) 2019.05.27

이번에는 많이들 궁금해하는 C++와 블루프린트의 연동에 대해 알아보겠습니다.

언리얼 프로젝트 제작에 있어서 개발 방식을 C++로만 해야할지, 블루프린트만 사용 할지,
두 언어를 혼합할지를 결정하는 것은 어려운 일입니다.

팀의 구성이나 개발자 분들의 선호에 따라 정답이란 존재하지 않는 부분이기 때문이지요. 

이번 강좌에서는 여러분들의 참고를 위해서 C++ 클래스를 블루프린트로 확장하는 방법에 대해 알아보겠습니다. 


 

지난 강좌에서 제작한 AWeapon 클래스를 상속받은 블루프린트를 만들어보겠습니다.

콘텐츠 브라우저에서 [신규추가 > 블루프린트 클래스]를 선택하고 부모 클래스를 Weapon으로 지정해 BP_CppWeapon이라는 블루프린트를 제작합시다.

결과는 아래와 같이 블루프린트 클래스가 생성되고 Weapon을 상속받은 BP_CppWeapon이라는 클래스가 새롭게 생성된 것을 확인할 수 있습니다. 

C++ 클래스를 상속받은 블루프린트 클래스의 제작

 

이제 이 블루프린트를 더블클릭해서 열어보면,
이전에 제작한 컴포넌트들이 (상속됨)이라는 형태로 이미 만들어져 있는 것을 볼 수 있습니다.

이전에 제작한 BP_Weapon과 유사하지만 다만 Weapon이라는 스켈레탈메시 컴포넌트를 선택해도 오른쪽
디테일 윈도우가 텅 비어 있어서 아무 것도 할 수 없다는 점이 다릅니다. 
(디테일 창 아무것도 안보인다.)

컴포넌트 수정이 불가능하게 제작된 블루프린트의 구성

이러한 이유는 C++에서 제작한 컴포넌트가 블루프린트에서 어떻게 보여질지를 우리가 지정하지 않았기 때문입니다.

 


언리얼오브젝트 권한

 

기본적으로 C++에서 제작된 언리얼 오브젝트나 데이터는 블루프린트에서는 편집이나 수정할 수 없는 것이 원칙입니다. 따라서 BP_Weapon과 같이 만들려면 C++에서 블루프린트에 충분한 권한을 주어야 합니다

이러한 권한은 클래스 / 멤버 변수 / 멤버 함수의 세 종류에서 지정할 수 있습니다.

[3번은 제일 밑에 있음]

1. 클래스의 권한

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

1. BlueprintType : 이 C++ 클래스는 블루프린트에서 변수로 선언이 가능한 타입임을 지정합니다.

2. Blueprintable : 블루프린트에서 이 C++ 클래스를 상속받아서, 새롭게 클래스를 확장할 수 있습니다. 

하지만 우리가 제작한 AWeapon 클래스는 위의 두 가지 키워드를 선언하지 않았는데도 불구하고,
블루프린트에서 잘 상속받아서 잘 쓰고 있는 상황입니다.

이것은 AWeapon이 상속받은 AActor에 이미 위의 두 선언이 들어가 있기 때문입니다.  

AActor를 선택하고 F12키를 눌러서 AActor의 정의를 보면
아래와 같이 BlueprintType과 Blueprintable이 가장 먼저 선언되어 있음을 확인할 수 있습니다.
따라서 액터 AActor를 상속받은 C++ 클래스는 블루프린트에서 상속 가능하고,
블루프린트에서 변수형으로 지정할 수 있습니다.  

UCLASS(BlueprintType, Blueprintable ...

class ENGINE_API AActor : public UObject
{

 

2. 멤버 변수의 권한 

멤버 변수의 권한UPROPERTY 매크로를 통해서 지정할 수 있습니다.
블루프린트 로직에서 해당 멤버 변수에 읽기쓰기를 할 수 있는지, 읽기만 하는지에 대한 권한을 각각 BlueprintReadWrite, BlueprintReadOnly 값을 지정할 수 있습니다. 

우리가 생성한 스켈레탈메시 컴포넌트인 Weapon 변수에 대해 BluerprintReadWrite 키워드를 부여한다고 가정해봅시다.

이 변수의 UPROPERTY(BlueprintReadWrite)라고 선언하면 스켈레탈 메시 컴포넌트는 CDO에서 액터와 함께
정적으로 생성되도록 지정했는데 블루프린트에서 오브젝트 값을 변경한다는 의미가 되어버립니다.

이렇다면 굳이 이 컴포넌트를 정적으로 생성하는 것이 의미가 없을 뿐더러, 엔진에서도 혼란스러워 할 것입니다.
따라서 CreateDefaultSubobject로 생성하는 정적인 컴포넌트들은 모두 UPROPERTY(BlueprintReadOnly)로 선언해야 초기 설계 개념에도 부합한 선언이 되겠습니다.

정리하자면 컴포넌트를 선언에 사용된 멤버 변수는 BluerprintReadOnly을 지정해야 한다라고 이해하시면 됩니다. 

이번에는 언리얼 오브젝트가 아닌 일반 밸류에 사용하는 float 형 변수 BaseDamage를 클래스 선언에 추가하겠습니다.
(이후 설명부터는 float , string , int32등과 같이 스택을 사용해 값을 전달하는 타입을 밸류타입이라고 통칭해서 이야기하겠습니다.) 

블루프린트에서 검의 데미지를 항시 고정시키려면 UPROPERTY(BlueprintReadOnly)를 사용하고 물약 효과 등에 의해 검의 데미지를 블루프린트에서 변경하고 싶다면 UPROPERTY(BlueprintReadWrite)로 선언해주면 됩니다.  

아래는 C++의 두 멤버 변수의 블루프린트 권한을 지정한 코드입니다. 

//weapon.h   
{
public:
  UPROPERTY(BlueprintReadOnly)           //메시 못 바꾸게
  class USkeletalMeshComponent* Weapon;  

  UPROPERTY(BlueprintReadWrite)          //대미지는 변경 되게
  float BaseDamage;     
}

 

컴파일을 완료한 후 BP_CppWeapon을 열고,
내 블루프린트 윈도우의 눈동자 모양의 옵션 버튼을 누르고 상속된 변수를 체크합시다.

그러면 아래와 같이 Weapon이라는 그룹에 BaseDamage 변수가 나타나는 것을 확인할 수 있습니다.
이것이 블루프린트에서 상속받은 변수입니다. Weapon과 BaseDamage를 각각 이벤트 그래프로 끌어다 놓으면,
Weapon은 읽기만, BaseDamage는 읽기 쓰기가 가능하게 UI가 나옵니다. 

상속받은 변수의 표시 

 

 


에디터 인터페이스에서 권한

 

편집가능여부 / 편집화면출력 여부

 

변수가 상속받아서 블루프린트 로직에서 읽거나 쓰기가 가능한 것은 확인했지만,
여전히 Weapon 스켈레탈메시 컴포넌트는 블루프린트에서 편집이 안되고,
BaseDamage 변수의 값도 편집할 수 있는 공간이 없습니다.

이는 블루프린트 스크립트의 읽고 쓰기 권한과

무관하게 에디터 인터페이스에서 변수를 편집할 수 있게 지정해주어야 하기 때문입니다. 

이를 지정하는 키워드는 여섯 개나 되어서 조금 복잡합니다.
아래의 여섯 가지 중 하나를 변수의 성격과 쓰임새에 따라 여러분들이 직접 지정해주어야 합니다.

  1. EditDefaultsOnly 
  2. EditInstanceOnly
  3. EditAnywhere
  4. VisibleDefaultsOnly
  5. VisibleInstanceOnly
  6. VisibleAnywhere

여섯 가지 키워드를 잘 살펴보면

전자인 Edit / Visible 그룹과
후자인 DefaultsOnly / InstanceOnly / Anywhere의 세가지로 나눌 수 있습니다. 

전자인 Edit와 Visible은 편집이 가능한지, 보여주기만 할 것인지를 지정하는데 사용합니다.
위에서 액터의 컴포넌트는 설계 단위에서부터 목적이 명확하기 때문에 향후에 바꿀일이 있으면 안된다고 설명드렸습니다. 따라서 우리의 스켈레탈 메시 컴포넌트는 Visible을 사용해야합니다. (보여주기만)

오브젝트에 Visible 키워드를 사용하면 오브젝트 레퍼런스는 변경이 되지 않지만,
오브젝트 내 속성들은 블루프린트에서 편집이 가능합니다.

그러면 딱 우리가 원하는 결과가 나옵니다.

반면, 밸류타입인 BaseDamage를 Visible로 지정하면 이는 읽기만 가능해집니다. 
따라서 밸류타입은 대부분 Edit를 사용하는 것이 일반적입니다. 


 

후자인 DefaultsOnly / InstanceOnly / Anywhere은 변수의 활용 용도에 따라 다릅니다.

DefaultsOnly는 클래스 설계도에서만 변수 편집 화면을 보여주도록 지정하는 키워드입니다.

InstanceOnly는 언리얼 오브젝트의 인스턴스에서만 변수 편집 화면을 보여주게 만드는 키워드이지요.

Anywhere은 말 그대로 둘 다 보여주게 하는 키워드입니다.

예를 들어서 하나의 몬스터를 기획하는데, 공격 범위와 몬스터의 레벨을 저장할 변수를 두 개를 선언했습니다.
이 몬스터는 에디터에서 블루프린트를 맵에 드래그하여 배치하는데, 플레이어의 시작지점과 가까운 위치에 있는 몬스터는 낮은 레벨로, 플레이어와 멀어질 수록 배치된 몬스터는 점점 높은 레벨로 설정할 수 있도록 몬스터를 설계한다고 가정합시다. 

맵에 배치된 모든 몬스터의 공격 범위를 동일하게 만들려면, 공격 범위에 해당하는 변수는 DefaultsOnly키워드를 사용해 모든 배치된 몬스터 액터들이 같은 값을 가지도록 설계하는 것이 관리적인 측면에서 유리할 겁니다.

하지만 몬스터 레벨의 정보는 플레이어 시작 위치로부터의 거리에 따라 배치된 몬스터 액터 인스턴스들마다 점점 높아지는 값을 가져야 합니다.

이 경우 레벨 변수에 InstanceOnly 키워드를 사용하는 것이 관리적인 측면에서 편리하겠지요.

이 값들의 용도를 정리하면 다음과 같습니다.

 

Edit 

Visible 

 DefaultsOnly

인스턴스가 공통으로 가져야 할 값의 편집 

설계화면에서만 보여질 값 

 InstanceOnly

 인스턴스 별로 다르게 있어야 할 값의 편집

인스턴스에서만 보여질 값 

 Anywhere

범용적으로 변경되는 값의 편집

객체 레퍼런스 및 모든 화면에서 보여질 값

 


이를 우리의 Weapon 클래스 멤버 변수에 적용해봅시다. 

 

객체 레퍼런스에 해당하는 스켈레탈메시 컴포넌트는 VisibleAnywhere 키워드를, 모든 인스턴스가 공통으로 가져야 할 BaseDamage는 기획의도에 따라 설계시 데미지 값으로 통일하고 싶으면 EditDefaultsOnly를 사용하고, 월드 상의 액터 인스턴스마다 다른 데미지 값을 가지고 싶으면 EditInstanceOnly를, 모든 곳에서 데미지 값을 변경하게 만들고 싶으면 EditAnywhere 키워드를 지정하면 됩니다. 

드문 경우이기는 한데, 에디터에서 편집은 안되고 읽기 전용으로 보여지기만 원한다면 VisibleAnywhere을 사용해도 무방하고, 노출 범위에 따라 VisibleDefaultsOnly 혹은 VisibleInstanceOnly를 사용하면 됩니다. 

 

아래는 용도에 맞게 UPROPERTY 매크로에 키워드를 추가한 코드 입니다. 

[인스턴스만 생성된 인스턴스에만 보이고, 디폴트로 해야 블루프린트에서도 보인다]

{
    UPROPERTY(BlueprintReadOnly, VisiableAnyWhere)
	class USkeletalMeshComponent* Weapon;

	UPROPERTY(BlueprintReadWrite, EditInstanceOnly) //EditDefaultsOnly
	float BaseDamage;
 }   

 



그러면 이제 스켈레탈메시 컴포넌트에 대해서 블루프린트 에디터에서 편집이 가능해지고,
액터 기본 값 목록에서 BaseDamage 값을 편집할 수 있게 되었습니다.

 

 블루프린트 에디터에 적용된 결과

 


 Category

현재 변수들은 기본적으로 카테고리가 상위 클래스 이름인 Weapon으로 지정되어 있습니다.


에디터에서 보여지는 카테고리 이름을 변경하고 싶으면 Category 키워드를 추가해주면 됩니다.
아래는 에디터에서 보여줄 카테고리 정보까지 지정한 최종 코드입니다.

{ 
 UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "Stat")

    float BaseDamage;
}

적용화면

 


meta = AllowPrivateAccess

이 외에도 UPROPERTY에는 meta 네임스페이스에서 지정된 여러가지 키워드를 사용해 에디터에서
도움말과 같은 것들을 출력할 수 있거나 에디터 연동 관련해서 기타 다양한 값들을 지정하는 것이 가능한데,

meta 키워드 중에서 눈여겨볼 부분은 AllowPrivateAccess라는 값입니다.
블루프린트는 C++ 클래스를 상속받는 개념이므로 기본적으로 public이나 protected 변수들만 적용이 가능합니다.

하지만 AllowPrivateAccess 키워드를 추가하면 private 변수도 블루프린트 스크립트에서 활용이 가능합니다.

이는 OOP 설계시 멤버 변수에 직접 접근하는 디자인보다 액세서(Accessor)라 불리는 함수를 통하는 디자인 방식을 선호하는 분을 위해 제공하는 기능입니다.

아래와 같이 UPROPERTY 매크로에 해당 키워드를 추가하면 OOP 코딩 설계를 따르면서도,
블루프린트로도 노출하는 변수 제작이 가능합니다. 

 

{
public:
    float GetMyHP() { return HP; }  //액세서

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


여기서 AllowPrivateAccess 부분이 없으면, 빌드가 오류난다. 
private 인데 BlueprintReadWrite이니 허용이 안되는 거 같다


3. 멤버 함수의 노출 

 

언리얼 오브젝트에서 클래스 멤버 함수도 마찬가지로 기본적으로 블루프린트에서 사용할 수 없지만,
BlueprintCallable이라는 키워드를 사용하면 블루프린트에서 호출이 가능합니다.

함수를 블루프린트에서 사용하고자 위의 키워드를 사용할 때에는 반드시 카테고리를 지정해서 블루프린트에서 검색할 수 있게 만들어주어야 합니다.

따라서 아래 코드는 Weapon 카테고리와 하위의 Stat 카테고리를 선언한 구문입니다. 

{
public:

    UFUNCTION(BlueprintCallable, Category = "Weapon|Stat")
	float GetDamage() { return BaseDamage; }


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

 

private 된 값을 얻어올 수 있게 함수가능

 

이 외에도 멤버 함수에는 블루프린트에서의 이벤트로 지정할 수 있는 BlueprintImplementableEvent 키워드와

C++와 블루프린트 두 군데에서 모두 이벤트를 처리할 수 있는 BlueprintNativeEvent 등의 키워드들이 있습니다.

전자는 이벤트 구현의 의무를 완젼히 블루프린트에 부여하고 싶을때에 사용하고,
후자는 C++에서 이벤트 로직을 구현하되, 블루프린트에서도 대신 구현할 수 있게 만들 때 유용합니다. 

 

 

+ Recent posts