언리얼 Interface 이용 방법
Unreal engine에서도 interface를 사용할 수 있다.
먼저 기본적인 C++ 인터페이스에 대해 이해하기 위해 다음 글을 참고하면 좋다.
https://algorfati.tistory.com/205
Interface를 TSharedPtr의 형태로 이용하는 예제를 만들어보자.
class IInterfaceA
{
public:
virtual ~IInterfaceA() {}
virtual void FunctionA() = 0;
};
class IInterfaceB
{
public:
virtual ~IInterfaceB() {}
virtual void FunctionB() = 0;
};
class FDerived : public IInterfaceA, public IInterfaceB
{
public:
FDerived()
{
UE_LOG(LogTemp, Warning, TEXT("FDerived Created."));
}
~FDerived()
{
UE_LOG(LogTemp, Warning, TEXT("FDerived Destroyed."));
}
virtual void FunctionA() override
{
UE_LOG(LogTemp, Warning, TEXT("FunctionA"));
}
virtual void FunctionB() override
{
UE_LOG(LogTemp, Warning, TEXT("FunctionB"));
}
};
TSharedPtr의 레퍼런스 카운팅 기능이 인터페이스를 통해서도 잘 동작하는지 확인해보자.
void AConvAIGameMode::Example()
{
TSharedPtr<IInterfaceB> InterfaceB;
{
TSharedPtr<IInterfaceA> InterfaceA;
{
TSharedPtr<FDerived> Derived = MakeShareable(new FDerived());
InterfaceA = Derived;
InterfaceA->FunctionA();
UE_LOG(LogTemp, Warning, TEXT("first block exited."));
}
UE_LOG(LogTemp, Warning, TEXT("second block exited."));
}
UE_LOG(LogTemp, Warning, TEXT("all block exited."));
}
Derived는 InterfaceA를 통해 공유되었고 첫번째 블럭을 나갈때 파괴되지 않게 되었다. 레퍼런스 카운팅 기능이 잘 동작된다고 볼 수 있다.
LogTemp: Warning: FDerived Created.
LogTemp: Warning: FunctionA
LogTemp: Warning: first block exited.
LogTemp: Warning: second block exited.
LogTemp: Warning: FDerived Destroyed.
LogTemp: Warning: all block exited.
다음 코드는 두번째 블럭을 나갈때도 Derived가 파괴되지 않아야 한다.
void AConvAIGameMode::Example()
{
TSharedPtr<IInterfaceB> InterfaceB;
{
TSharedPtr<IInterfaceA> InterfaceA;
{
TSharedPtr<FDerived> Derived = MakeShareable(new FDerived());
InterfaceA = Derived;
InterfaceA->FunctionA();
UE_LOG(LogTemp, Warning, TEXT("first block exited."));
}
// explicit downcast
InterfaceB = StaticCastSharedPtr<FDerived>(InterfaceA);
InterfaceB->FunctionB();
UE_LOG(LogTemp, Warning, TEXT("second block exited."));
}
UE_LOG(LogTemp, Warning, TEXT("all block exited."));
}
출력결과를 보면 잘 동작함을 확인할 수 있다.
LogTemp: Warning: FDerived Created.
LogTemp: Warning: FunctionA
LogTemp: Warning: first block exited.
LogTemp: Warning: FunctionB
LogTemp: Warning: second block exited.
LogTemp: Warning: all block exited.
LogTemp: Warning: FDerived Destroyed.
인터페이스를 이용할 때 implicit downcast를 이용하여 여러 부모 클래스의 파생 클래스들 중 특정 인터페이스를 구현한 파생 클래스들에 대해서만 특정 동작을 처리하도록 구현하는 패턴을 자주 사용한다. 다만, 언리얼에서는 TSharePtr을 이용한 implicit downcast(dynamic cast)를 지원하지 않기 때문에, 이와 같은 방법을 이용할 수 없다.
(UObject를 이용한 downcast는 Cast 라는 함수의 형태로 지원하고 있기 때문에, 위와 같은 패턴을 UObject로 구현하는것은 가능하다.)
언리얼의 Native C++을 이용하는 경우에는 C++ Interface 이용방법과 비슷하게 사용하면 되겠지만, UObject를 상속받은 객체에 인터페이스를 추가하는 경우에는 사용방법이 조금 다르다. 이런 경우 일반적인 C++ 인터페이스처럼 IMyInterface 클래스 외에도 UMyInterface 라는 추가적인 UInterface 객체를 정의해주어야한다. 그리고 이 UInterface 객체는 UINTERFACE(MinimalAPI) 메크로를 지정해주어야 한다. (MinimalAPI 태그는 컴파일 최적화를 위해 사용된다고 한다.)
가장 기본적인 예제는 다음과 같다.
UINTERFACE(MinimalAPI)
class UGenerateAssetInterface : public UInterface
{
GENERATED_BODY()
};
class IGenerateAssetInterface
{
GENERATED_BODY()
public:
virtual bool ShouldGenerateAsset() const = 0;
virtual FString GetAssetName() const = 0;
};
Blueprint에서 이용가능한 함수를 위한 인터페이스 메서드도 추가할 수 있다.
C++로 기본 구현을 제공하면서도 blueprint에서 사용 및 override가 가능하도록 하기 위해 BlueprintNativeEvent를 이용하도록 한다. 기본 메서드와 구현 메서드로 구분해서 정의하고, 구현 메서드의 경우 interface를 상속받은 객체에서 메서드 몸체 내부를 채워주면 된다.
일반적으로 blueprint를 위한 함수의 경우 K2(Kismet 2.0) 접두어를 사용하고, property meta를 통해 blueprint 내에서 보이는 이름을 일반 코드의 함수 이름과 동일하게 하도록 제공한다.
Header
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "Hittable.generated.h"
// This class does not need to be modified.
UINTERFACE(BlueprintType, Blueprintable, MinimalAPI)
class UHittable : public UInterface
{
GENERATED_BODY()
};
class TERRITORYFIGHT_API IHittable
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
// blueprint를 위한 메서드
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, meta = (DisplayName = "OnHit", ScriptName = "OnHit"))
void K2_OnHit(float InDamage, int InHitIdx);
virtual void K2_OnHit_Implementation(float InDamage, int InHitIdx) = 0;
// c++을 위한 메서드
virtual void OnHit(float InDamage, int InHitIdx) = 0;
// 구현이 포함된 함수
virtual bool TestFunction();
};
Source
#include "Hittable.h"
bool IHittable::TestFunction()
{
// 구체적인 구현
}
Object
UCLASS(BlueprintType)
class TERRITORYFIGHT_API AMyCharacter : public ACharacter, public IHittable
{
//...
// interface
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, meta = (DisplayName = "OnHit", ScriptName = "OnHit"), Category = "Hittable")
void K2_OnHit(float InDamage, int InHitIdx);
virtual void K2_OnHit_Implementation(float InDamage, int InHitIdx) override { OnHit(InDamage, InHitIdx); }
void OnHit(float InDamage, int InHitIdx) override;
//...
}
블루프린트에서 구체적인 구현을 하는것도 가능하다.
물론 블루프린트에서 구현을 한 경우에는 c++ 구현 내용은 블루프린트의 구현 내용으로 override된다.
반대로 c++에서 blueprint로 구현된 interface 함수를 사용하고 싶을 수 있다.
중요한 부분은 UTestInterface의 UINTERFACE 안에 BlueprintType을 넣어줘야한다는 것이다.
Header
// TestInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "TestInterface.generated.h"
UINTERFACE(MinimalAPI, BlueprintType)
class UTestInterface : public UInterface
{
GENERATED_BODY()
};
class ITestInterface
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, Category = "Test Interface")
void TestInterfaceFunction();
};
Source
void ATestGameMode::TestInterfaceOnActor(AActor* Actor)
{
// iface is null if actor was a blueprint that extended the interface
if (ITestInterface* iface = Cast<ITestInterface>(Actor))
{
iface->TestInterfaceFunction();
}
// works for both C++ and blueprint classes that extend the interface
if(Actor->GetClass()->ImplementsInterface(UInteractiveActor::StaticClass()))
{
ITestInterface::Execute_TestInterfaceFunction(Actor);
}
}
Interface 상속
언리얼 인터페이스가 언리얼 인터페이스를 상속하는 구조로 설계도 가능하다.
IGameData
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "GameData.generated.h"
UINTERFACE(MinimalAPI)
class UGameData : public UInterface
{
GENERATED_BODY()
};
class TERRITORYFIGHT_API IGameData
{
GENERATED_BODY()
public:
virtual int GetTeamId() const = 0;
virtual void SetTeamId(int InTeamId) = 0;
};
IHitData
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "GameData.h"
#include "Hittable.generated.h"
UINTERFACE(BlueprintType, Blueprintable, MinimalAPI)
class UHittable : public UGameData
{
GENERATED_BODY()
};
class TERRITORYFIGHT_API IHittable : public IGameData
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, meta = (DisplayName = "OnHit", ScriptName = "OnHit"))
void K2_OnHit(float InDamage, int InHitIdx);
virtual void K2_OnHit_Implementation(float InDamage, int InHitIdx) = 0;
virtual void OnHit(float InDamage, int InHitIdx) = 0;
virtual void SetDead() = 0;
virtual bool IsDead() const = 0;
};
'게임 엔진 > Unreal' 카테고리의 다른 글
[Unreal] 인공지능 에이전트 개발 방법 (AIController, BehaviorTree, Blackboard, Decorator, Service, Task) (0) | 2020.10.22 |
---|---|
[Unreal] 프로퍼티 관련 예제 (0) | 2020.10.08 |
[Unreal] [Example] Animation에서 Physics Velocity 계산 (0) | 2020.09.09 |
[Unreal] [Example] Transform (0) | 2020.09.07 |
[Unreal] [Editor] Slate를 이용한 커스텀 에디터 List View 제작 (0) | 2020.06.29 |