본문으로 바로가기

[Unreal Engine 4] 클래스 생성

category UnrealEngine/기능 2021. 5. 28. 22:51

이번에 만들 것은 표준 C++ 클래스이며, UCLASS라고 부르는 것을 만들 것이다.

UE4 블루프린트 편집기와 잘 통합되는 C++ 클래스와 구조체는 UCLASS 매크로를 제대로 구성하면 UCLASS를 복제하거나 재사용할 수 있게 만들어서 커스텀 C++ 오브젝트를 언리얼의 비주얼 스크립팅 언어 블루프린트 내에서 사용할 수 있게 된다.

만일 팀 내에 디자이너가 있다면, 코드에 접근하지 않고도 프로젝트의 여러 형상에 접근하고 수정할 수 있게 된다.

UCLASS에서 생성한 샘플 오브젝트는 복제하거나 재사용 가능하더라도 레벨에 배치되지 않는다.
배치되려면 C++ 클래스가 Actor base class 또는 그 아래의 서브클래스에서 파생되어야 한다.
이 내용은 나중에 다룬다.

UCLASS 매크로를 사용하면 할당과 해제를 할 때 클래스가 UE4의 스마트 포인터메모리 관리 루틴을 사용하게 된다.

이러한 스마트 포인터 규칙은 UE4 편집기에서 자동으로 불러와 읽을 수 있다.

UCLASS 매크로를 사용할 때, UCLASS 오브젝트의 생성과 삭제는 UE4에 의해 처리돼야한다.
오브젝트의 인스턴스를 생성할 때는 NewObject함수를 사용해야 하며
오브젝트를 삭제할 때는 UObject::ConditionalBeginDestroy() 함수를 사용해야 한다. new와 delete를 사용하면 안된다.

UE4 에디터에서 실행 중인 프로젝트의 File -> New C++ Class를 선택한다.

Add C++ Class 대화상자가 나타나면 창의 오른쪽 상단으로 이동해 Show All Classes 확인란을 선택한다.

다음 Object를 선택하고 Next를 클릭한다.

Add C++ Class 대화상자에선 'Object'라고 보이지만 이는 'UObject'이다. U가 선행된다는 점을 알아두자
UObject(Actor 이외의 분기)에서 파생된 UCLASS는 U로 시작하는 이름이어야 한다.
Actor에서 파생된 UCLASS는 A로 시작하는 이름이어야 한다.
이는 UE4에서 사용하는 이름 규칙임을 알아두자.

이름을 설정하고 Create Class를 선택한다. 나는 'UserProfile'로 설정했다.

Create Class를 선택하면 컴파일을 마친 후 파일이 생성된다. 이후, IDE를 열어 자신이 만든 파일의 .cpp 파일을 연다.

클래스의 헤더가 다음과 같은지 확인한다.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "UserProfile.generated.h"

/**
 * 
 */
UCLASS()
class CPPTUTORIAL_API UUserProfile : public UObject
{
	GENERATED_BODY()
	
};

프로젝트를 컴파일 하고 실행하면 UE4 편집기에서뿐만 아니라 IDE 내에서도 커스텀 UCLASS를 사용할 수 있다.

UE4는 커스텀 UCLASS를 위한 많은 양의 코드를 생성하고 관리한다. 이 코드는 UPROPERTY, UFUNCTION, UCLASS 매크로와 같은 UE4 매크로를 사용한 결과로 생성된다.

생성된 코드는 UserProfile.generated.h에 저장된다. 성공적으로 컴파일하려면 UCLASSNAME.generated.h 파일을 #include 해야한다.

기본적으로 편집기에 이 파일이 자동적으로 포함되며, 만약 포함하지 않으면 컴파일에 실패한다.

또한 UCLASSNAME.generated.h 파일은 UCLASSNAME.h의 마지막에 #include 되어야 한다.

 

UCLASS에는 동작에 변화를 주는 다수의 키워드가 존재한다. 다음은 Blueprint에 관련된 키워드들이다.

  • Blueprintable : UE4 편집기 내의 Class Viewer에서 블루프린트를 생성하고자 할 때 사용한다. 마우스 오른쪽 버튼을 클릭하고 Create Blueprint Class...를 선택해서 사용할 수 있다. 이 키워드를 사용하지 않으면 블루프린트화 할 수 없다.
  • BlueprintType : UCLASS를 다른 블루프린트에서 변수처럼 사용할 수 있다. 
  • NotBlueprintType : 이 블루프린트 변수 타입을 블루프린트 다이어그램에서 변수로 사용할 수 없게 된다. 변수 목록에서 Create Blueprint Class...가 나타나지 않는다.

