[UE5] Unreal Engine 5 길라잡이 - 4. 액터 배치 및 컴포넌트 생성

세동네·2022년 6월 10일
0
post-thumbnail

이 시리즈는 이득우의 언리얼 C++ 게임 개발의 정석을 바탕으로 작성되었습니다.

지난 포스팅에서 Fountain이라는 액터 클래스를 만들어 보았다. 다음 단계는 해당 액터 클래스를 레벨에 배치하는 등 활용해보는 것이다. 다시 언리얼 에디터를 실행해 액터 클래스를 찾아보자.

액터 클래스가 사라졌다.

실수로 삭제했을리는 없지만, 혹시 모르니 다시 만들어보려고 했다. 하지만 분명히 아직 존재한다고 한다. 솔루션 파일을 IDE에서 확인해보아도 소스 파일들을 정상적으로 확인할 수 있다.

이때는 당황하지 말고 [Ctrl + Alt + F11] 단축키를 입력해 컴파일을 해보자.

· 라이브 코딩

앞선 [Ctrl + Alt + F11] 단축키는 UE5 에디터의 컴파일 역할을 하는 라이브 코딩 기능을 사용하는 것이다. UE4 버전까지 언리얼 엔진에는 컴파일 버튼이 따로 있었고, 에디터 실행 중에 컴파일을 하면 이전 모듈을 모두 내리고 새로운 임시 모듈을 만들어 적용하는 핫 리로드 시스템을 사용했었다.

UE5에 오면서 이 핫 리로드 방식을 비활성화하고 라이브 코딩 방식을 디폴트로 적용한다. 라이브 코딩 기능을 사용하면 핫 리로드 시스템보다 유연하고 훨씬 빠르게 C++ 코드를 리컴파일하고 프로젝트의 바이너리를 패치할 수 있다. 또한 에디터에서 플레이(Play In Editor, PIE)를 사용하는 도중에도 패치가 가능하다.

라이브 코딩을 완료하면 액터 클래스를 다시 돌려낼 수 있다!

만약 언리얼 엔진 한글 버전을 이용 중이라면, 라이브 코딩 중 이런 상황을 마주할 수 있다.

에러 메시지가 깨지는 현상을 마주하는 것인데, 이는 컴퓨터의 언어 설정을 바꿔주면 된다. 윈도우 기준 [제어판 - 시계 및 국가 - 국가 또는 지역 - 관리자 옵션 - 시스템 로캘 변경]에서 'Beta: 세계언어 지원을 위해 Unicode UTF-8 사용' 항목을 체크해주면 된다.

· 액터 배치

콘텐츠 브라우저 패널에서 액터 클래스를 선택한 뒤 뷰포트 패널에 드래그 앱 드롭을 하면 위와 같이 액터가 레벨에 배치된다. 하지만 아직 메시가 없어 형태는 존재하지 않는다. 컴포넌트는 에디터 디테일 패널에서 추가할 수도 있지만, 클래스 파일에 코드로 컴포넌트를 추가해줄 수도 있다.

· 컴포넌트 선언

Fountain.h 파일을 열어 클래스 몸체에 public으로 다음과 같이 선언해주자.

// Fountain.h
...

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

	UPROPERTY()
	UStaticMeshComponent *Body;

	UPROPERTY()
	UStaticMeshComponent *Water;
 
 };
 

UStaticMeshComponent 클래스를 포인터로 선언했으므로 구현부에서 메모리를 동적으로 대입할 것이다. 일반 C++ 프로그래밍은 이렇게 포인터를 선언하면 명시적으로 객체를 소멸시켜야 차후 메모리 관리에 문제가 발생하지 않지만, 언리얼 엔진은 언리얼 실행 환경(Runtime)을 통해 객체가 더 이상 사용되지 않으면 할당된 메모리를 자동으로 소멸시키는 기능을 제공한다.

UPROPERTY() 매크로는 언리얼이 우리가 선언한 객체를 자동으로 관리하게 만들어주는 기능을 한다. 해당 매크로를 넣는 것을 잊어버려 메모리 관리에 문제가 생기고 원인을 파악하기 힘든 에러들이 발생하기 때문에, 매크로 선언을 잊어버리지 않도록 주의해야 한다.

