이벤트지정 키워드?

 

BlueprintImplementableEvent


-블루프린트에서의 이벤트로 지정할 수 있는 키워드
-이벤트 구현의 의무를 완젼히 블루프린트에 부여하고 싶을때에 사용

BlueprintNativeEvent 

- C++와 블루프린트 두 군데에서 모두 이벤트를 처리할 수 있는 등의 키워드
- C++에서 이벤트 로직을 구현하되, 블루프린트에서도 대신 구현할 수 있게 만들 때 유용합니다. 

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

레퍼런싱 - 작성 중  (0) 2019.05.29
언리얼 INI 파일 설정 기능  (0) 2019.05.29
ConstructorHelpers + 애셋 로딩 시점관련  (0) 2019.05.28
액터 검색 및 순회 방법  (0) 2019.05.27
TFieldIterator, NativeFunctionLookupTable  (0) 2019.05.27

액터

정해진 기능에 따라 자신의 역할을 수행하는 기본 객체

 

이번 강좌에서는 드디어 언리얼 엔진의 게임 컨텐츠 핵심이라 할 수 있는 액터(Actor)를 제작해보겠습니다.

언리얼 엔진 메뉴얼에서는 액터를 "레벨에 배치하거나 스폰시킬 수 있는 기본 객체"라고 설명합니다.

저 개인적으로는 이를 좀 더 보강해서 월드에 존재하면서,
정해진 기능에 따라 자신의 역할을 수행하는 기본 객체라고 액터를 정의하고 싶습니다. 




트랜스폼

 

월드에서 존재한다는 이야기는 3차원 가상 공간에서 항상 자신의 위치, 방향과 크기가 지정되어 있다는 뜻입니다.
일반적으로 많은 3D 소프트웨어에서는 이를 구현하기위해 
위치,회전,스케일 정보를 묶은 트랜스폼(Transform)이라는 정보를 사용합니다. 

우리가 사용하는 언리얼 엔진도 모든 액터에 트랜스폼 정보가 들어있습니다.
월드 아웃라이너에서 어떤 액터를 선택하더라도
에디터의 디테일 뷰에 항상 트랜스폼 정보가 가장 먼저 지정되어 있음을 확인할 수 있습니다.

 

액터와 트랜스폼 정보


액터의 제작


단순하게 이야기하자면 액터월드 트랜스폼이 있는 언리얼 오브젝트라고 할 수 있겠습니다. 

C++로 액터를 만들어봅시다.
Actor를 상속받은 클래스를 하나 제작하고 이름을 Weapon이라고 만들겠습니다.
지금까지 하던 방식대로 
[파일 > 새로운클래스 생성] 메뉴를 선택하고 부모클래스를 액터(Actor)로 지정합시다. 

자동 생성된 코드를 살펴보면, 아래와 같이 클래스 이름이
UWeapon이 아닌
AWeapon이 되는 것을 확인할 수 있습니다. 

언리얼 오브젝트의 접두사는 U, A 그리고 S가 있습니다. 
- 액터 기반이라면 A를 
- 액터 기반이 아니라면 모두 U 
- UI를 담당하는 슬레이트만 S를 사용하면 됩니다.

 

Weapon class  U가 아닌 A로 생성됨

 

언리얼 엔진에서 액터를 상속받는 클래스는 모두 A라는 접두사를 사용하도록 설계되어 있습니다. 

게임을 프로그래밍 프레임웍의 관점에서 확장해 컨텐츠 제작의 관점으로 본다면,
게임 컨텐츠는 월드에 존재하는 물체간의 상호작용이라고 할 수 있습니다. 

액터는 월드에 존재하는 물체의 기본 단위이기 때문에, 게임 컨텐츠의 설계는 액터에서부터 시작한다고 할 수 있습니다. 그래서 언리얼 엔진이 액터 클래스의 정의에 U접두사를 사용하지 않고, A접두사를 사용하는 이유는,
컨텐츠 제작에 기여하는 오브젝트들을 묶어서 
관리하기가 용이하기 때문입니다.

