What is "Interface"?
인터페이스(interface)란 특정 기능을 구현할 것을 약속한 추상 형식을 말합니다. Java나 C#등 다른 객체지향 언어에서는 인터페이스 형식을 제공하지만 C++언어에서는 제공하지 않습니다. 하지만 순수 가상 메서드를 이용하여 정의할 수 있습니다. 인터페이스는 멤버필드나 구체적으로 구현한 메서드를 갖지 않고 특정 기능을 약속한 메서드만 갖습니다. 그리고 모든 멤버는 사용하는 개발자와의 약속으로 public 접근 지정합니다. C++에서는 구조체는 디폴트 가시성이 public이어서 구조체를 이용하여 인터페이스를 정의하는 이들도 많습니다.
인터페이스 명명규칙
개발자들 사이에 인터페이스 이름은 I로 시작하고 뒤에 약속하는 기능을 붙입니다. 물론 여러 개의 기능을 약속하는 인터페이스도 있겠지만 이때도 대표하는 기능을 이름에 포함시키는 것이 좋습니다.
인터페이스를 기반으로 파생한 형식은 약속한 기능을 재정의하여 구체적으로 구현하여야 개체를 생성할 수 있습니다.
개념
#include <iostream>
class Gun {
public:
void Fire() {
std::cout << "Fire the Gun" << std::endl;
}
};
class Soldier {
public:
void FireGun(Gun* gun) { gun->Fire(); }
};
int main() {
Soldier soldier1;
Gun gun1;
soldier1.FireGun(&gun1);
}
인터페이스의 개념을 보다 쉽게 이해하기 위해 예를 들어보겠습니다. 군인과 총 클래스를 각각 만든 상황에서 새로운 총인 MachinGun을 만들어야 한다고 가정하면, 기존에 군인 클래스에 존재하던 FireGun 함수는 Gun객체만을 인자로 받기 때문에 문제가 생깁니다. 문제를 아주 간단히 해결하려면 FireGun함수를 오버로드하여 동일이름에 인자만 MachinGun을 받는 함수를 추가 제작해 주면 됩니다. 하지만 코드를 좀 더 구조적으로 짜려면 다음 원칙들을 유의하여 코딩하는 것이 좋습니다.
1. 개방 폐쇄의 법칙 (OCP : Open Close Principle)
기능 확장(모듈, 클래스, 함수 추가)에 열려 있고, 수정(기존 코드 수정)에는 닫혀 있어야 한다는 원칙이다.
2. 강한 결합 (tightly coupling)
객체와 다른 객체의 관계가 서로 강하게 연결되어 있는 것을 뜻한다. 이러한 코드는 교체 불가능하고 확장성이 떨어진다.
이제 이를 유의하며 코드를 수정해보겠습니다.
#include <iostream>
class IGun //총 인터페이스
{
public:
virtual void Fire() = 0;
};
class NormalGun : public IGun //보통의 총
{
public:
void take(){ std::cout<<"Fire NormalGun!!!"<<std::endl; }
}
class MachineGun : public IGun //머신 건
{
public:
void take(){ std::cout<<"Fire MachineGun!!!"<<std::endl; }
}
class Soldier //군인 클래스
{
public:
void FireGun(IGun* gun) { gun->Fire(); }
};
int main() //메인함수
{
Soldier s1;
NormalGun g1;
MachinGun g2;
s1.FireGun(&g1);
s1.FireGun(&g2);
}
이제는 어떠한 Gun 클래스를 추가하더라도 기존의 코드를 수정할 필요가 없습니다.
언리얼의 인터페이스
인터페이스 클래스는 (잠재적으로) 무관한 클래스 세트가 공통의 함수 세트를 구현할 수 있도록 하는 데 쓰입니다. 그대로라면 유사성이 없었을 크고 복잡한 클래스들에 어떤 게임 함수 기능을 공유시키고자 하는 경우 인터페이스를 사용하면 좋습니다.
예를 들어 트리거 볼륨에 들어서면 함정이 발동되거나, 적에게 경보가 울리거나, 플레이어에게 점수를 주는 시스템을 가진 게임이 있다 칩시다. 함정, 적, 점수에서 ReactToTrigger 함수를 구현해 각각 위의 동작을 구현하면 될 것입니다. 하지만 함정은 AActor에서 파생될 수도, 적은 특수 APawn이나 ACharacter의 서브클래스일 수도, 점수는 UDateAsset일 수도 있습니다. 이 모든 클래스에 공유 함수 기능이 필요하지만, UObejct 말고는 공통 조상이 없습니다. 이럴 때 인터페이스를 쓰는 것을 추천합니다.
인터페이스 선언
인터페이스 클래스의 선언은 일반 언리얼 클래스 선언과 비슷하나, 크게 두가지 차이점이 있습니다.
1. 인터페이스 클래스는 UCLASS매크로 대신 UINTERFACE 매크로를 사용하며 UObject를 직접 상속하는 대신 UInterface를 상속합니다.
UINTERFACE([specifier, specifier, ...], [meta(key=value, key=value, ...)])
class UClassName : public UInterface
{
GENERATED_BODY()
};
2. UINTERFACE클래스는 실제 인터페이스가 아닙니다. 언리얼 엔진의 리플렉션 시스템에 보이도록 하기 위해서만 존재하는 비어 있는 클래스입니다. 다른 클래스에서 상속하게 되는 실제 인터페이스는 같은 클래스 이름에 첫 글자만 U에서 I로 바뀝니다.
//ReactToTriggerInterface.h
#pragma once
#include "ReactToTriggerInterface.generated.h"
UINTERFACE(Blueprintable)
class UReactToTriggerInterface : public UInterface
{
GENERATED_BODY()
};
class IReactToTriggerInterface
{
GENERATED_BODY()
public:
/** 이 오브젝트를 활성화시키는 트리거 볼륨에 반응합니다. 반응에 성공하면 true 를 반환합니다. */
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category="Trigger Reaction")
bool ReactToTrigger() const;
};
함수를 헤더파일에 직접 작성할 수 있는데, 기본적으로 아무 것도 하지 않는 함수이거나 false, 0, 비어 있는 스트링 반환처럼 사소한 동작을 하는 함수의 경우 종종 그렇게 합니다. 보다 복잡한 함수는 소스파일에 작성하여 컴파일 시간을 줄일 수 있습니다. 순수 가상함수(pure virtual)가 지원됩니다. 여기 샘플 함수는 헤더파일에 작성해도 될 만큼 간단하긴 하지만 소스파일에 작성하도록 하겠습니다. 소스 파일에 이제 다음과 같은 부분이 있을 것입니다.
#include "ReactToTriggerInterface.h"
bool IReactToTriggerInterface::ReactToTrigger() const
{
return false;
}
접두사가 "U"인 클래스는 생성자나 기타 다른 함수가 필요치 않은 반면, "I"인 클래스는 모든 인터페이스 함수를 포함하며, 다른 클래스에 실제로 상속되는 클래스이기도 합니다.
인터페이스 지정자
- BlueprintType : 이 클래스를 블루프린트에서 변수로 사용할 수 있는 유형으로 노출합니다.
- DependsOn = (ClassName1, ClassName2, ....) : 나열된 모든 클래스는 이 클래스에 앞서 컴파일됩니다. ClassName은 같은(또는 기존) 패키지의 클래스를 지정해야 합니다. 쉼표로 구분되는 단일 DependsOn, 또는 각 클래스마다 별도의 DependsOn을 사용하여 다중 종속 클래스를 지정할 수 있습니다. 컴파일러는 이미 컴파일된 클래스에 있는 것만 알기때문에, 다른 클래스에 선언된 구조체 또는 열거형을 사용할 때는 컴파일 순서 또한 중요하게 작용합니다.
- MinimalAPI : 클래스의 형 정보만 다른 모듈에서 사용할 수 있도록 익스포트하게 만듭니다. 클래스는 형변환 가능하지만, 그 클래스의 함수는 (인라인 함수를 제외하고) 호출할 수 없습니다. 그러면 다른 모듈에서 접근할 수 없는 함수가 모두 필요치 않은 클래스에 대해 모든 것을 익스포트하지 않아 컴파일 시간이 빨라집니다.
C++로 인터페이스 구현하기
새 클래스에서 인터페이스를 사용하려면, 사용중인 UObject기반 클래스에 추가로 접두사가 "I"인 인터페이스 클래스를 상속하면 됩니다.
class ATrap : public AActor, public IReactToTriggerInterface
{
GENERATED_BODY()
public:
virtual bool ReactToTrigger() const override;
};
주어진 클래스의 인터페이스 구현 여부 확인
인터페이스를 구현하는 두 C++클래스와 블루프린트 클래스 사이의 호환을 위해서는 다음 함수 중 하나를 사용하면 됩니다.
bool bIsImplemented = OriginalObject->GetClass()->ImplementsInterface(UReactToTriggerInterface::StaticClass()); // OriginalObject 가 UReactToTriggerInterface 를 구현한다면 bIsImplemented 는 true 가 됩니다.
IReactToTriggerInterface* ReactingObject = Cast<IReactToTriggerInterface>(OriginalObject); // OriginalObject 가 UReactToTriggerInterface 를 구현한다면 ReactingObject 는 null 이외의 값이 됩니다.
U 접두사 클래스에 Cast를 사용하려고 하면 실패하는 반면, StaticClass함수는 I접두사 클래스에 구현되어 있지 않아 컴파일 되지 않습니다.
다른 언리얼 유형으로의 형 변환
언리얼 엔진의 형변환 시스템은 한 인터페이스에서 다른 인터페이스로, 또는 인터페이스에서 적합한 경우 언리얼 유형으로 형 변환을 지원합니다.
IReactToTriggerInterface* ReactingObject = Cast<IReactToTriggerInterface>(OriginalObject); // 인터페이스가 구현된 경우 ReactingObject 는 null 이외의 값이 됩니다.
ISomeOtherInterface* DifferentInterface = Cast<ISomeOtherInterface>(ReactingObject); // ReactingObject 가 null 이외의 값이고 ISomeOtherInterface 를 구현하는 경우 DifferentInterface 는 null 이외의 값이 됩니다.
AActor* Actor = Cast<AActor>(ReactingObject); // ReactingObject 가 null 이외의 값이고 OriginalObject 는 AActor 또는 AActor 파생 클래스인 경우, Actor 는 null 이외의 값이 됩니다.
블루프린트 구현가능 클래스
블루프린트가 이 인터페이스를 구현할 수 있도록 하려면, Blueprintable 메타데이터 지정자를 사용해야 합니다. 블루프린트 클래스가 덮어쓰려는 모든 인터페이스 함수는 BlueprintNativeEvent 또는 BlueprintImplementableEvent여야 합니다. BlueprintCallable 마킹된 함수는 여전히 호출은 가능할 것이나, 덮어쓰기는 불가능합니다. 다른 모든 함수는 블루프린트에서 접근할 수 없을 것입니다.
참고: https://dhshin94.tistory.com/64
'UE4' 카테고리의 다른 글
[UE4] MSB 3073 오류 해결법 (0) | 2023.03.29 |
---|---|
[UE4] 아키타입(Archetype) (0) | 2023.03.02 |
[UE4] 스마트 포인터 (Smart Pointer) (0) | 2023.02.24 |
[UE4] 언리얼 ui 최적화 기법 (4) | 2023.02.21 |
[UE4] Projectile Movement Component (0) | 2023.02.14 |