게임을 제작하는 과정에서 객체를 초기화할 때, 프로젝트에 포함된 다른 클래스나 오브젝트, 리소스를 가져와야하는 경우가 종종 생긴다.
그런 경우 블루프린트의 이벤트 그래프에서 작업하는 경우라면 위의 이미지와 같이 콘텐츠 브라우저에 있는 리소스나 블루프린트 클래스 등을 곧바로 선택할 수 있지만, C++ 코드에서는 직접 경로를 지정해서 코드를 작성해야 한다.
단, C++ 코드에서 직접 경로를 지정해서 리소스나 블루프린트 클래스를 가져올 때, 주의할 점은 리소스나 블루프린트 클래스의 경로나 파일명이 자주 바뀌는 상황을 피하는 게 좋다. 경로를 지정한 이후에 경로가 바뀌지 않을 것이 확실하다면 C++ 코드로 경로를 지정해서 가져오는게 낫겠지만 자주 바뀌는 상황이라면 바뀐 리소스를 불러오는 모든 코드를 일일이 찾아서 수정하고 컴파일하는 문제가 발생한다.
그렇기 때문에, 경로나 리소스의 파일명이 자주 바뀔 상황이라면 위의 이미지처럼 블루프린트를 이용해서 초기화를 진행하거나, 별도의 기능을 만들어서 일일이 경로를 지정하고 바꾸는 작업을 자동화시키는 것이 좋다.
우선 C++ 코드에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 가져오기 위해서는 다음의 헤더를 전처리기로 포함시켜주어야 한다.
#include "UObject/ConstructorHelpers.h"
ConstructorHelpers는 생성자에 도움을 주는 클래스로 생성자에서 콘텐츠 브라우저의 리소스나 블루프린트 클래스를 불러오는 작업을 도와주는 기능들을 가지고 있다. ConstructorHelpers는 생성자에서 사용되는 기능이기 때문에 생성자 이외의 장소에서 ConstructorHelpers를 사용하려고 시도하면 컴파일 에러가 발생하게 된다.
C++ 코드에서 블루프린트 클래스 가져오기
콘텐츠 브라우저 패널에 Blueprints 폴더 안에 TestBlueprintClass라는 이름의 APawn 클래스를 상속받은 블루프린트 클래스가 있다고 가정할 때, 그것을 C++ 코드에 가져오기 위해서는 다음 예시와 같이 코드를 작성하면 된다.
static ConstructorHelpers::FClassFinder<APawn> BPClass(TEXT("/Game/Blueprints/TestBlueprintClass")); if (BPClass.Succeeded() && BPClass.Class != NULL) { // 가져온 BPClass.Class를 통한 작업 }
FString 경로를 통해서 불러오는 것이니 만큼, 오타나 변경된 경로나 파일명으로 인해서, 클래스가 제대로 불러와지지 않는 경우가 발생할 수 있기 때문에, Succeeded() 함수와 Class의 NULL 체크를 통해서 성공적으로 클래스가 불러와졌는지 체크하고 사용해야 한다.
클래스 탐색자(Class Finder)는 성공적으로 블루프린트 클래스를 가져온 경우, Class 멤버 변수 안에 TSubclassOf<T> 타입으로 해당 클래스를 가지고 있게 된다. 이것을 이용해서 필요한 작업을 진행하면 된다.
C++ 코드에서 리소스 가져오기
이번에 알아볼 것은 C++ 코드에서 콘텐츠 브라우저 패널의 리소스를 가져오는 과정이다. 리소스의 종류는 여러가지가 될 수 있는데 대표적인 것으로는 스태틱 메시나 텍스처를 예로 들 수 있다.
아래의 예시코드는 드롭된 아이템의 메시가 아이템의 종류에 따라서 달라진다는 가정하에 만들어졌다. 콘텐츠 브라우저의 Item/StaticMesh 폴더 안에 SM_Helmet 이라는 이름을 가진 헬멧 모양의 스태틱 메시가 있을 때, FObjectFinder를 통해서 가져올 수 있다.
FObjectFinder를 통해서 가져온 오브젝트 역시 Succeeded() 함수와 Object 변수의 null 체크를 통해서 리소스가 제대로 불러와졌는지 체크를 한 뒤 사용해야 한다.
C++ 코드에서 C++ 클래스 가져오기
C++ 코드에서 블루프린트 클래스가 아닌 직접 작성한 C++ 클래스를 가져와서 사용하고 싶을 수도 있다. 예를 들어 게임 모드 클래스에서 기본 폰이나 기본 플레이어 컨트롤러를 설정하려고 할 때, C++로 작성한 폰 클래스나 플레이어 컨트롤러 클래스를 기반으로 블루프린트 클래스를 생성해서 넣어주는게 아니라 C++ 클래스를 곧바로 코드에서 넣어주고자 한다면 다음 예시 코드와 같이 작성하면 된다.
게임을 제작할 때 레벨업에 필요한 경험치량이나 스킬의 계수 등 추후에 밸런스 수정 작업이 필요한 값들은 함부로 코드에 상수로 넣어서는 안된다. 이런 부분은 기획자가 손쉽게 접근이 가능해야 하기 때문에, 기획자들이 주로 사용하는 엑셀이나 스프레드시트의 데이터를 언리얼 엔진으로 임포트해서 사용하는 방식을 지원한다. 이것을 데이터 주도형 접근법이라고 한다.
언리얼 엔진에서는 기획자들이 주로 사용하는 엑셀이나 스프레드시트에서 손쉽게 만들어낼 수 있는 .CSV 파일이나 서버 프로그램에서 주로 사용되는 JSON 파일을 손쉽게 임포트하는 기능을 제공한다.
데이터 테이블 임포트
데이터 테이블은 유용한 방식으로 짜여진 표를 의미한다. .CSV 파일을 임포트하기 위해서는 우선 프로그래머가 데이터를 엔진이 인식할 수 있게 Row 컨테이너를 만들어서 엔진에 데이터 해석 방식을 알려줘야 한다.
우리가 예시로 사용할 .CSV 파일은 다음 레벨업까지 필요한 경험치의 양에 대한 것이고 그 내용은 다음과 같다.
이런 컨테이너를 만드는 방법은 두 가지가 있는데 블루프린트를 이용하는 방식과 C++ 코드를 통해 만드는 방식이 있다.
블루프린트
데이터 테이블 로우를 만들기 위해서는 구조체를 생성해야 한다. 구조체의 이름은 BP_LevelUpTableRow로 한다.
블루프린트 구조체가 생성되면 더블클릭해서 블루프린트 구조체 에디터를 열고 변수를 추가한다. 추가하는 변수의 이름은 ExpToNextLevel과 TotalExp로 각 열의 이름과 순서가 일치해야 한다. 제일 첫 열인 Name은 게임 내에게 각 행에 접근하는 이름이 되는 것으로 따로 변수를 추가하지 않아도 된다.
변수를 모두 추가한 뒤에는 구조체를 저장하고 에디터를 닫는다. 그리고 콘텐츠 브라우저 패널에서 파일 창에 우클릭하여 /Game에 임포트... 를 선택한다.
CSV 파일을 임포트한다.
데이터 테이블 옵션 창이 뜨면 데이터 테이블 행 유형 선택을 방금 추가한 구조체로 설정하고 확인을 누른다.
추가된 데이터 테이블을 열어보면 .CSV 파일의 내용이 훌륭하게 임포트된 것을 확인할 수 있다.
C++ 코드
행 컨테이너를 블루프린트 구조체로 만들 경우, C++ 코드에서는 사용할 수 없다는 단점이 있다. C++ 코드에서 사용하기 위해서는 USTRUCT로 만들어야 되는데 언리얼 구조에 대한 설명은 C++ / USTRUCT 사용자 정의 구조체 만들기 문서에서 참고할 수 있다.
우선 Actor 클래스를 상속받아서 CustomDataTables라는 더미 클래스를 생성한다.
클래스가 생성되면 전처리기와 클래스 선언 사이에 구조체를 선언하는 코드를 추가해준다. 행 컨테이너로 사용되는 구조체는 FTableRowBase를 상속받아야만 한다.
// Fill out your copyright notice in the Description page of Project Settings.
프로그래밍 작업을 할 때 캐스팅, 즉 형 변환은 상당히 중요하다. 특히 여러 클래스가 상속으로 엮여있는 상황이라면 더더욱 중요해진다. 언리얼 엔진에서는 대부분의 클래스가 AActor 클래스를 상속받고 있고, 개발자가 만들어내는 클래스 역시 상당수는 AActor를 상속받게 된다.
C++
그렇기 때문에 몇몇 함수들은 이렇게 메인이 되는 부모 클래스를 매개변수로 받거나 돌려준다. 간단한 예를 들자면 다음 함수가 있다.
NotifyActorBeginOverlap() 함수는 액터의 콜리전에 콜리전을 가진 다른 액터가 들어오기 시작했을 때 호출되는 함수로, 매개변수를 통해서 자신의 콜리전과 접촉한 액터를 알려준다. 언리얼 엔진에서는 레벨에 배치되는 모든 오브젝트는 AActor 클래스를 상속받기 때문에, 콜리전과 접촉한 액터가 어떤 클래스던지 상관없이 무조건 AActor 클래스로 보내주는 것이다.
만약 콜리전 체크를 하는 액터가 겹침 이벤트가 발생할때마다 데미지를 입는 클래스인데 데미지를 입힐 수 있는 클래스가 AProjectile 클래스라고 가정했을 때, 위의 예시 코드처럼 별도의 검사를 하지 않는다면, 액터가 아무 물체에나 스칠 때마다 데미지를 입어버릴 것이다.
그래서 필요한 것이 바로 캐스팅이다. 언리얼 엔진에서는 Cast<T>() 라는 함수로 기본적인 캐스팅을 제공한다.
void AActor::NotifyActorBeginOverlap(AActor* OtherActor)
{
AProjectile* Projectile = Cast<AProjectile>(OtherActor);
if (Projectile)
{
// Damage Process
}
}
바로 위의 예시 코드처럼 캐스팅을 진행하면 된다. 만약 콜리전에 검출된 액터가 AProjectile 클래스가 아니라면 캐스팅에 실패할 것이고 Projectile 변수의 값을 nullptr이 되기 때문에 if문 안으로 진행하지 못해서 Damage Process가 진행되지 않는다.
블루프린트
블루프린트 작업에서도 캐스팅이 가능하다.
블루프린트 컨텍스트 메뉴에서 "형변환"이나, 캐스팅하고자 하는 타입의 클래스 명을 검색하면 해당 클래스로 형변환할 수 있는 노드를 추가할 수 있다.
위의 코드는 틱이 작동하는 동안 bSeeingThrough의 상태에 따라서 다이내믹 머티리얼 인스턴스의 머티리얼 파라미터 "Opacity"를 0에서 1로 만들거나 1에서 0으로 만들고 작동이 끝나면 Tick() 함수의 작동을 멈추게 한다. 참고로 머티리얼 파라미터 "Opacity"는 이후 작업에서 추가한다.
SetShowSeeingThrough() 함수는 bShow 변수를 받아서 액터가 투명해지기 시작하는지 불투명해지기 시작하는지 결정한 뒤 Tick() 함수를 작동시킨다.
코드 작업이 끝났다면 솔루션 탐색기에서 프로젝트를 빌드한 뒤, 에디터로 돌아간다.
투명해지는 만들기
이번에는 SeeingThroughActor가 사용할 머티리얼을 만들 차례이다. Props 폴더 안에 Materials 폴더를 만든 다음, 콘텐츠 브라우저 패널의 파일 창에 우클릭해서 머티리얼을 선택한다. 그리고 생성된 머티리얼의 이름을 M_SeeingThrough로 한다.
머티리얼을 더블클릭해서 머티리얼 에디터를 열고 디테일 패널에서 Material 카테고리의 Blend Mode를 Translucent로 설정한다.
그리고 TextureSamleParameter2D와 ScalarParameter를 추가하고 각각 이름을 Texture와 Opacity로 한다.
그리고 Opacity 파라미터 노드를 선택한 뒤, 디테일 패널에서 Default Value를 1로 설정한다.
그 다음, 적용과 저장을 하고 머티리얼 에디터를 닫는다.
언리얼 에디터로 돌아와서, 콘텐츠 브라우저 패널에서 방금 만든 머티리얼을 우클릭하고 머티리얼 인스턴스 생성을 선택한다.
머티리얼 인스턴스가 만들어지면 더블클릭해서 머티리얼 인스턴스 에디터를 열고 디테일 패널에서 Opacity를 체크해주고 머티리얼 인스턴스를 저장한 뒤, 머티리얼 인스턴스 에디터를 닫는다.
SeeingThroughActor 배치
이제 SeeingThroughActor를 배치할 차례이다. 콘텐츠 브라우저 패널에서 SeeingTroughActor를 찾아서 벽 토대 위에 배치한다.