Content Browser에서 마우스 오른쪽 클릭 -> Blueprint Class를 선택하여 UserProfile을 검색하면 보이게 된다.

다음은 변수의 선언에 대해 설명한다.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "UserProfile.generated.h"

UCLASS(Blueprintable)
class CPPTUTORIAL_API UUserProfile : public UObject
{
	GENERATED_BODY()

	UPROPERTY()
	float armor;

	UPROPERTY()
	float hp;
};

UPROPERTY() 매크로로 전달되는 파라미터는 변수에 관한 중요한 정보를 지정한다.

  • EditAnywhere : 이는 프로퍼티를 블루프린트에서 직접 수정할 수도 있고 게임 레벨 내의 각 UClass 오브젝트 인스턴스에서 수정할 수도 있음을 의미한다.
  • EditDefaultsOnly : 블루프린트의 값이 편집 가능하지만, 인스턴스 단위로는 편집할 수 없다.
  • EditInstanceOnly : 이렇게 하면, 기본 블루프린트 자체가 아닌 UClass 오브젝트의 게임 레벨 인스턴스에서 속성을 편집할 수 있다.
  • BlueprintReadWrite : 이는 블루프린트 다이어그램에서 특성을 읽고 쓸 수 있음을 의미한다. BlueprintReadWrite를 가진 UPROPERTY()는 public 멤버여야 하고, 그렇지 않으면 컴파일 되지 않는다.
  • BlueprintReadOnly : 속성은 C++에서 설정해야 하며 블루프린트에서 수정할 수 없다.
  • Category : 정리된 상태를 유지하기 위해 웬만하면 UPROPERTY()Category 속성을 지정해야 한다.
    Category는 속성 편집기에서 UPROPERTY()가 표시될 하위 메뉴를 결정한다.
    예를 들어 'Category=Stats'Stats 라는 영역에 속성들이 나타난다.
    만약 설정하지 않으면 UPROPERTY는 기본 카테고리인 UserProfile에 나타난다.

예를 들어,

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "UserProfile.generated.h"

UCLASS(Blueprintable)
class CPPTUTORIAL_API UUserProfile : public UObject
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, Category = stats)
	float armor;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = stats)
	float hp{ 10.0f };
};

다음과 같이 수정하고, 블루프린트화 시켜서 변수를 확인하면

다음과 같이 블루프린트에 나타나게 된다.

 

이제는 실제로 UCLASS를 인스턴스화 하는 방법을 알아보자.

UCLASS에서 다른 UCLASS를 인스턴스화 할 것이다.

UObject의 파생 모델을 인스턴스화할 수 있도록 NewObject라는 함수를 사용해야한다.

NewObject는 생성하려는 오브젝트의 C++ 클래스 이름만 취하는 것이 아니라, C++ 클래스의 클래스 파생 모델(UClass*) 도 필요로 한다. UClass* 참조는 파생된 클래스에 대한 포인터다.

 

우리는 특정 '블루프린트의 인스턴스'를 인스턴스화 할 것이다. 하지만 C++에서는 블루프린트의 인스턴스를 접근할 수 없다.

C++코드에서 인스턴스화 하기 위해 어떤 형태로든 블루프린트의 이름을 알려줄 방법이 필요하다.

 

이를 위해 TSubclassOf타입의 변수를 이용해 사용자가 편집할 수 있는 UPROPERTY를 제공해야 한다.

예를 들어, 플레이어 오브젝트가 C++ 코드에서 블루프린트로 파생되었다고 하자. 우리가 가지고 있는 UserProfile은 플레이어 오브젝트의 UCLASS를 알지 못한다. 

다음과같이 변수를 작성해서 해본다.

 

//...
UCLASS(Blueprintable)
class CPPTUTORIAL_API UUserProfile : public UObject
{
	GENERATED_BODY()

public:
//...
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Unit)
	TSubclassOf<UObject> classOfPlayer;

	UPROPERTY(EditAnywhere, meta = (MetaClass = "GameMode"))
	FStringClassReference UClassGameMode;
};