하지만 모든 객체를 UPROPERTY() 매크로로 관리해줄 수 있는 것은 아니다. 언리얼 오브젝트라는 특별한 객체에만 사용할 수 있는데, 이는 언리얼 실행 환경에 의해 관리되는 C++ 객체를 의미한다. 콘텐츠를 구성하는 객체들은 모두 언리얼 오브젝트라고 볼 수 있으며, 액터 그 자체, 혹은 스태틱 메시 컴포넌트는 콘텐츠를 구성하기 때문에 언리얼 오브젝트로 취급된다.

Fountain 액터 클래스에 generated.h 헤더가 포함된 것, UCLASS()GENERATED_BODY() 매크로를 선언한 것, 클래스명에 A라는 클래스 이름 접두사가 붙어 액터로 취급되는 것 등 모든 것이 언리얼 오브젝트 클래스로 사용하기 위함이었던 것이다.

· 컴포넌트 객체 생성

다시 본론으로 돌아와, 스태틱 메시 컴포넌트를 선언했으니 이제 실체를 생성해야 할 것이다. Fountain.cpp 파일로 이동해 AFountain() 생성자에 다음 구문을 추가해준다.

// Fountain.cpp

// Sets default values
AFountain::AFountain()
{
	PrimaryActorTick.bCanEverTick = true;

	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
	Water = CreateAbstractDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));

	RootComponent = Body;
	Water->SetupAttachment(Body);

}

컴포넌트를 동적으로 생성하기 위해 언리얼 엔진에선 new 키워드 대신 CreateDefaultSubobject() API라는 특별한 함수를 제공한다. <UStaticMeshComponent> 타입의 서브오브젝트를 생성하며, 인자로 액터에 속한 컴포넌트를 구별용의 Hash를 생성하는데, 다른 컴포넌트와 중복되지 않아야 함에 유의하자.

따라서 구별이 쉬운 문자열을 인자로 전달하였는데, 언리얼 엔진은 문자열을 생성할 때 모든 플랫폼에서 2바이트 문자열 체계를 동일하게 유지하는 TEXT 매크로를 사용한다.

분수대의 몸체에 해당하는 스태틱 메시 컴포넌트가 Body, 분수대에서 뿜어져 나오는 스태틱 메시에 해당하는 컴포넌트가 Water이므로 루트 컴포넌트는 Body로 설정해주고, Wayer 컴포넌트를 Body에 종속시켜주었다. Water도 트랜스폼을 갖는 씬 컴포넌트이므로 루트 컴포넌트에 종속되어야 함을 잊지 말자.

· 컴포넌트 편집

이처럼 작성하고 에디터로 돌아와 [Ctrl + Alt + F11]를 눌러 컴파일해주자.

Fountain 객체를 선택하고 디테일 패널에서 Body 컴포넌트가 생성된 것을 관찰할 수 있다. 이때 아직 편집이 가능한 형태가 아니기 때문에 루트 컴포넌트의 자식 컴포넌트를 관찰할 수 없고, Body 컴포넌트의 각 세션이 회색 창으로 비활성화된 것을 확인할 수 있다.

컴포넌트를 에디터에서 편집 가능하도록 설정해주자. Fountain.h 파일로 이동해 컴포넌트 변수의 선언부를 다음과 같이 수정해준다.

// Fountain.h
...

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

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Body;

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Water;
 
 };
 

컴포넌트 변수 선언 윗 줄의 UPROPERTY() 매크로 안에 VisibleAnywhere이라는 키워드가 추가된 것을 확인할 수 있다. 수정사항을 저장하고 에디터로 돌아가서 컴파일해준다.

컴포넌트를 편집할 수 있도록 UI 설정이 변경되었고, 루트 컴포넌트의 자식 컴포넌트들도 정상적으로 확인할 수 있다.

이제 액터에 스태틱 메시를 지정해줄 수 있다.

이번 포스팅의 스태틱 메시 애셋은 [에픽스토어 언리얼 엔진 마켓플레이스]의 "Infinity Blade: Grass Lands"를 사용하였다.

무료 애셋이며, 애셋을 구매한 뒤 사용 중인 프로젝트에 추가해주면 즉시 사용할 수 있다.

Body의 스태틱 메시는 SM_Plains_Castle_Fountain_01, Water의 스태틱 메시는 SM_Plains_Fountain_02로 지정해주었다. Water의 메시 트랜스폼이 적절하지 않을 텐데, Water 컴포넌트를 선택한 후 트랜스폼 Z 값을 적당히 조절해주자. 컴포넌트 선택 후 에디터에서 w 키를 누르면 트랜스폼 변경 모드로 돌입할 수 있다.

