액터

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

 

이번 강좌에서는 드디어 언리얼 엔진의 게임 컨텐츠 핵심이라 할 수 있는 액터(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++코드)

 

 

+ Recent posts