다음과 같이 UObject에서 파생된 인스턴스들만 보이게 된다.

 

UE4는 UClass 또는 예상되는 클래스 타입을 지정하는 다양한 방법을 제공한다.

 

  • TSubclassOf : TSubclassOf<> 를 사용하면, '<>' 사이에 있는 타입에서 파생된 인스턴스 (혹은 객체) 만 가리킬 수 있다. 위의 예제에서 'TSubclassOf<UObject>' 였으므로, UObject에서 파생된 모든 인스턴스들이 보이게 된다.
  • FStringClassReference : FStringClassReference'MetaClass'에서 파생될 것으로 예상되는 C++ 클래스를 가리킨다.
    위의 예제에서 meta = (MetaClass = "GameMode") 였으므로, GameMode에서 파생된 인스턴스 (혹은 객체) 들만 보이게 된다.
    모든 인스턴스를 보여주려 한다면 MetaClass를 그냥 두면 된다.

이제 실제로 UserProfile을 인스턴스화 해보자

 

UObject에서 파생된 클래스는 NewObject<> 를 사용한다.

Actor에서 파생된 클래스는 SpawnACtor<>를 사용한다.

UE4 편집기의 Content Browser를 보면 C++ Classes 폴더가 보일 것이다. 이 폴더는 C++ 인스턴스 (객체)들의 폴더인데, 이곳에서 GameModeBase를 찾아 열어본다.

 

다음과 같이 작성한다.

 

// GameModeBase.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "cpptutorialGameModeBase.generated.h"

UCLASS()
class CPPTUTORIAL_API AcpptutorialGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

	UPROPERTY()
	TSubclassOf<class UUserProfile> userProfileClass;

public:
	virtual void BeginPlay() override;
};
// GameModeBase.cpp
#include "cpptutorialGameModeBase.h"
#include "UserProfile.h"

void AcpptutorialGameModeBase::BeginPlay()
{
	userProfileClass = UUserProfile::StaticClass();
	
	auto profile = NewObject<UUserProfile>(userProfileClass);
}

Type::StaticClass() 를 통해 UClass*를 얻어낼 수 있다.

 

이와 반대로 ConditionalBeginDestroy() 함수를 통해 객체를 삭제할 수도 있다.

void AcpptutorialGameModeBase::BeginPlay()
{
	userProfileClass = UUserProfile::StaticClass();
	
	auto profile = NewObject<UUserProfile>(userProfileClass);

	if (profile)
	{
		profile->ConditionalBeginDestroy();
		profile = nullptr;
	}
}

ConditionalBeginDestroy() 함수는 내부 엔진 연결을 모두 제거하는 방식으로 파괴 프로세스를 시작한다.

내부 속성을 먼저 파괴한 후 실제 오브젝트를 파괴하는 과정으로 전체적인 프로세스를 진행한다.

 

때로는 class가 아닌 다수의 멤버를 갖는 데이터의 구조체를 만들고 싶을 때가 있는데, 이를 위해 USTRUCT를 사용한다.

 

어려울 건 따로 없으므로, 코드만 업로드하겠다.

// ColoredTexture.h

#pragma once

#include "ColoredTexture.generated.h"

USTRUCT()
struct CPPTUTORIAL_API FColoredTexture
{
	GENERATED_USTRUCT_BODY()
    
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUD)
    UTexture* texture;
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUD)
    FLinearColor color;
}

이를통해 색상과 텍스쳐를 관리하는 FColoredTexture Struct를 생성했다.

 

이번에는 열거형(enum)을 만들어보자. 열거형은 기본적으로 상수처리이므로 가독성을 높혀줄 뿐더러 성능 향상의 여지가 있다.

이 또한 코드만 업로드하겠다.

UENUM()
enum Status
{
    Stopped UMETA(DisplayName = "Stopped")
    Moving  UMETA(DisplayName = "Moving")
    Attacking UMETA(DisplayName = "Attacking")
};

다음과 같이 UCLASS() 내에서 UENUM()을 사용한다.

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Status)
TEnumAsByte<STatus> status;

DisplayName은 블루프린트 내에서 보여질 이름이다.