액터가 가지는 또 다른 특징은 제작자가 액터에 부여한 기능입니다.
언리얼 엔진은 액터에 기능을 부여할 수 있도록, 엔진의 기능을 잘 모듈화하여 컴포넌트란 이름으로 포장을 해서 제공을 합니다.
제작자는 액터를 설계할 때 언리얼 엔진이 제공하는 컴포넌트를 골라 부착해, 액터의 기능을 지정할 수 있습니다. ( 물론 제작자가 컴포넌트를 직접 제작하는 것도 가능합니다. )


블루프린트 액터 제작

 

우리가 제작할 무기 액터는 우선 무기의 시각적 요소를 보여줘야하는 기능이 필요합니다.
이를 위해서 언리얼 엔진이 제공하는 스켈레탈메시 컴포넌트를 사용하면 됩니다. 
이해를 돕기 위해 먼저 블루프린트로 무기를 제작하고, 
동일하게 C++로 만들겠습니다. 

무기를 시각적으로 표현하기 위해 무료 애셋을 다운로드 받읍시다.
언리얼 마켓플레이스에서 Infinity라고 검색한 후 아래의 Infinity Blade Weapons 애셋을 현재 프로젝트로 추가로 임포트 시키겠습니다.  콘텐츠 임포트가 완료되면 여러분 프로젝트의 콘텐츠 브라우저에 InfinityBladeWeapons라는 폴더가 생성됩니다. 

마켓 플레이스의 Infinity Blade Weapons 애셋 



블루프린트 액터의 제작



이제 생성한 BP_Weapon 블루프린트를 더블클릭합시다.
좌측 상단에 있는 컴포넌트 추가 메뉴를 누른 후, 스켈레탈 메시(Skeletal Mesh) 컴포넌트를 선택해 추가하고,
컴포넌트의 이름을 Weapon으로 변경합시다. 그리고 생성된 스켈레탈 메시 컴포넌트를 최상단에 있는 DefaultSceneRoot 컴포넌트에 드래그하여 루트 컴포넌트(Root Component)로 설정합시다. 

 

블루프린트 액터의 설정

 

이제 스켈레탈 메시 컴포넌트를 선택하고 오른쪽의 디테일 윈도우의 Mesh 섹션에서 Skeletal Mesh 애셋을 SK_Blade_Blackknight로 지정합시다.
( 사실 다른 무기 애셋을 선택해도 강좌 진행에는 지장이 없습니다. ) 완료되면 컴파일을 눌러 완료합시다. 

최종 블루프린트 무기 액터의 설정은 아래와 같습니다. 

 

스켈레탈 메시 컴포넌트의 세팅 

 


 

블루프린트 작업을 정리하면, 우리는 무기 역할을 수행할 액터에서 무기를 비주얼적으로 보여주기 위해,
스켈레탈 메시 컴포넌트를 사용하였습니다.

( 애니메이션이 없는 칼임에도 스태틱 메시 컴포넌트를 사용하지 않고 스켈레탈 메시 컴포넌트를 사용한 이유는 활과 같은 다른 무기에 애니메이션이 있을 수 있기 때문입니다. )

그리고 스켈레탈 메시 컴포넌트를 액터의 움직임이나 충돌 기능을 대표하도록 루트 컴포넌트로 설정했습니다. 

완성된 BP_Weapon 블루프린트를 레벨에 드래그하면 액터 인스턴스가 현재 레벨에 생성됩니다.
배치된 액터는 플레이 버튼을 누르지 않아도 이미 액터에 컴포넌트 인스턴스가 생성되어 있음을 확인할 수 있습니다.

이처럼 액터를 제작할 때에는, 생성 시점에 관련 컴포넌트들도 함께 생성되는 방식으로 동작합니다.
블루프린트도 앞서서 설명한 C++의 CDO메카니즘과 유사하게 동작한다고 이해하시면 되겠습니다.

이렇게 액터를 정의한 블루프린트의 설계도를 다른 말로는 아키타입(Archetype)이라고 합니다. 

 

블루프린트 액터의 배치

 


C++ 액터 제작



(위에 만든 AWepon을 계속 이용하여 제작함)


블루프린트 제작이 완료되었으니, BP_Weapon 블루프린트와 동일하게 C++ 클래스인 AWeapon를 만들어봅시다.

