Unreal engine/Unreal C++

2_1_언리얼 C++ 모던객체지향 설계_인터페이스

devRiripong 2023. 9. 22.
반응형

강의 내용

언리얼 C++ 인터페이스의 선언과 활용

 

강의 목표

언리얼 C++ 인터페이스 클래스를 사용해 보다 안정적으로 클래스를 설계하는 기법의 학습

 

정리

언리얼 C++ 인터페이스

1. 클래스가 반드시 구현해야 하는 기능을 지정하는데 사용한다.

2. C++ 기본적으로 다중상속을 지원하지만 언리얼 C++의 인터페이스를 사용해 가급적 축소된 다중상속의 형태로 구현하는 것이 유지보수에 도움이 된다. 

3. 언리얼 C++ 인터페이스는 두 개의 클래스를 생성한다. 

4. 언리얼 C++ 인터페이스는 추상 타입으로 강제되지 않고, 내부에 기본 함수를 구현할 수 있다. 

 

언리얼 C++ 인터페이스를 사용하면, 클래스가 수행해야 할 의무를 명시적으로 지정할 수 있어 좋은 객체 설계를 만드는데 도움을 줄 수 있다. 

 

 

프로젝트 생성

실습 프로젝트를 만들어 보자

프로젝트 이름은 UnrealInterFace

Instance를 추가하는데 이번에는 다른 방식으로 추가를 해보자.

지난 시간에 생성한 MyInstance 클래스를 탐색기에서 복붙해 가져와보자.

비쥬얼 스튜디오에서 안보이면 언리얼 에디터의 Tools에서 Refresh Visual Studio 2022 Project를 누른다.

이걸 그대로 쓸 수 있는 건 아니다.

UCLASS()
class OBJECTREFLECTION_API UMyGameInstance : public UGameInstance
{

헤더파일을 보면 OBJECTREFLECTION_API라는 디파인된 전처리 구문이 있어. 외부 모듈이 현재 언리얼 인터페이스 모듈 내의 클래스인 UMyGameInstance를 접근할 수 있는지 지시하는 키워드야. 키워드가 정의되어 있지 않기 때문에 컴파일 에러가 발생한다. 이걸 현재 프로젝트에 맞는 키워드로 변경을 해줘야 해.

보충설명

===============================================================

Unreal Engine 프로젝트에서는 여러 개의 모듈을 가질 수 있고, 각 모듈은 코드를 구성하는 하나의 단위입니다. 이 때 모듈 간에 클래스나 함수 등을 공유하기 위해서는 특별한 식별자를 사용합니다. 이 식별자가 바로 _API로 끝나는 매크로입니다. (OBJECTREFLECTION_API, UNREALINTERFACE_API 등)

  • OBJECTREFLECTION_API: 이는 원래 "ObjectReflection" 프로젝트에서 정의된 클래스가 외부 모듈에서 접근 가능하도록 하기 위한 매크로입니다.
  • UNREALINTERFACE_API: 이는 "UnrealInterface" 프로젝트에서 클래스가 외부에서 접근 가능하도록 하기 위한 매크로입니다.

이런 매크로는 일반적으로 프로젝트의 빌드 설정에서 자동으로 생성되므로, 다른 프로젝트에서 코드를 가져왔다면 해당 프로젝트의 매크로로 변경해주어야 합니다.

===============================================================

프로젝트 이름과 같게 UNREALINTERFACE_API로 키워드를 바꿔준다.

class UNREALINTERFACE_API UMyGameInstance : public UGameInstance

헤더를 변경했기 때문에 에디터를 닫아주고 컴파일을 해보자.

.cpp파일도 변경한다. Student와 Teacher를 제거해주고 Init 함수 내에 있는 것들도 제거해준다.

컴파일 후 디버깅 없이 실행을 한다.

그리고 언리얼의 Project setting에서 map을 클리어하고, Game Instance class를 MyGameInstance로 설정해준다.

그리고 에디터를 재시작한다.

클래스 생성

이번에는 인물객체인 Person을 생성해보자. 기본 Object를 상속 받아서 Person이라고 한다.

그리고 Person을 상속받은 세가지 Student, Teacher, Staff 클래스를 만들어 보자.

Person에다가 이름 속성을 부여해보자.

reflection에 관련된 헤더 수정이 들어가므로 에디터를 끄고 작업을 하는게 안전하다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

/**
 * 
 */
UCLASS()
class UNREALINTERFACE_API UPerson : public UObject
{
	GENERATED_BODY()

public: 
	UPerson(); 
	
