UE5

UFUNCTION() 응용을 위한 가이드

게임 개발 2023. 10. 16. 23:45

블루프린트는 프로그래머와 디자이너의 협업이 가능하도록 해줄 때 편리하다.

unreal은 디자이너도, 프로그래머도 둘 다 사용이 가능하다는 점에서 큰 이점이 된다.

 

이에 대한 예시로 디자이너가 새로운 유형의 무기를 구현할 때,

프로그래머가 무기에 대한 클래스를 만들어 놓고

Fire() 함수 등 조작할 수 있는 중요한 몇가지의 함수를 블루프린트로 만들어 가시화 해놓으면,

디자이너가 코드를 읽어야 할 필요는 없다.

프로그래머도 디자이너에게 코드를 맡길 필요가 없다.

총 발사 속도를 다시 코딩하고 게임을 컴파일 시킬 필요도 없다.

디자이너는 블루프린트를 이용해 직접 발사속도 및 스태틱 매쉬만 변경해주면 된다.

이는 디자이너 프로그래머 모두에게 시간 절약이 될 수 있다.

 

근데 프로그래머에게도 BP를 사용할지 C++을 사용하여 코딩할지는 매우 중요하다.

단순히 모든 기능을 C++로 구현할 수 있지만,

대부분의 프로젝트에서는 C++과 BP를 적절히 섞여있다.

UI같은 경우는 BP로 구현하는 것이 훨씬 간편할 것이며,

자주 연산되어야 하는 기능은 C++로 구현되어야 속도면에서 빠른 반응 속도를 보일 것이다.

 

처음 Unreal을 접하게 되면 이에 대한 기준이 모호하다.

이에 대한 명확한 기준을 Unreal 공식 홈페이지에서 제공하고 있다.

 

해당 게시물 역시 이러한 언리얼 공식 사이트를 참고하여 작성되었다.

https://docs.unrealengine.com/5.1/en-US/exposing-gameplay-elements-to-blueprints-visual-scripting-in-unreal-engine/https://docs.unrealengine.com/5.1/en-US/exposing-cplusplus-to-blueprints-visual-scripting-in-unreal-engine/

 

Exposing Gameplay Elements to Blueprints

Technical guide for gameplay programmers exposing gameplay elements to Blueprints.

docs.unrealengine.com

 

개발자가 

C++를 쓸 것이냐, 블루프린트를 사용할 것이냐의 결정하는 주요 요인은 두 가지로 판단하면 된다.

 

1. 속도

2. 표현식의 복잡도

 

이 두 가지 요소 이외에도 많은 부분은 게임의 복잡도와 팀의 구성에 따라 달린 문제이긴 하지만,

프로그래머 보다 아티스트가 더 많고 이에 대한 기능을 중점으로 생각한다면,

BP로 구현할 것이 많을 것이며,

프로그래머가 더 많다면 C++로 구현될 것이 더 많을 수 있다.

또한 C++로 구현 된 것을 BP로 만질 수 있도록 구현한다거나,

BP의 기능을 C++에서 그대로 가져올 수 있도록 해야할 수도 있다.

 

 

이에 대한 판단 기준의 첫 번째는 속도이다.

 

속도

 

속도의 경우, 블루프린트 실행은 C++ 실행보다 느리다.

퍼포먼스가 나쁘다는 뜻은 아니고, 계산이 많이 필요한 작업을 수행하거나,

매우 자주 실행되는 연산인 경우, 블루프린트 보다는 C++를 사용하는 편이 낫다는 의미이다.

하지만 프로젝트의 퍼포먼스나 팀에 최적인 방식으로 둘을 조합하는 것이 가능하다.

함수성이 많은 블루프린트가 있는 경우,

그 중의 일부를 C++로 돌려서 속도를 올리고

나머지는 BP로 남겨 유연성을 유지할 수 있다.

프로파일링에 어느 한 연산이 BP로 시간이 많이 걸리는 경우,

그 부분만을 C++로 옮기고 나머지는 BP 남겨둘 수 있다.

 

블루프린트 비주얼 스크립트로 작업을 했을 때 시간이 많이 걸리는 시스템의 예로는,

수천 단위의 액터를 제어하는 크라우드 시스템을 들 수 있다.

