언리얼 C++ 인터페이스
- 반드시 구현할 행동을 지정하는데 활용한다.
- 접두사 U-> Type 클래스 / 접두사 I -> 인터페이스 클래스
- 언리얼 C++ 인터페이스는 자바 등과 다르게 기본로직에 구현이 가능하다.
- 인터페이스에서 가상 함수를 선언하게 되면 상속받은 클래스들은 구현이 필수이다.
- 가상함수를 인터페이스 내에서 꼭 가상으로 유지할 필요 없다. 인터페이스 선언에서도 코드 구현이 가능하다. (단지 이때는 하위 클래스에서 구현을 하면 안된다. 추상이 아니므로.)
class UNREALINTERFACE_API ILessonInterface
{
GENERATED_BODY()
public:
UNREALINTERFACE_API (프로젝트 이름_API)
이 전처리 구문은 외부 모듈이 현재 모듈 내의 클래스에 접근 가능한지 지시하는 키워드이다.
FORCEINLINE FString& GetName() { return Name; }
FORCEINLINE void SetName(const FString& InName) { Name = InName; }
FORCEINLINE 키워드는 100퍼센트 인라인 실행을 보장해주지는 않지만, 함수 호출 시 구문을 복사해서 호출 지점에 삽입한다. 따라서 오버헤드를 최대한 줄일 수 있다.
GPT가 설명하는 인라인 함수를 만들어야 하는 경우
인라인 함수를 만들어야 하는 경우는 여러 가지가 있습니다. 여기에는 다음과 같은 이유들이 있습니다:
짧은 함수: 함수가 매우 간단하고 짧을 때, 인라인으로 만들면 함수 호출에 따른 오버헤드를 줄일 수 있습니다. 함수 호출에 따른 스택 프레임 생성 및 제거, 인자 전달 등의 비용을 줄여서 실행 속도를 향상시킬 수 있습니다.
빈번한 호출: 함수가 자주 호출되는 경우에는 인라인으로 만들면 호출하는 곳마다 함수의 내용이 복사되어 실행되므로, 함수 호출에 따른 오버헤드를 줄일 수 있습니다. 이는 코드 실행 속도를 향상시키는 데 도움이 됩니다.
템플릿 함수: 템플릿 함수의 경우 호출될 때마다 컴파일러가 해당 함수를 인스턴스화하여 새로운 함수를 생성합니다. 이 때 인라인 함수로 만들면 컴파일 시점에 함수의 내용이 호출 지점에 삽입되므로 코드의 중복을 줄일 수 있습니다.
접근 지역성(Locality of reference): 인라인 함수를 사용하면 함수 호출 시에 해당 함수의 코드가 호출 지점에 삽입되므로, CPU 캐시의 지역성을 높일 수 있습니다. 이는 코드 실행 성능을 향상시키는 데 도움이 됩니다.
따라서 함수가 짧고 간단하며 빈번하게 호출되는 경우에는 인라인으로 만들면 실행 속도를 향상시킬 수 있습니다. 그러나 함수가 너무 크거나 복잡하거나, 호출 빈도가 낮은 경우에는 인라인으로 만들지 않는 것이 좋습니다. 인라인 함수로 만들 때는 함수의 크기와 호출 빈도 등을 고려하여야 하며, 최적화된 코드를 작성하는 데 도움이 됩니다.
추가로 읽어볼 거리
https://sheep-adult.tistory.com/69
[Unreal Engine 5] FORCEINLINE
FORCEINLINE은 함수를 강제적으로 inline화 시킨다. inline화란 컴파일 단계에서 컴파일러가 함수 호출 지점에 함수 내용을 갖다 붙이는 것을 말한다. 왜냐하면, 간단한 함수의 경우(한 줄짜리 getter, set
sheep-adult.tistory.com
UCLASS()
class UNREALINTERFACE_API UStudent : public UPerson, public ILessonInterface # 인터페이스 상속
{
GENERATED_BODY()
아래 코드에서 Student 는 Person 의 상속을 받고 있기 때문에 Super 키워드로 인터페이스의 함수를 호출할 수 없다. 따라서 어쩔 수 없이 아래처럼 직접 호출해 주어야 한다.
추가로 인터페이스 구현을 해야 하는지 아닌지는 인터페이스를 상속 받았는지의 여부를 형변환(Cast) 를 통해 알아내면 된다.
모던 객체지향 설계에서 인터페이스는 반드시 구현해야 하는 기능을 지정하는 데 유용하다.
UMyGameInstance::UMyGameInstance()
{
SchoolName = TEXT("기본학교");
}
void UMyGameInstance::Init()
{
Super::Init();
UE_LOG(LogTemp, Log, TEXT("============================"));
TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() }; # TArray 는 추후 설명
for (const auto Person : Persons) # For Each 문으로 순회
{
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());
LessonInterface->DoLesson();
}
else
{
UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 없습니다."), *Person->GetName());
}
}
UE_LOG(LogTemp, Log, TEXT("============================"));
}
for each 순회 할 때 const 를 붙이는 이유는 코드의 안전성과 의도를 명확히 하기 위해서이다.
컴포지션
- 상속은 Is - A 형태인데 이것만 의존해서는 설계와 유지보수가 어렵다.
- 상속에 비해서 명세에 변경이 발생하더라도 구성 요소를 쉽게 변경할 수 있다.
- 컴포지션은 Has - A 관계를 구현하는 설계 방법이다.
- 컴포지션 관계에서 클래스 선언 시 전방선언 해 주는 것이 좋다(의존성 감소)
추가 읽어볼 거리
객체 지향적 관점에서의 has-a와 is-a 차이점
객체 지향의 꽃이라고도 할 수 있는 객체들 간의 '관계'는 설계에 있어서 중요하다고 할 수 있습니다. 객체 간의 관계를 정의하는 키(key)라고도 볼 수 있습니다. 이 포스팅에서 is-a 와 has-a에 대해
minusi.tistory.com
SOLID
언리얼 엔진에서의 컴포지션 구현 방법
- 하나의 언리얼 오브젝트에는 항상 클래스 기본 오브젝트 CDO 가 있다.
- 언리얼 오브젝트에 다른 언리얼 오브젝트를 조합할 때 두 가지의 선택지가 존재한다.
- 방법 1 : CDO 에 미리 언리얼 오브젝트를 생성해 조합한다(필수로 포함해야 하는 경우).
- 방법 2 : CDO 에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합한다(선택적으로 포함해야 하는 경우).
- 언리얼 오브젝트를 생성할 때 컴포지션 정보를 구축할 수 있다.
- 내가 소유한 언리얼 오브젝트를 Subobject 라고 한다.
- 나를 소유한 언리얼 오브젝트를 Outer 라고 한다.
예제에서는 방법 1 을 사용하였다.
ENUM(Card.h에 선언 되어있음)
UENUM()
enum class ECardType : uint8
{
Student = 1 UMETA(DisplayName = "For Student"),
Teacher UMETA(DisplayName = "For Teacher"),
Staff UMETA(DisplayName = "For Staff"),
Invalid
};
Person.h
// 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 UNREALCOMPOSITION_API UPerson : public UObject
{
GENERATED_BODY()
public:
UPerson();
FORCEINLINE const FString& GetName() const { return Name; } # const 반환을 명시해주는 이유는 레퍼런스는 데이터 조작이 가능하기 때문이다.
FORCEINLINE void SetName(const FString& InName) { Name = InName; }
FORCEINLINE class UCard* GetCard() const { return Card; } # 이 부분도 TObjectPtr로 변경해야 하지만 강의에선 건너뛴 듯 하다.
FORCEINLINE void SetCard(class UCard* InCard) { Card = InCard; }
protected:
UPROPERTY()
FString Name;
UPROPERTY()
TObjectPtr<class UCard> Card; # 전방선언
};
위의 코드에서 Card 클래스에 대해 전방선언을 하였다. 따라서 #inlcude 또한 되어 있지 않은 것을 확인 할 수 있다.
Person.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "Person.h"
#include "Card.h"
UPerson::UPerson()
{
Name = TEXT("홍길동");
Card = CreateDefaultSubobject<UCard>(TEXT("NAME_Card"));
}
언리얼 엔진 5 마이그레이션 가이드
언리얼 엔진 4 프로젝트로 언리얼 엔진 5로 이주하는 방법 및 요구 사항.
docs.unrealengine.com
언리얼4 -> 언리얼5 마이그레이션 가이드를 참고하면 언리얼엔진 5부터는 TObjectPtr 을 사용해야 한다.
[Unreal] Unreal Engine 5 TObjectPtr
개요 언리얼 4에서 하던 프로젝트가 왜인지는 모르겠으나 맛이 가버려서 이참에 언리얼 5를 공부해보자는 생각으로 받아서 해보는 도중, 기본 캐릭터에게서 TObjectPtr이라는 단어를 보게 되었습
husk321.tistory.com
관련 글을 찾아보니 왜 사용해야 하는지보다는 사용해야 한다에 초첨을 맞추고 있는 듯 하다.
현재는 선언부에서 TObjectPtr 을 사용하고 구현부에서는 원시 포인터(*) 를 사용해도 된다고 생각하자.
더 자세한 이유
뭐 이제 기반이 되는 모든 함수, 클래스 등들이 TObjectPtr로 대체되기 때문에 이를 따르는 건 사실 당연한 이야길 수 있습니다. 그러니 이를 따르는 것도 중요한 숙제가 되겠죠.
그 외 "엑세스 트래킹"이라고 하면 개체가 사용되는 시점을 감지하는 것으로 입력 스트림의 어떤 위치에 어떤 객체를 넣을지 결정하는 데 사용할 수 있으며 액세스 순서대로 객체를 넣을 수 있다고 합니다. 그리고 에디터, 디버그 모드에서 검사를 추가해 잘못된 포인터를 할당하려고 할 때 충돌이 발생하지 않고 발생한 위치를 볼 수 있다고 합니다.
이건 언리얼 포럼에서 엔진 기여자의 이야기인데 아무튼 기존의 원시포인터보다 좋다고 하니... 일단 이를 따라서 앞으로의 선언에서 원시 포인터를 버려야 할 이유는 충분할 것 같습니다.
출처: https://husk321.tistory.com/377 [껍데기방:티스토리]
GameInstance
void UMyGameInstance::Init()
{
Super::Init();
UE_LOG(LogTemp, Log, TEXT("============================"));
TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };
for (const auto Person : Persons)
{
const UCard* OwnCard =Person->GetCard();
check(OwnCard); # if문 대체
ECardType CardType = OwnCard->GetCardType();
//UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %d"), *Person->GetName(), CardType);
const UEnum* CardEnumType = FindObject<UEnum>(nullptr, TEXT("/Script/UnrealComposition.ECardType")); # 절대 주소 /Script/프로젝트이름.열거형이름
if (CardEnumType)
{
# 선언해놓았던 메타데이터 정보를 빼내 로그 출력
FString CardMetaData = CardEnumType->GetDisplayNameTextByValue((int64)CardType).ToString();
UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %s"), *Person->GetName(), *CardMetaData);
}
}
UE_LOG(LogTemp, Log, TEXT("============================"));
}
'언리얼엔진5 > [Part1] 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
[이득우의 언리얼 프로그래밍 Part1 필기] 10 - 11. TArray, TSet, 구조체, TMap (0) | 2024.02.20 |
---|---|
[이득우의 언리얼 프로그래밍 Part1 필기] 9. 설계 : 델리게이트 (0) | 2024.02.19 |
[이득우의 언리얼 프로그래밍 Part1 필기] 5-6. 언리얼 엔진 리플렉션 시스템 (0) | 2024.02.13 |
[이득우의 언리얼 프로그래밍 Part1 필기] 4. 언리얼 오브젝트 기초 (0) | 2024.02.08 |
[이득우의 언리얼 프로그래밍 Part1 필기] 3.언리얼C++ 기본타입과 문자열 (0) | 2024.02.08 |