파란색 화살표를 클릭 앤 드래그하면 Z 값을 조절할 수 있다. 혹은 Fountain.cpp의 생성자 함수 몸체에 아래와 같은 구문을 추가해 컴포넌트의 기본 위치 값을 변경할 수도 있다.

// Fountain.cpp

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

	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BODY"));
	Water = CreateAbstractDefaultSubobject<UStaticMeshComponent>(TEXT("WATER"));

	RootComponent = Body;
	Water->SetupAttachment(Body);

	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 135.0f));
}

이때 새로운 접두사 F를 마주하는데, 이는 언리얼 오브젝트와 관련 없는 일반 C++ 클래스 혹은 구조체를 의미한다. 위와 같은 코드로 기본 위치 값을 변경하면 디테일 패널에서 위치 프로퍼티가 { 0.0, 0.0, 135.0 }으로 기본 설정된 것을 확인할 수 있다.

같은 방식으로 UPointLightComponentUParticleSystemComponent를 추가하여 분수대에 조명과 물이 찰랑이는 듯한 효과를 줄 수 있다.

· 언리얼의 값 유형

앞선 컴포넌트와 같은 언리얼 오브젝트의 클래스 포인터를 언리얼의 '객체 유형'이라고 하고, 정수형 변수나 문자열 변수와 같은 데터를 '값 유형'이라 한다. 언리얼 C++에서 사용하는 값 유형은 일반 C++ 값 유형과 조금씩 다른데, 언리얼 오브젝트의 대표적인 값 유형은 다음과 같다.

  • 바이트 : uint8
  • 정수 : int32
  • 실수 : float
  • 문자열 : FSting, FName
  • 구조체 : FVector, FRotator, FTransform

이 값 유형으로 클래스 멤버 변수를 선언하고 UPROPERTY() 매크로를 붙이면 따로 초기화를 해주지 않아도 기본값으로 초기화된다. 또한, 이러한 값 유형 멤버 변수를 디테일 패널에서 확인하고자 한다면 마찬가지로 UPROPERTY() 매크로에 특정 키워드를 추가해주면 되는데, 객체 유형에선 VisibleAnywhere를 추가해주었지만, 값 유형에선 EditAnywhere를 추가해주어야 한다.

- UPROPERTY() 속성 지정자

잘 생각해보면 VisibleAnywhere 키워드를 붙였을 때 객체를 수정할 수 있었던 것에서 이상함을 느낄 수 있다. 그저 '볼 수 있게' 했을 뿐인데, 수정까지 할 수 있다니, 값 유형 멤버 변수를 선언하고 컴파일해보자.

// Fountain.h

...


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

	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent *Body;

	...

	UPROPERTY()
	int32 ID;
};

ID 멤버 변수의 존재는 디테일 패널에서 찾아볼 수 없다. 그렇다면 IDUPROPERTY() 매크로에 VisibleAnywhere 키워드를 추가해보자.

	...
    
	UPROPERTY(VisibleAnywhere)
	int32 ID;
};

Fountain 섹션이 추가됐고, ID 필드를 확인할 수 있다. 하지만 VisibleAnywhere의 뜻 그대로 '볼 수 있을'뿐, 값을 수정할 수는 없다. 그런데 왜 객체 유형은 수정할 수 있는 것일까?

말의 의미가 조금 다른데, '객체 유형 변수 자체'를 수정하는 것이 아니라, 변수의 속성을 변경할 수 있는 것이다. 언리얼 엔진은 객체 유형에 VisibleAnywhere 키워드를 추가했을 때 해당 객체 유형 변수의 속성을 조회하고 수정하는 기능을 제공한다.

앞서 값 유형 변수는 EditAnywhere 키워드를 추가해야 필드를 수정할 수 있다고 했다.

	...
    
	UPROPERTY(EditAnywhere)
	int32 ID;
};

이와 같이 필드를 선택해 값을 변경할 수 있게 된다. 그렇다면 객체 유형 변수에 EditAnywhere 키워드를 추가하면 어떻게 될까?

	...
    
	UPROPERTY(EditAnywhere)
	UStaticMeshComponent *Body;
    
    ...

스태틱 메시 섹션에 있던 Body 컴포넌트가 Fountain 섹션으로 이동했고, '스태틱 메시'를 변경하는 것이 아닌 '컴포넌트 유형'을 변경할 수 있다. 이 말인 즉슨 '변수 유형'을 수정할 수 있게 되는 것이다. 같은 스태틱 메시 유형의 컴포넌트 안에서 그 종류를 선택할 수 있게 된다. 스태틱 메시 애셋을 고르는 것은 스태틱 메시 컴포넌트 변수가 가지는 부가적인 속성일 뿐이다.