먼저 C++ 코드에서 스켈레탈메시 컴포넌트를 선언해봅시다.
스켈레탈 메시 컴포넌트를 추가하려면 이 컴포넌트의 클래스 이름을 알아야 할텐데요,
아래와 같이 블루프린트에서 컴포넌트를 우클릭해 헤더를 열면 쉽게 파악할 수 있습니다. 

컴포넌트가 선언된 헤더파일을 열기

 

컴포넌트는 액터와 달리 월드에서 자립할 수 없기 때문에 U접두사를 사용하며,
소스 파일을 연 후, 비주얼 스튜디오에서 U로 시작하는 클래스를 찾으면 쉽게 확인할 수 있습니다. 

스켈레탈 메시 컴포넌트의 검색



이제 프로젝트에서 AWeapon 클래스에 아래와 같이 변수를 하나 추가합시다

 

//AWeapon.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Weapon.generated.h"

UCLASS()
class ABC_API AWeapon : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AWeapon();

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

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

//추가됨
	UPROPERTY()
	class USkeletalMeshComponent* Weapon;

};

 

헤더 선언이 완료되면 이제 구현부에 가서 생성자 코드에

CreateDefaultSubobject API를 사용해 스켈레탈 메시 컴포넌트를 생성해줍시다.

NewObject가 아닌 CreateDefaultSubobject를 사용하는 이유에 대해서는 이전 강에서 설명을 드렸습니다.

모든 액터는 컴포넌트를 항상 기본 탑재하게 되므로 
게임플레이 런타임에서 컴포넌트를 생성하기보다는   (동적생성)
CDO 생성시에 포함시키는 방식이 유리하다고 설명드렸습니다.  (정적생성)

스켈레탈 메시 컴포넌트가 생성되면 이를 바로 루트컴포넌트로 지정합시다.

 

아래는 이를 구현한 코드입니다.

//Weapon.cpp

#include "Weapon.h"  //위에 있어야 오류가 발생 안한다
#include "ABC.h"


AWeapon::AWeapon()
{
    PrimaryActorTick.bCanEverTick = true;

    Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WeaponHashValue"));
    RootComponent = Weapon;
}

void AWeapon::BeginPlay()
{
    Super::BeginPlay();
}

void AWeapon::Tick( float DeltaTime )
{
    Super::Tick( DeltaTime );
}

 

 

이제 스켈레탈 메시 컴포넌트에 애셋을 지정해줍시다.  
언리얼 프로젝트에서는 모든 애셋은 경로 기반으로 관리됩니다.

그래서 C++ 코드에서 애셋을 로딩하려면 먼저 사용할 애셋의 경로를 알아야합니다.
애셋의 경로는 에디터의 콘텐츠 브라우저에서 애셋을 우클릭하고 레퍼런스 복사를 누르면,
애셋의 경로가 클립보드로 복사됩니다. 

콘텐츠 브라우저에서 아래와 같이 "black knight"라고 검색한 후
SK_Blade_BlackKnight 애셋을 우 클릭하고 경로를 클립보드로 복사합시다

SkeletalMesh'/Game/InfinityBladeWeapons/Weapons/Blade/Swords/Blade_BlackKnight/SK_Blade_BlackKnight.SK_Blade_BlackKnight'

 애셋 경로의 복사

 


CDO를 제작하는 생성자 코드에서 애셋에 관련된 정보를 불러올 때에는

ConstructorHelpers라는 특수한 클래스를 사용합니다.


이 클래스는 애셋 내 가져올 종류에 따라 ObjectFinder와 ClassFInder라는 두 API를 제공하는데,

1. ClassFinder는   애셋의 형(Type) 정보를 가져올 때
2. ObjectFinder애셋의 내용물을 가져올 때
( 이는 하나의 언리얼 오브젝트가 UClass와 CDO로 나누어진다는 것을 다시 되새기면 이해가 가실 겁니다. ) 

ConstructorHelpers


우리는 애셋의 내용물을 가져와야 하므로 
ObjectFinder를 사용하겠습니다. 

우리가 콘텐츠 브라우저에서 선택한 스켈레탈 메시 애셋은 USkeletalMesh라는 언리얼 오브젝트가 통채로 저장된 형태입니다. ( 언리얼 오브젝트의 저장(Serialization)에 대해서는 이후 강좌에서 다루겠습니다. )