이 경우 퍼포먼스를 위해 의사 결정, 길찾기 등의 기타 함수성은 C++ 로 처리하고,

나머지 트윅 파라미터나 제어 함수는 블루프린트로 노출시킬 수도 있다.

 

 

복잡도

 

표현식 복잡도의 경우, BP보다 C++로 하는 편이 수월할 수 있다.

BP가 할 수있는 일은 많이 있지만, 노드로 표현하기는 힘든 경우가 있다.

대규모 데이터 집합에 대한 연산, 문자열에 대한 조작, 대규모 데이터 세트에 대한 복잡한 연산 등

모두 간단한 노드로 표현하기에는 복잡하다.

비주얼 시스템으로 처리하기에 쉽지 않은 작업이 될 수 있다.

그런 작업은 BP보다는 C++로 작성하는 편이 더욱 수월하다.

이러한 것들은 BP보다는 C++로 구현 되기 쉽도록 언리얼에서 제공하는 여러 기능을 이용하면 된다.

C++로 구현했다면 코드를 보고 파악하면 된다.

 

 

이러한 시간과 복잡도를 고려하는 개발을 구현하기 위해서는

BP와 C++에 대한 프로퍼티 기능과 BP API 를 만들 수 있어야 한다.

 

해당 포스팅의 순서는 첫 번째 프로퍼티 두 번째 BP API로 진행하겠다.

 

 

C++ 파일에서 생성된 어느 한 클래스를 확장하여

블루프린트로 만들기 위해서는, 해당 클래스에 Blueprintable 키워드를 정의해야 한다.

그러기 위해서는 크랠스 정의부에 선헌하는 UCLASS() 매크로 안에 해당 키워드를 추가해 줘야 한다.

이 키워드는 해당 클래스를 블루프린트 시스템에 인식시켜

새 블루프린트 대화창의 크랠스 목록에 표시되도록하며 블프 생성시 부모로 선택할 수 있도록 만드는 방법이다.

 

 

++ plus

 

프로그래밍에서 일반적인 프로퍼티란? 

일부 객체 지향 프로그래밍 언어에서 필드(데이터 멤버)와 메소드 간 기능의 중간인

클래스 멤버의 특수한 유형이다.

 

언리얼 프로퍼티란?

언리얼 엔진에서는 엔진과 상호작용하려는 코드를 작성하기 위해

언리얼에서 제공하는 각종 매크로의 도움을 받을 수 있다.

그중 멤버 변수 앞, 클래스 앞, 함수 앞에 해당 매크로를 응용하여

프로퍼티 지정자를 나열함으로써,

해당 프로퍼티가 리플렉션 데이터를 생성하여

엔진과 에디터의 다양한 부분과 상호작용 할 수 있도록 돕는다.

 

리플렉션이란?

프로그램 실행 시간에 자기 자신을 조사하는 기능으로

자기 자신은 클래스, 구조체, 함수, 멤버 변수, 열거형 등을 의미한다.

 

자기 자신의 관련된 정보를 수집, 조작하는 별도의 시스템으로

런타임에 클래스의 상속관계, 멤버 변수, 멤버 함수 정보를 확인하고

더 나아가 멤버 변수 접근, 멤버 함수 실행 등의 기능 구현이 가능하다.

 

기존 C++의 RTTI보다 좀 더 다양한 기능을 제공한다.

기존의 C++ 문법은 런타임에서 객체 정보 확인이 불가능하였으나,

RTTI는 단순하게 클래스 타입만 확인할 수 있었다.

언리얼에서는 리플렉션 패턴을 구현하여 프로그래머가 사용할 수 있도록 제공하고 있다.

 

 

++ 

 

 

Blueprintable 클래스에 대한 가장 단순한 형태의 선언은 다음과 같다.

 

UCLASS(Blueprintable)
class AMyBlueprintableClass : AActor
{
    GENERATED_BODY()
}

 

 

C++ 에서 만들 클래스를 BP클래스로 만들기 위해서는

BP 클래스를 클릭하고

 

모든 클래스에서 해당 C++에서 생성된 클래스를 검색하여 불러오면 된다.

 

 

다시 C++에서 클래스를 관리하는 UCLASS() 프로퍼티에 대한 설명으로 돌아가보자.

 