이 경우 스태틱 메시 애셋을 고르는 속성은 Fountain 액터의 Body 컴포넌트를 들어가서 변경할 수 있다.

또한, UPROPERTY() 매크로에 Category 키워드를 추가하면 해당 멤버 변수가 속할 섹션을 직접 지정해줄 수 있다.

	...
    
	UPROPERTY(EditAnywhere, Category = "ID")
	int32 ID;
};

· 액터 생성과 동시에 애셋 지정

같은 액터를 레벨에 배치할 때마다 애셋 정보를 계속 직접 지정해주는 것은 상당히 번거로운 일일 것이다. 따라서 액터를 레벨에 배치할 때 자동으로 애셋을 지정해주는 방법을 알아보자.

먼저 필요한 것은 원하는 액터의 경로 정보이다. 언리얼 에디터 콘텐츠 브라우저 패널에서 원하는 액터를 검색해 우클릭 후 레퍼런스 복사를 눌러주면 애셋의 경로를 알아낼 수 있다.

StaticMesh'/Game/InfinityBladeGrassLands/Environments/Plains/Env_Plains_Ruins/StaticMesh/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'

바로 아래에 파일 경로 복사 기능이 있는데, 이는 PC 저장소에 저장된 경로이고, 언리얼 엔진에서 사용되는 애셋 주소는 '레퍼런스'라고 한다.

위 레퍼런스에서 '' 내의 경로만 사용할 것이다. 앞의 StaticMesh는 오브젝트 타입을 말하며, 해당 애셋 경로 정보를 사용하기 위해선 불필요한 정보이다. Fountain.cpp 파일로 이동해 Fountain() 생성자에 다음과 같은 코드를 추가해주자.

// Fountain.cpp

AFountain::AFountain()
{

	...
    
	ConstructorHelpers::FObjectFinder<UStaticMesh> SM_BODY(TEXT("/Game/InfinityBladeGrassLands/Environments/Plains/Env_Plains_Ruins/StaticMesh/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01"));

	if (SM_BODY.Succeeded()) {
		Body->SetStaticMesh(SM_BODY.Object);
	}
}

위와 같은 ConstructorHelpers::FObjectFinder() 함수를 이용해 애셋에 대한 포인터를 가져올 수 있다.

ConstructorHelpers 클래스는 이름에서 유추할 수 있듯이 언리얼 오브젝트의 생성자 안에서만 사용할 수 있는 클래스이다. 이후 <> 안에 가져올 애셋의 타입을 지정하고 애셋의 경로를 FText로 만들어 전달한다. 이때 위에서 복사한 레퍼런스의 작은 따옴표 안 경로를 TEXT() 함수의 인자로 전달한다.

애셋이 존재하여 정상적으로 포인터를 가져왔다면 FObjectFinder의 멤버 함수 Succeeded()true를 반환하는데, 이때 원하는 스태틱 메시 컴포넌트에 해당 애셋의 오브젝트를 지정해줄 수 있다.

위 코드를 저장하고 언리얼 에디터로 돌아와 컴파일한 후 콘텐츠 브라우저 패널에서 Fountain 액터를 레벨에 끌어와 배치하면 아래와 같이 애셋이 자동으로 로드되는 것을 확인할 수 있다.

물론 아직 Body에 대한 애셋만 지정해주었으므로 WaterSplash의 애셋은 지정되지 않은 상태이다. 다른 애셋들도 같은 코드를 작성해주면 아래와 같은 결과를 얻을 수 있다.

추가로 애셋의 레퍼런스는 게임 실행 중에 변경될 일이 없어 게임에서 생성자 코드를 여러번 호출해야 할 때마다 지역 변수를 생성하고 초기화하는 것은 불필요한 작업이다. 따라서 가능하다면 해당 지역 변수는 static으로 선언해 한 번만 초기화하는 것이 바람직하다.

// Fountain.cpp

AFountain::AFountain()
{

	...
    
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_BODY(TEXT("...")); 
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_WATER(TEXT("...")); 
	static ConstructorHelpers::FObjectFinder<UParticleSystem> PS_SPLASH(TEXT("..."));

	...
}

· 참고

프로퍼티 속성 지정자 VisibleAnywhere과 EditAnywhere의 차이
ConstructorHelpers::FObjectFinder()에 대하여

0개의 댓글