를 끄집어내 컴포넌트의 SetSkeletalMesh 함수를 사용해 애셋을 지정해주어야 합니다.
아래는 이를 구현한 코드입니다.
(애셋의 주소는 직접 입력하지 말고 레퍼런스 복사 메뉴를 사용해 복사한 주소를 붙여넣기하는 것이 좋습니다. )



AWeapon::AWeapon()
{
	PrimaryActorTick.bCanEverTick = true;

	Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WeaponHashValue"));

	RootComponent = Weapon;

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

	if (SK_BlackKnight.Succeeded())
	{
		Weapon->SetSkeletalMesh(SK_BlackKnight.Object);
	}
}

 

위의 구문은 단 두 줄이지만, 여러가지 의미를 담고 있습니다.  

먼저 ConstructorHelpers 클래스는 말 그대로 생성자(Constructor)에서만 사용이 가능합니다.

즉 CDO 제작에만 사용된다는 이야기지요.
만일 게임플레이 런타임에서 애셋을 로딩하기 위해서는 ConstructorHelper가 아닌 
StaticLoadObject와 같은 다른 API를 사용하여야 합니다.

이렇게 애셋을 로딩 시점을 1. 엔진 초기화 런타임과 2. 게임플레이 런타임으로 구분하는 이유는, 안전을 위해서입니다. 

일반적으로 컨텐츠에서 사용하는 애셋들은 엔진 초기화 시점에서 우리가 사용할 애셋이 확실히 존재하는지 검증하고 다음 단계를 진행하는 것이 안전합니다. 

만일 런타임에서 애셋을 로딩하게 된다면 예기치 않은 문제들이 발생할 수 있습니다.
예를 들어 우리가 제작한 무기를 게임 플레이 런타임에서 로딩한다면, 캐릭터가 등장하고 열심히 탐험하고 몬스터 공격을 위해 칼을 빼들 때야 비로서 로딩을 시작하겠지요. 그런데 이 때 칼 애셋이 없는 경우에는 잘못해서 콘텐츠가 크래시가 일어날 수 있습니다. 

불러들이는 애셋의 경로는 특별한 이유가 없다면 로딩된 이후에 변경될 일이 없을테니, 사용할 애셋들은 ConstructorHelpers 클래스를 사용해 애셋의 유무를 미리 확인하고 로딩하는 것이 좋습니다. 
(게임플레이런타임에서 애셋을 로딩하는 기능은 이후 강좌에서 다룰 예정입니다.)

ConstructorHelpers 클래스를 사용해 제작한 변수에는 static 키워드를 앞에 붙였습니다.

이 구문은 스태틱 키워드를 사용하지 않아도 동작에는 무방하지만,
이렇게 스태틱 변수로 선언한 이유는 리소스는 공유해서 사용하는 자원이므로

모든 언리얼 오브젝트 인스턴스마다 애셋 정보를 로딩할 필요가 없기 때문입니다.

그래서 ConstructorHelpers 로 변수를 선언할 때에는
관련 인스턴스들이 
유해서 쓰도록 로컬 스태틱 변수로 선언하는 것이 일반적입니다. 

 

아래는 이렇게 완성된 코드를 컴파일하고 생성된 C++ Weapon 클래스를 레벨에 끌어다 배치한 결과입니다. 

결과는 우측에 위치한 블루프린트 인스턴스와 동일함을 확인할 수 있습니다.

 

좌(블루프린트) 우(C++코드)

 

 

CDO를 제작하는 생성자 코드에서 애셋에 관련된 정보를 불러올 때에는

ConstructorHelpers라는 특수한 클래스를 사용합니다.


이 클래스는 애셋 내 가져올 종류에 따라 ObjectFinder와 ClassFInder라는 두 API를 제공하는데,

1. ClassFinder는   애셋의 형(Type) 정보를 가져올 때

2. ObjectFinder 애셋의 내용물을 가져올 때

( 이는 하나의 언리얼 오브젝트가 UClass와 CDO로 나누어진다는 것을 다시 되새기면 이해가 가실 겁니다. ) 

 

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

 