UCLASS에는 3가지 선언이 있다.

 

키워드 설명
Blueprintable 블루프틴트 기능 -
이 클래스를 가지고 블루프린트를 만들 수 있는 클래스로 노출시킨다.
기본값은 별도로 상속하지 않는 한 NotBlueprint이다.
이 키워드는 서브클래스에 상속된다.
BlueprintType 블루프린트 유형 -
이 클래스는 블루프린트에서 변수로 사용 가능한 유형으로 노출시킨다.
NotBlueprintable 블루프린트 불가능 -
이 클래스를 가지고 블루프린트를 만들 수 없다고 지정한다.
부모 클래스에 Blueprintable 키워드가 지정된 것을 무효화 시킨다.

 

 

 

읽기가능 및 쓰기가능 프로퍼티

 

 

C++ 클래스에서 정의된 변수를 해당 클래스에서 확장된 블루프린트에 노출시키기 위해서는,

변수 정의부 앞에 UPROPERTY() 매크로 안에서 아래 나열된 키워드 중 하나를 사용해서

변수를 정의해 줘야 한다.

이 키워드는 블루프린트 시스템에 변수를 알려 내 블루프린트 패널에 표시되도록하여

그 값을 설정하고 접근할 수 있다.

 

//Character's Health
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Character")
float health;

 

키워드 설명
BlueprintReadOnly 블루프린트 읽기 전용 -
이 프로퍼티는 블루프린트에서 읽기만 가능하고 변경은 불가능하다.
BlueprintReadWrite 블루프린트 읽기 쓰기 -
이 프로퍼티는 블루프린트에서 읽고 쓸 수 있다.
멀티캐스트 델리게이트 키워드
키워드 설명
BlueprintAssignable 블루프린트 할당 기능 -
이 프로퍼티는 블루프린트에서 할당 가능하도록 노출된다.
BlueprintCallable 블루프린트 호출 가능 -
이 프로퍼티는 블루프린트 그래프에서 호출 가능하도록 노출된다.

 

 

실행가능 및 덮어쓰기 기능 함수

 

블루프린트에서 네이티브 함수를 호출하기 위해서,

함수 정의부 앞에 오는 UFUNCTION() 매크로 안에서

아래 나열된 키워드 중 하나를 사용하여 함수를 정의해 줘야 합니다.

 

이 키워드는 블루프린트 시스템에 함수를 인식시켜

컨텍스트 메뉴나 팔레트에 나타나도록 함으로써

그래프에 추가하여 실행되도록 할 수도 있고,

이벤트의 경우 덮어쓰거나 실행되도록 할 수도 있다.

 

BlueprintCallable 함수의 가장 단순한 형태의 선언은 다음과 같다.

 

<카테고리가 없는 상태>

<카테고리가 있는 상태>

//무기 발사
UFUNCTION(BlueprintCallable, Category="Weapon")
void Fire();

 

카테고리를 달게 되면 해당 카테고리가 BP 에디터에 생기게되고

해당 카테고리 안에서 함수를 꺼내올 수 있다.

 

이렇게 가저온 함수로 함수의 시그니처를 만들었을 때,

매개변수( =parameter) 를 레퍼런스 전달하도록 만들면

블루프린트에서 출력 핀이 된다.

 

매개변수의 레퍼런스를 전달하도록 하면서

동시에 입력으로 표시하려면 UPARAM() 매크로를 사용하면 된다.

ue5 공식 사이트 발췌

또한 UPARAM()을 사용하여 핀 표시면을 변경할 수도 있는데,

예를 들어 KismetMathLibrary의 MakeRotator 함수는 UPARAM()과

DisplayName 키워드를 사용하여 블루프린트

(예시 parameter임) Roll, Pitch, Yaw 파라미터가 나타나는 방식을 변경할 수 있다.

 

ue5 공식 사이트 발췌

/** Makes a rotator {Roll, Pitch, Yaw} from rotation values supplied in degrees */
UFUNCTION(BlueprintPure, Category="Math|Rotator", meta=(Keywords="construct build rotation rotate rotator makerotator", NativeMakeFunc))
static FRotator MakeRotator(
UPARAM(DisplayName="X (Roll)") float Roll,  
UPARAM(DisplayName="Y (Pitch)") float Pitch,
UPARAM(DisplayName="Z (Yaw)") float Yaw);

 