	FORCEINLINE FString& GetName() { return Name; }
	FORCEINLINE void SetName(const FString& InName) { Name = InName; }

protected: 
	UPROPERTY()
	FString Name; 
	
};

이렇게 헤더의 코드를 작성하고, UPerson의 구현부에서 Name의 기본값을 설정한다.

UPerson::UPerson()
{
	Name = TEXT("홍길동"); 
}

그리고 한글을 사용하였기 때문에 다른 이름으로 저장을 해서 유니코드의 설정을 바꿔준다.

Student, Teacher, Staff도 동일하게 생성자를 선언하고 구현부에서 이름을 설정해준다.

각각에서 이름을 한글로 설정해 줬다면 각각 파일의 인코딩을 유니코드로 바꿔준다.

컴파일을 하고, MyGameInstance로 가서 Teacher, Student, Staff한명을 array로 묶어보자.

#include "MyGameInstance.h"
#include "Student.h"
#include "Teacher.h"
#include "Staff.h"

UMyGameInstance::UMyGameInstance()
{
	SchoolName = TEXT("기본학교");
}

void UMyGameInstance::Init()
{
	Super::Init();

	UE_LOG(LogTemp, Log, TEXT("===================================")); 
	TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };
	for (const auto Person : Persons)
	{
		UE_LOG(LogTemp, Log, TEXT("구성원 이름 : %s"), *Person->GetName());
	}
	UE_LOG(LogTemp, Log, TEXT("==================================="));
}

UPerson* 의 Array를 만들 수 있어.

언리얼 엔진이 제공하는 라이브러리로 TArray가 있다.

디버깅 없이 실행을 한 뒤 플레이를 눌러 LogTemp로 필터를 걸고 보면

 

이렇게 하고 이번엔 인터페이스를 추가해보자.

인터페이스 생성

C++ 추가에서 CommonClass에서 UnrealInterface를 선택하고, LessonInterface라고 이름을 짓고 클래스를 생성해 본다.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "LessonInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class ULessonInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class UNREALINTERFACE_API ILessonInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
};

UINTERFACE라는 매크로가 선언되어 있고, U로 시작하는 클래스가 하나 선언되어 있다.

타입 정보를 관리하기 위해서 언리얼 엔진이 자동으로 생성한 클래스인데, 여기서 딱히 뭔가를 할 필요는 없다. 타입정보를 보관하기 위해 생성한 클래스다 정도로 이해하면 된다.

실제로 구현하기 위한 인터페이스에 대한 내용은 ILessonInterface라는 클래스에 있다.

여기에 인터페이스에 관련된 함수들과 기능을 구현할 수 있다고 할 수 있다.

이 섹션을 이용해 인터페이스를 직접 구현해 보도록 하자.

abstract 가상함수로 선언한 경우

class UNREALINTERFACE_API ILessonInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void DoLesson() = 0;
};

이렇게 abstract 가상함수로 선언하게 되면, 인터페이스를 상속받는 클래스들은 반드시 이 DoLesson 함수를 구현을 해줘야 한다.

상속 관계를 이용해서 특정 클래스에게 구현을 강제할 수 있는데, 지금 우리가 생성한 세가지 클래스 중에서 Teacher와 Student에게 이 인터페이스를 상속받게 해서 구현을 강제해보도록 하자.

#include "CoreMinimal.h"
#include "Person.h"
#include "LessonInterface.h"
#include "Student.generated.h"

/**
 * 
 */
UCLASS()
class UNREALINTERFACE_API UStudent : public UPerson, public ILessonInterface
{
	GENERATED_BODY()
	
public: 
	UStudent(); 

	virtual void DoLesson() override; 
};

이렇게 하고 DoLesson의 구현부는 아무 내용도 넣지 않는다.

Teacher로 가서 동일한 내용을 구현해 본다.

만약 인터페이스를 상속만 받고 가상함수를 구현하지 않고 언리얼에서 alt+ ctrl+ f11로 빌드를 하면 에러가 뜬다.

D:\UE5Part1\UnrealInterface\Source\UnrealInterface\Teacher.h(16): error C2259: 'UTeacher': cannot instantiate abstract class

그렇기 때문에 구현을 강제할 수 있다.

Teacher에 가서 DoLesson의 인터페이스 구현부를 구현해주자.

Student.cpp에서는 공부를 하는 거니

void UStudent::DoLesson()
{
	UE_LOG(LogTemp, Log, TEXT("%s님은 공부합니다."), *Name);
}

이렇게 하고, 선생님은 가르쳐야 하니,

void UTeacher::DoLesson()
{
	UE_LOG(LogTemp, Log, TEXT("%s님은 가르칩니다."), *Name); 
}

Abstract 가상함수가 아닌 인터페이스에서 구현한 경우