먼저 ConstructorHelpers 클래스는 말 그대로 생성자(Constructor)에서만 사용이 가능합니다.

즉 CDO 제작에만 사용된다는 이야기지요. 
만일 게임플레이 런타임에서 애셋을 로딩하기 위해서는 ConstructorHelper가 아닌 
StaticLoadObject와 같은 다른 API를 사용하여야 합니다.

이렇게 애셋을 로딩 시점을 1. 엔진 초기화 런타임과 2. 게임플레이 런타임으로 구분하는 이유는, 안전을 위해서입니다. 

일반적으로 컨텐츠에서 사용하는 애셋들은 엔진 초기화 시점에서 우리가 사용할 애셋이 확실히 존재하는지 검증하고 다음 단계를 진행하는 것이 안전합니다. 

만일 런타임에서 애셋을 로딩하게 된다면 예기치 않은 문제들이 발생할 수 있습니다. 
예를 들어 우리가 제작한 무기를 게임 플레이 런타임에서 로딩한다면, 캐릭터가 등장하고 열심히 탐험하고 몬스터 공격을 위해 칼을 빼들 때야 비로서 로딩을 시작하겠지요. 그런데 이 때 칼 애셋이 없는 경우에는 잘못해서 콘텐츠가 크래시가 일어날 수 있습니다. 

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

 

이번 강좌에서는 언리얼 오브젝트의 계층 구조를 다뤄보겠습니다.

먼저 게임인스턴스 헤더에 WebConnectionNew라는 변수를 같은 타입으로 동일하게 선언해줍시다.

 

////GameInstance.h 
#pragma once

#include "Engine/GameInstance.h"
#include "WebConnection.h"
#include "ABGameInstance.generated.h"

UCLASS()
class ARENABATTLE_API UABGameInstance : public UGameInstance
{
    GENERATED_BODY()

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

    UPROPERTY()
    class UWebConnection* WebConnection;
 
    UPROPERTY()
    class UWebConnection* WebConnectionNew;
};


////GameInstance.cpp
#include "ABCGameInstance.h"
#include "ABC.h"


UABCGameInstance::UABCGameInstance()
{
	AB_LOG(Warning, TEXT("Constructor Call Start"));
	WebConnection = CreateDefaultSubobject<UWebConnection>(TEXT("MyWebConnection"));
	AB_LOG(Warning, TEXT("Constructor Call End"));
}

void UABCGameInstance::Init()
{
	Super::Init();
	AB_LOG_CALLONLY(Warning);
}

 

지난 번 CDO 강좌에서 WebConnection이라는 기본서브오브젝트(DefaultSubobject)를 생성해봤는데요,
CDO에서 기본서브오브젝트를 생성하면,
새로운 언리얼 오브젝트CDO 인스턴스 간에는 아래와 같은 관계를 가지게 됩니다. 

 

기본서브오브젝트와 외부참조(Outer) 

 

아래는 이를 출력한 코드입니다.

/////ABGameInstance.cpp

void UABGameInstance::Init()

{
    Super::Init();
    AB_LOG_CALLONLY(Warning);


    TArray<UObject*> DefaultSubobjects;         //UObject 배열을 만들고,
    
    GetDefaultSubobjects(DefaultSubobjects);    //GetDefault 해서 언어온다.


    for (const auto& Entry : DefaultSubobjects)  //auto문을 이용해서 DefaultSubobjects 끝까지 순회한다
    {
        AB_LOG(Warning, TEXT("DefaultSubobject : %s"), *Entry->GetClass()->GetName());
        AB_LOG(Warning, TEXT("Outer of DefaultSubobject : %s"), *Entry->GetOuter()->GetClass()->GetName());    
    }
}

 

실행결과

 


 

이번에는 기본서브오브젝트와 동일한 타입의 WebConnection 언리얼 오브젝트를
게임 플레이 런타임인 Init 함수에서 생성하겠습니다. 

게임플레이 런타임에서 언리얼 오브젝트를 생성하기 위해서는 NewObject 함수를 사용하면 됩니다.
이 때 첫 번째 인자 정보에 따라 새롭게 생성된 언리얼 오브젝트의 Outer가 결정됩니다.