만약 ue에서 native c++을 물어본다면 Blueprint to Native를 뜻할 가능성이 있다.

native c++을 언리얼에서 써 보았느냐는,

블루프린트에서 native로 통신을 해 보았냐라는 뜻일 것이다.

주로 BlueprintCallable을 뜻할 것이다.

앞서 말한 바와 같이,

BlueprintCallable을 사용하면 디자이너와 개발자 모두 편하다.

블루프린트에서 네이티브로 통신 ( Blueprint to Native Communication) 
키워드 설명
BlueprintCallable 블루프린트 호출 가능 -
블루프린트에서 호출할 수 있는 네이티브 함수로,
호출중인 오브젝트에 관해서나 다른 글로벌한 상태를 바꾸는
네이티브 코드를 호출한다.
즉, "예정(= scheduled )" 등록되거나,
다른 노드와 비교한 실행 순서를 명시적으로 알려줘야 한다는 뜻이다. 
여기서는 하양 실행 선으로 결정한다.
모든 블루프린트 호출가능 함수는 하양 실행 선 상에 나타내는 순서대로 호출된다.
BlueprintPure 블루프린트 순수 -
블루프린트에서 호출할 수 있는 네이티브 함수로,
호출중인 오브젝트에 관해서나 다른 글로벌한 상태를 바꾸지 않는
네이티브 코드를 호출한다.
즉, 이 노드를 호출해서 무언가 바뀌는 것은 없으며,
그저 입력을 받고 출력을 알려주는 함수라는 뜻이다.

여기에는 수학 (+,-,*, 등) 노드, 변수, getter 등을 이용해서 바꿀 수 없다는 뜻이 내포되어 있다.
이 노드의 결과 데이터를 필요로 하는 BlueprintCallable 노드에 따라
컴파일러가 자동으로 알아서 해당 계산을 해준다.

네이티브에서 블루프린트로 통신 (Native to Blueprint Communication)
키워드 설명
BlueprintImplementableEvent 블루프린트 구현가능 이벤트(BIE) -
네이티브 함수가 블루프린트로 통신을 보낼 수 있도록 하는 주요한 방법이다.
블루프린트 자체 내에 구현하는 가상 함수같은 것이다.
다른 구현이 없으면, 함수 호출은 무시가 된다.
중요한 점 한가지는, 이 BIE 에 반환값이나
출력 매개변수(parameter)가 없는 경우,
블루프린트의 이벤트 그래프에 우클릭을하고
선택하여 사용할 수 있는 이벤트로 나타난다는 점이다.
반환값이나 출력 파라미터가 있는 경우, " My Blueprints" 탭에 나열되며,
우클릭 후 함수 "구현"을 선택하는 것으로 덮어쓸 수 있다.
참고로 bie에는 함수의 네이티브 구현이 없다
( C++에서 = 함수 바디 구현이 없다)
정리 첨언 C++바디 구현이 불가능하다 (모든 것이 블루프린트에서 발생하기 때문)
=> 해더파일에만 작성 되는 함수이다.
일반적으로 블루프린트에서 오버라이드가 된다.
블루프린트 이벤트 그래프에서 커스텀 이벤트에 가깝다.
이 지정자를 사용하는 C++ 함수에는 파생 클래스에서
함수 내용을 재정의 할수있도록 하는 Virtual을 붙일 수 없다.
붙이고싶다면 BlueprintNativeEvent 사용할 것

또한 해당 함수의 parameter를 넘겨야 한다면 const FString& 으로
상수 객체 참조에 의한 전달을 해줘야
오버로딩 관련 컴파일 에러가 발생하지 않으므로
주의해서 사용해야 한다.

즉, C++에서 함수 정의를 만들 필요는 없지만,
이 함수를 호출할 위치는 선택해야 제 기능을 해나간다.

사용 예 ) 
헤더



C++


