Unreal Engine

Delegate(언리얼 엔진)

Keisa 2023. 10. 4. 11:46

C++ 에서는 델리게이트 시스템이 없지만, 언리얼은 자체적으로 델리게이트를 제공하고 있다. 델리게이트를 이용하는 것은 기존에 함수 포인터 등을 이용했던 것보다 간편하며 안전하다. 

대리자를 통해 함수를 호출하므로 호출할 함수나 이를 포함하는 객체가 없어져도 대리자가 체크해 안전하게 함수를 호출할 수 있으며, 동일한 형을 가진 함수 여러 개를 대리자가 묶어서 관리하고 필요할 때 동시에 모두 호출할 수도 있다.

 

가장 간단히 익숙한 C++의 문법으로 델리게이트를 이해하려면 함수포인터 배열이라고 생각하면 된다. 다만, 함수포인터 배열보다는 안정성과 편의성을 조금 더 높였고, Blueprint와의 연계도 고려된 것이 언리얼의 Delegate이다.

 

델리게이트 오브젝트는 복사해도 완벽히 안전하다. 델리게이트는 값으로 전달 가능하나 보통 추천할 만 하지는 않는데, heap 에 메모리를 할당해야 하기 때문. 가급적이면 델리게이트는 항상 참조 전달해야 한다.

 

Delegate는 클래스간 결합도를 낮추는데 용의하고 함수의 반환값과 매개변수(페이로드)만 일치한다면 어떤 함수든 Delegate로 연결하여 호출이 가능하기 때문에 코드의 유연성을 높일 수 있다.

또한 UFUNCTION(BlueprintAssignable) 매크로 설정을 통해서 블루프린트 이벤트 발생에도 아주 용의하여 다방면으로 유용하다.

 

델리게이트 바인딩하기

UObject 나 공유 포인터 클래스 멤버에 델리게이트를 바인딩하는 경우, 델리게이트 시스템은 그 오브젝트에 대한 약한 레퍼런스를 유지할 수 있어, 델리게이트 치하에서 오브젝트가 소멸된 경우 IsBound()  ExecuteIfBound() 함수를 호출하여 처리해 줄 수 있다. 참고로 여러가지 지원되는 오브젝트 유형에 대해서는 특수한 바인딩 문법이 사용된다.

 

함수설명

Bind() 기존 델리게이트 오브젝트에 바인딩합니다.
BindStatic() raw C++ 포인터 글로벌 함수 델리게이트를 바인딩합니다.
BindRaw() 날(raw) C++ 포인터 델리게이트에 바인딩합니다. 날 포인터는 어떠한 종류의 레퍼런스도 사용하지 않아, 만약 오브젝트가 델리게이트 치하에서 삭제된 경우 호출하기가 안전하지 않을 수도 있습니다. Execute() 호출시에는 조심하세요!
BindSP() 공유 포인터-기반 멤버 함수 델리게이트에 바인딩합니다. 공유 포인터 델리게이트는 오브젝트로의 약한 레퍼런스를 유지합니다. ExecuteIfBound() 로 호출할 수 있습니다.
BindUObject() UObject 기반 멤버 함수 델리게이트를 바인딩합니다. UObject 델리게이트는 오브젝트로의 약한 레퍼런스를 유지합니다. ExecuteIfBound() 로 호출할 수 있습니다.
UnBind() 이 델리게이트 바인딩을 해제합니다.

 

페이로드 데이터

델리게이트에 바인딩할 때, 페이로드 데이터를 같이 전해줄 수 있다. 페이로드 데이터란 바인딩된 함수를 불러낼(invoke) 때 직접 전해지는 임의의 변수로써 즉, 매개변수이다. 바인딩 시간에 델리게이트 자체적으로 파라미터를 보관할 수 있게 되니 매우 유용하다. ("다이내믹"을 제외한) 모든 델리게이트 유형은 페이로드 변수를 자동으로 지원한다. 이 예제는 커스텀 변수 둘, 즉 bool 과 int32 를 델리게이트에 전달한다. 그런 다음 델리게이트를 불러낼 때 이 파라미터가 바인딩된 함수에 전달된다. 여분의 변수 인수는 반드시 델리게이트 유형 파라미터 인수 이후에 받아야 한다.

MyDelegate.BindRaw( &MyFunction, true, 20 );

델리게이트 실행하기

델리게이트에 바인딩된 함수는 델리게이트의 Execute() 함수를 호출하여 실행된다. 델리게이트를 실행하기 전 "바인딩" 되었는지 반드시 확인해야 한다다. 이는 코드 안전성을 도모하기 위함인데, 초기화되지 않은 상태로 접근이 가능한 반환값과 출력 파라미터가 델리게이트에 있을 수 있기 때문. 바인딩되지 않은 델리게이트를 실행시키면 일부 인스턴스에서 메모리에 낙서를 해버릴 수가 있다. 델리게이트가 실행해도 안전한 지는 IsBound() 를 호출하여 검사해 볼 수 있다. 또한 반환값이 없는 델리게이트에 대해서는 ExecuteIfBound() 를 호출할 수 있으나, 출력 파라미터는 초기화되지 않을 수 있다는 점 주의하시기 바람.

 

사용예제

어디서든 호출 가능한 Log용 함수가 있다.

class FLogWriter
{
    void WriteToLog( FString );
};

WriteToLog를 호출하려면, 해당 함수와 시그니쳐(반환값, 매개변수)가 똑같은 델리게이트가 선언되어야한다.

DECLARE_DELEGATE_OneParam( FStringDelegate, FString );

'FStringDelegate'라고 델리게이트를 생성하며, 'FString' 유형의 매개변수를 받는다.

클래스에서 이 'FStringDelegate'를 어떻게 선언하는지 아래와 같다.

class FMyClass
{
    FStringDelegate WriteToLogDelegate;
};

델리게이트가 선언되었으므로 여기에 함수를 담아서 사용할 수 있게 되었다. 다만, 시그니쳐가 같아야 한다는 점은 명심해야한다.

델리게이트에 함수를 연결한다. TSharedRef<>로 생성된 객체의 멤버함수를 이용하기 때문에 BindSP를 이용한다. 함수를 델리게이틑에 바인딩한 때, 위의 '델리게이트를 바인딩하기'에 나온 것처럼 지정된 Bind용 함수를 이용해서 바인딩해야한다. 

TSharedRef< FLogWriter > LogWriter( new FLogWriter() );

WriteToLogDelegate.BindSP( LogWriter, &FLogWriter::WriteToLog );

델리게이트에서 'Execute()' 메서드를 사용하면 바인딩된 함수가 호출된다.

WriteToLogDelegate.Execute( TEXT( "델리게이트 쥑이네!" ) );

델리게이트에 실행을 요청하기 전 바인딩 여부를 검사하여 실행하려면 아래와 같이 하면 된다. 

WriteToLogDelegate.ExecuteIfBound( TEXT( "함수가 바인딩되었을 때만 실행!" ) );