이번 예제에서는 게임 인스턴스와의 연결을 위해 this를 넣어서 생성하겠습니다.
그러면 새롭게 생성되는 WebConnectionNew의 Outer는 게임 인스턴스가 됩니다. 

 

////ABGameInstance.cpp

void UABGameInstance::Init()
{
    Super::Init();
    AB_LOG_CALLONLY(Warning);

    TArray<UObject *> DefaultSubobjects;
    GetDefaultSubobjects(DefaultSubobjects);
    for (const auto& Entry : DefaultSubobjects)
    {
        AB_LOG(Warning, TEXT("DefaultSubobject : %s"), *Entry->GetClass()->GetName());
        AB_LOG(Warning, TEXT("Outer of DefaultSubobject : %s"), *Entry->GetOuter()->GetClass()->GetName());    
    }

////추가
    WebConnectionNew = NewObject<UWebConnection>(this);
    AB_LOG(Warning, TEXT("Outer of NewObject : %s"), *WebConnectionNew->GetOuter()->GetClass()->GetName());
}

 

실행화면

 

플레이를 누르면 런타임에서 최종 구조는 아래와 같이 되겠지요. 

언리얼 오브젝트간의 연결 구성 

 

그러면 기존의 WebConnection과 새로운 WebConnectionNew와의 차이는 무엇일까요? 

사실 사용에 있어서 두 객체간의 차이는 없다고 보면 됩니다. 

다만 생성방식에서만 차이가 있는데,

전자인 WebConnection은 ABGameInstance의 인스턴스가 생성되면 자동으로 따라 생성되는 반면,
후자인 WebConnectionNew는 게임 플레이 런타임에서 우리가 직접 생성해줘야 합니다.

전자(WebConnection)의 방식을 스태틱(Static)하게 생성한다고 표현하겠습니다. 
이는 기획상으로 변경될 여지가 거의 없는 오브젝트 묶음을 한번에 빠르게 생성할 때 유용합니다. 

후자(WebConnectionNew)의 방식을 다이나믹(Dynamic)하게 생성한다고 표현하겠습니다.
후자 방식은 느리지만 세팅에 따라 다양한 오브젝트를 생성할 수 있어서,
유연하게 상황에 대처할 수 있다는 장점이 있습니다. 


Level

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

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

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

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

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

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

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

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

 

월드의 계층 구조

 

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

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

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

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

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

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

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

 

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


FRangedWorld

이제 월드에 있는 언리얼 오브젝트들을 살펴봅시다. 

월드에 속한 액터를 빠르게 검색하는 방법은 두 가지입니다.

하나는 For Ranged Loop 구조에 맞게 추가된 FRangedWorld를 사용하는 방법이 있습니다.

이 방식은 모든 액터를 간편하게 검색할 수 있다는 장점이 있습니다.

액터 내 컴포넌트는 모두 기본서브오브젝트로 등록되어 있으니 쉽게 검색이 가능합니다.
( "Runtime/Engine/Public/EngineUtils.h" 추가해야함)


////GameInstance.cpp

#include "Runtime/Engine/Public/EngineUtils.h"

void UABCGameInstance::Init()
{
....

	UWorld* CurrentWorld = GetWorld();

	for (const auto& Entry:FActorRange(CurrentWorld))
	{
		AB_LOG(Warning, TEXT("Actor : %s"), *Entry->GetName());
		TArray<UObject*> Components;
		Entry->GetDefaultSubobjects(Components);

		for (const auto& CEntry : Components) //순회
		{
			AB_LOG(Warning, TEXT(" -- Component : %s"), *CEntry->GetName());
		}
	}
}


출력하면 아래와 같은 결과를 볼 수 있습니다.
여기서 액터 목록이 월드 아웃라이너와 다른데,
이는 게임인스턴스의 Init이 호출되는 시점에 아직 게임 플레이가 완성되지 않아서 그렇습니다.

기본 템플릿 월드 내 액터와 컴포넌트의 출력 결과

검색만 간단한 방법?

 


TActorIterator

 

다른 하나는 TActorIterator를 사용하는 방식입니다. 