헤더에서 선언만하고 CPP 파일에서 정의를 하지 않더라도
함수 호출이 가능하다. 
BlueprintNativeEvent 블루프린트 네이티브 이벤트 (BNE) -
위와 같지만, 블루프린트가 함수를 덮어쓰지 않는 경우
호출되는 함수의 기본 네이티브가 구현이 있다는 점이 다르다.
( = 함수의 바디가 있다는 점에서 BIE와 다르다는 말이다)
블루프린트 구현이 없는 경우 일정한 기본 동작이 있었으면 하면서도,
원하는 경우에는
블루프린트가 함수성을 덮어쓸 수 있었으면 하는 상황에서 사용하기 유용하다.
비용이 더 들기에,
함수성이 필요한 경우에만 넣는 것이 좋다.
BNE를 덮어쓸 때 필요한 경우, 이벤트나 함수 입구 노드에 우클릭한 다음
"Add call to parent"(부모로의 호출 추가)를 선택하여
네이티브 구현을 호출하는 것도 여전히 가능하다.
정리 첨언 BlueprintNativeEvent 함수는 BlueprintCallable 와 BlueprintImplementableEvent 함수를
조합한 것과 같은 것이다.

C++로 기본 작동방식이 프로그래밍 되어있지만,
블루프린트 그래프로 덮어써서 보조 또는 대체가 가능한 기능이다.
이에 대한 프로그래밍을 할 때 가장 유연한 옵션이다.

해당 기능을 사용하면 추상클래스로 상속하던 기능을
vitual도 빼주고 = 0; 도 빼줘도 상속 가능하다.

기술적으로는 더 이상 가상함수가 아니지만,
기능적으로 동일한 기능을 수행한다.
우리가 재정의 할 수 있는 C++ 함수이며,
기능적 가상함수로 다른 C++ 파일 및 BP에서도 구현 가능하다.

상속받은 함수는 " _Implementation " 구문을 추가하면 된다.

해당 함수를 블프에서 가져오면 이렇게 꽤나 귀여운 아이콘이 달린
BP 함수를 볼 수 있다.

이렇게 부모 함수도 불러 올 수 있다.



해당 함수의 부모도 역시 BlueprintNativeEvent 선언이 되었기에 가능한 일이다.





ex_ 디자이너 응용 예시)

 

 

이제 예제를 통해 BP API를 만들어보자.

 

예제에 들어가기 전에 짤막하게 3가지 정도를 설명하겠다.

C++ 또는 BP로 처리하면 최고인 함수성이 몇 가지가 있다.

C++ 프로그래머와 블루프린트 제작자가

게임 제작 도중 같이 작업하는 예제 몇 가지는 다음과 같다.

 

  • 프로그래머가 커스텀 이벤트를 정의하는 Character 클래스를 C++로 만든 다음,
    블루프린트를 사용해서 Character 클래스를 확장하고
    실제 메시 할당 및 기본값을 설정한다. ShooterGame 샘플 프로젝트에서
    플레이어 캐릭터와 적 봇을 통해 이와 같은 구현을 확인한다.
  • 능력 시스템의 기본 클래스는 C++로 구현하고 실제 내용은 디자이너가 블루프린트로 제작한다.
    StrategyGame 샘플에서 보면, 기본 터렛은 c++로 정의되어 있지만,
    화염방사기, 대포, 화살 터렛의 작동방식은 모두 블루프린트로 정의되어 있다.
  • 픽업의 경우 "Collect" 또는 "Respawn" 함수는 Blueprint Implementable Event로 하여,
    디자이너가 이를 덮어써서 다양한 파티클 이미터와 사운드 이펙트를 스폰시킬 수 있다.
    ShooterGame 및 StrategyGame 양쪽 다 픽업은 이런 식으로 만들었다.

 

블루프린트 API 만들기 : 팁과 정보 (Tips and Tricks)

 

블루프린트 노출 API 를 만드는 프로그래머가 몇 가지 고려할 사항은 이렇다.

 

1) 옵션 파라미터는 블루프린트로 잘 처리된다.