언리얼 C++의 인터페이스의 특징 중 하나는 DoLesson이란 함수를 Abstract 상태로 유지를 하는 것이 좋지만, 꼭 그렇게 해야 할 필요는 없다. LessonInterface.h의 DoLesson에 코드를 넣을 수 있다는 얘기다.

virtual void DoLesson()
	{
		UE_LOG(LogTemp, Log, TEXT("수업에 입장합니다.")); 
	}

이렇게 할 경우 상속받은 하위 클래스의 경우는 강제로 구현해 주지 않아도 무방하다.

추상 클래스가 아니기 때문에 Teacher의 DoLesson의 구현부를 주석처리 한 뒤 ctrl+alt+f11로 컴파일을 해도 아까와 같은 에러가 발생하지 않는 것을 알 수 있다.

이건 여러분들이 선택하기 나름인데 언리얼 C++에 맞는 후자의 경우로 진행을 해보겠다.

LessonInterface에서 기본적인 로그를 구현하고, Teacher와 Student는 필요에 따라 추가적으로 구현을 하는 것으로.

모던 객체 지향에서 추구하는 방식과는 거리가 먼데, 언리얼 소스 코드를 보면 이런 것들을 활용하는 것들이 종종 보인다. 그래서 엄격한 규칙을 따르기 보단 편리한 형태를 구현하는 것으로 활용해보겠다.

인터페이스에서 구현한 함수를 자식 클래스 함수에서 호출할 경우

LessonInterface에 구현된 로그도 출력하고, Student의 로그도 같이 출력하고 싶다고 하면, 상위 클래의 두 레슨 로직을 호출해줘야 하는데 이 때는 Super 키워드를 사용할 수 없다. Student의 Super는 Person으로 지정되어 있지, ILessonInterface로 지정되어 있지 않다. 이런 클래스 정보에 대해 단위 상속만 지원하기 떄문에 ILessonInterface의 DoLesson 함수를 Super로 가져올 수 없다.

이 때는 직접 입력을 해줘야 한다.

void UStudent::DoLesson()
{
	ILessonInterface::DoLesson(); 
	UE_LOG(LogTemp, Log, TEXT("%s님은 공부합니다."), *Name);
}
void UTeacher::DoLesson()
{
	ILessonInterface::DoLesson();
	UE_LOG(LogTemp, Log, TEXT("%s님은 가르칩니다."), *Name); 
}

이렇게 하고 다시 언리 에디터로 돌아가서 빌드를 한다.

빌드가 잘 된다.

인터페이스를 상속 받은 클래스와 받지 않은 클래스 구분 방법

수업을 받을 수 있는 구성원과 없는 구성원을 구분하는 걸 해볼 건데 그러기 위해서는 ILessonInterface를 상속 받았는지 체크 해주면된다. 이 때 유용하게 사용하는 것이 형변환, 캐스팅 연산자가 된다. 언리얼 엔진은 안정적으로 캐스팅을 할 수 있기 때문에 만약 형변환에 실패하면 null값을 반환하게 되어서 우리가 이것을 구현했는지 구현 안했는지를 파악할 수 있게 된다. 이 코드를 작성해보자.

MyGameInstance.cpp로 가서

void UMyGameInstance::Init()
{
	Super::Init();

	UE_LOG(LogTemp, Log, TEXT("===================================")); 
	TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };
	for (const auto Person : Persons)
	{
		UE_LOG(LogTemp, Log, TEXT("구성원 이름 : %s"), *Person->GetName());
	}
	UE_LOG(LogTemp, Log, TEXT("==================================="));

	for (const auto Person : Persons)
	{
		ILessonInterface* LessonInterface = Cast<ILessonInterface>(Person); 
		if (LessonInterface)
		{
			UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 있습니다."), *Person->GetName());
		}
		else
		{
			UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 없습니다."), *Person->GetName());
		}
	}
	UE_LOG(LogTemp, Log, TEXT("==================================="));

}

이렇게 구현하고 출력 로그를 통해 확인해 보자.

이렇게 확인할 수 있다.

이제 마지막으로 수업에 참여할 수 있는 사람은 DoLesson 함수를 호출해 준다.

for (const auto Person : Persons)
{
	ILessonInterface* LessonInterface = Cast<ILessonInterface>(Person); 
	if (LessonInterface)
	{
		UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 있습니다."), *Person->GetName());
		LessonInterface->DoLesson(); 
	}

그리고 LessonInterface.h의 인코딩 설정을 한다.

컴파일을 하고,

플레이를 누르면

이렇게 언리얼 C++ 를 활용해서 간단한 교실 활동에 대해 구현해 보았다.

반응형

댓글