이 방식은 액터 중에서도 우리가 원하는 타입만 선별해서 목록을 빠르게 뽑아낼 수 있어서 
대부분 많이 사용하는 방식입니다. 

아래는 현재 월드에서 스태틱메시액터 타입만 뽑아낸 예시입니다.

UWorld* CurrentWorld = GetWorld();

for (TActorIterator<AStaticMeshActor> It(CurrentWorld); It; ++It)
{
    AB_LOG(Warning, TEXT("StaticMesh Actor : %s"), *It->GetName());
}





TObjectIterator

 

액터를 포함해 현재 월드에 로딩된 모든 언리얼 오브젝트를 가져오기 위해서는 TObjectIterator를 사용하면 됩니다.

아래 코드를 우리가 생성한 WebConnection가 세 개 로딩되어 있는 것을 확인할 수 있습니다.

하나는 ABGameInstance의 CDO에 있는 객체,
다른 하나는 실제 인스턴스의 기본서브오브젝트로 있는 객체,  
나머지는 런타임에서 생성한 객체입니다.

(#include "Runtime/CoreUObject/Public/UObject/UObjectIterator.h" 추가해야 사용가능)

WebConnection->Host = TEXT("localhost");
WebConnectionNew->Host = TEXT("127.0.0.1");

for (TObjectIterator<UWebConnection> It; It; ++It)
{
    UWebConnection* Conn = *It;
    AB_LOG(Warning, TEXT("WebConnection Object Host : %s"), *Conn->Host);
}

 

실행 화면

이정도만 알아두면 이제 월드 내에서 원하는 언리얼 오브젝트를 쉽게 검색할 수 있을 겁니다~

액터에서 컴포넌트의 제작은 CreateDefaultSubobject를 사용해야 한다로 요약하면 되겠습니다. 



 

[참고] [1-7] 언리얼 오브젝트의 계층 구조

 

월드에 속한 액터를 빠르게 검색하는 방법은 두 가지입니다.

하나는 For Ranged Loop 구조에 맞게 추가된 FRangedWorld를 사용하는 방법이 있습니다.


#include "Runtime/Engine/Public/EngineUtils.h"

void UABCGameInstance::Init()
{
....

	UWorld* CurrentWorld = GetWorld();

	for (const auto& Entry:FActorRange(CurrentWorld))
	{
		AB_LOG(Warning, TEXT("Actor : %s"), *Entry->GetName());
		TArray<UObject*> Components;
		Entry->GetDefaultSubobjects(Components);

		for (const auto& CEntry : Components) //순회
		{
			AB_LOG(Warning, TEXT(" -- Component : %s"), *CEntry->GetName());
		}
	}
}

 


다른 하나는 TActorIterator를 사용하는 방식입니다.


이 방식은 액터 중에서도 우리가 원하는 타입만 선별해서 목록을 빠르게 뽑아낼 수 있어서
대부분 많이 사용하는 방식입니다.
아래는 현재 월드에서 스태틱메시액터 타입만 뽑아낸 예시입니다.

 for (TActorIterator<AStaticMeshActor> It(CurrentWorld); It; ++It)
{
    AB_LOG(Warning, TEXT("StaticMesh Actor : %s"), *It->GetName());
}

 


액터를 포함해 현재 월드에 로딩된 모든 언리얼 오브젝트를 가져오기 위해서는 TObjectIterator를 사용하면 됩니다.

아래 코드를 우리가 생성한 WebConnection가 세 개 로딩되어 있는 것을 확인할 수 있습니다.
하나는 ABGameInstance의 CDO에 있는 객체, 다른 하나는 실제 인스턴스의 기본서브오브젝트로 있는 객체,
나머지는 런타임에서 생성한 객체입니다.

(#include "Runtime/CoreUObject/Public/UObject/UObjectIterator.h"추가해야함)

 

WebConnection->Host = TEXT("localhost");
WebConnectionNew->Host = TEXT("127.0.0.1");

for (TObjectIterator<UWebConnection> It; It; ++It)
{
    UWebConnection* Conn = *It;
    AB_LOG(Warning, TEXT("WebConnection Object Host : %s"), *Conn->Host);

}

 

 



1. ObjectIterator/ObjectRange

 

현재 월드에 로딩된 언리얼 오브젝트(UObject)를 순회하기 위해 사용한다.

모든 언리얼 오브젝트를 순회하는 FObjectIterator와 특정 타입의 언리얼 오브젝트를 순회하는 TObjectIterator/TObjectRange로 나눌 수 있다.

Iterator와 Range 클래스는 "Runtime/CoreUObject/Public/UObject/UObjectIterator.h" 파일에 정의되어 있다.

 

1) FObjectIterator

 

언리얼 오브젝트(UObject) 타입을 특정짓지 않고, 순회할 때 사용하는데, 이 녀석은 Range 클래스가 별도로 존재하지 않는 점은 참고/유의해야 한다.

아래 예제는 현재 월드에 로딩된 모든 언리얼 오브젝트(UObject)를 순회한다.

// World에 로딩된 모든 UObject 순회
for (FObjectIterator entity; entity; ++entity)
{
    UE_LOG(LogABGameInstance, Warning, TEXT("Loaded UObject : %s"), *entity->GetName());
}

 

2) TObjectIterator/TObjectRange

 