/**
 * Prints a string to the log, and optionally, to the screen
 * If Print To Log is true, it will be visible in the Output Log window.  Otherwise it will be logged only as 'Verbose', so it generally won't show up.
 *
 * @param   InString        The string to log out
 * @param   bPrintToScreen  Whether or not to print the output to the screen
 * @param   bPrintToLog     Whether or not to print the output to the log
 * @param   bPrintToConsole Whether or not to print the output to the console
 * @param   TextColor       Whether or not to print the output to the console
 */
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject", CallableWithoutWorldContext, Keywords = "log print", AdvancedDisplay = "2"), Category="Utilities|String")
static void PrintString(UObject* WorldContextObject, const FString& InString = FString(TEXT("Hello")), bool bPrintToScreen = true, bool bPrintToLog = true, FLinearColor TextColor = FLinearColor(0.0,0.66,1.0));

 

 

2) 구조체를 반환하는 함수보다는, 반환 파라미터가 많은 함수가 낫다.
출력 핀이 다수인 노드를 만드는 법을 보여주는 예시이다.

 

UFUNCTION(BlueprintCallable, Category = "Example Nodes")
static void MultipleOutputs(int32& OutputInteger, FVector& OutputVector);

 

 

3) 기존 함수에 파라미터를 새로 추가하는 것은 괜찮지만,
변경 또는 제거할 필요가 있는 경우 원래 것을 폐기 (deprecate) 시키고 새 함수를 추가해야 한다.
deprecation 메타데이터를 꼭 사용해 줘야 새 함수 관련 정보가 블루프린트에 나타난다.

UFUNCTION(BlueprintCallable, Category="Collision", meta=(DeprecatedFunction, DeprecationMessage = "Use new CapsuleOverlapActors", WorldContext="WorldContextObject", AutoCreateRefTerm="ActorsToIgnore"))
static ENGINE_API bool CapsuleOverlapActors_DEPRECATED(UObject* WorldContextObject, const FVector CapsulePos, float Radius, float HalfHeight, EOverlapFilterOption Filter, UClass* ActorClassFilter, const TArray<AActor*>& ActorsToIgnore, TArray<class AActor*>& OutActors);

 

 

4) 함수가 enum 을 받아야 하는 경우 , 'expand enum as execs' 메타데이터 사용을 고려한다면

노드 사용이 시워질 수 있다.

UFUNCTION(BlueprintCallable, Category = "DataTable", meta = (ExpandEnumAsExecs="OutResult", DataTablePin="CurveTable"))
static void EvaluateCurveTableRow(UCurveTable* CurveTable, FName RowName, float InXY, TEnumAsByte<EEvaluateCurveTableResult::Type>& OutResult, float& OutXY);

 

 

5) 완료까지 시간이 걸리는 작업(e.g move here)은 잠재된(latent) 함수여야 한다.

= Many operations that take time to complete (e.g. move here) should be latent functions.

/** 
 * Perform a latent action with a delay.
 * 
 * @param WorldContext  World context.
 * @param Duration      length of delay.
 * @param LatentInfo    The latent action.
 */
UFUNCTION(BlueprintCallable, Category="Utilities|FlowControl", meta=(Latent, WorldContext="WorldContextObject", LatentInfo="LatentInfo", Duration="0.2"))
static void Delay(UObject* WorldContextObject, float Duration, struct FLatentActionInfo LatentInfo );

 

 

6) 가급적 함수는 shared library에 넣도록 해야한다.

이렇게 하면 함수들을 여러 클래스에 걸쳐 사용하기 쉬우며, (= across multiple classes)

'target' 핀을 피할 수 있다. 

class DOCUMENTATIONCODE_API UTestBlueprintFunctionLibrary : public UBlueprintFunctionLibrary

 

 

7) 가급적 pure = 원래 기능(ex. 0 and max -1 주석과 같은)을 써놓자,

노드에서 사용 할 때 불필요하게 노드간 연결을 요하는 실행 핀 연결을 피해갈 수 있다.

/* Returns a uniformly distributed random number between 0 and Max - 1 */
UFUNCTION(BlueprintPure, Category="Math|Random")
static int32 RandomInteger(int32 Max);

 

 

8) 함수에 const를 써놓는 것 만으로도 블루프린트 노드에서 함수에 필요하지 않는

실행 핀이 생기지 않도록 한다.

/**
 * Get the actor-to-world transform.
 * @return The transform that transforms from actor space to world space.
 */
UFUNCTION(BlueprintCallable, meta=(DisplayName = "GetActorTransform"), Category="Utilities|Transformation")
FTransform GetTransform() const;