FObjectIterator/FObjectRange와 달리 TObjectIterator/TObjectRange는 특정 타입의 언리얼 오브젝트를 순회할 수 있기에, 훨씬 더 실용성이 높다.

아래 예제는 현재 로딩된 UStaticMesh만 순회한다.

 

// 월드에 로딩된 UStaticMesh 오브젝트 순회
// 아래 2문장은 완전히 동일하다

for (TObjectIterator<UStaticMesh> entity; entity; ++entity)

for (const auto& entity : TObjectRange<UStaticMesh>())
{
    UE_LOG(LogABGameInstance, Warning, TEXT("Loaded UStaticMesh : %s"), *entity->GetName());
}

 

 

2. ActorIterator/ActorRange

 

Actor는 레벨에 배치된 UObject이므로 순회에 현재 UWorld 객체를 요구한다.

월드 내 레벨에 배치된 모든 Actor를 순회하는 FActorIterator/FActorRange와 특정 타입의 액터를 순회하는 TActorIterator/TActorRage로 나눌 수 있다.

Iterator와 Range 클래스는 "Runtime/Engine/Public/EngineUtils.h" 파일에 정의되어 있다.

 

1) FActorIterator/FActorRange 예제

ActorType을 특정하기 않고, 모든 Actor를 순회할 때 사용한다.

아래 예제는 월드 내 모든 Actor에 대해 각 Actor들이 어떤 DefaultSubobject들을 가지고 있는지 출력한다.

UWorld* world = GetWorld();

// World의 모든 Actor 순회
// 아래 2문장은 완전히 동일하다
for (FActorInterator(world) entity; entity; ++entity)
for (const auto& entity : FActorRange(world))
{
    UE_LOG(LogABGameInstance, Warning, TEXT("Actor : %s"), *(entity->GetName()));

    TArray<UObject*> components;

    // UObject::GetDefaultSubobjects (Runtime/CoreUObject/Private/UObject/Obj.cpp)
    // 해당 UObject의 defaultSubobject들을 out parameter인 TArray에 채워줌

    entity->GetDefaultSubobjects(components);
    for (const auto& entity : components)
    {
        UE_LOG(LogABGameInstance, Warning, TEXT("   -- Component : %s"), *(entity->GetClass()->GetName()));
    }

}



2) TActorIterator / TActorRange

 

FActorIterator/FActorRange와 달리 TActorIterator/TActorRange는 ActorType을 지정하여 순회할 수 있어, 
개발 과정에서 더 자주 쓰이는 녀석이라 할 수 있다.

아래 예제는 레벨 내 Actor중 StaticMeshActor만 순회한다.

 

// World의 Actor중 StaticMeshActor 한정 순회
// 아래 2문장은 완전히 동일하다

for (TActorIterator<AStaticMeshActor> entity(world); entity; ++entity)
for (const auto& entity : TActorRange<AStaticMeshActor>(world))
{
    UE_LOG(LogABGameInstance, Warning, TEXT("StaticMeshActor : %s"), *(entity->GetName()));
}

 

+ Recent posts