UE4技術總結——委托


UE4技術總結——委托

在開始前先說明,這部分內容主要基於UE 4.26.2版本[1]編寫,因此讀者可能需要注意時效性。如果您對源代碼感興趣,可以直接閱讀引擎的DelegateCombinations.hDelegate.h以及相關代碼。

因為是一個非常基礎,時不時會用到的功能,所以這里就不介紹使用場景了,直接進入正題。

一、定義

首先,官方定義如下[2]

委托 是一種泛型但類型安全的方式,可在C++對象上調用成員函數。可使用委托動態綁定到任意對象的成員函數,之后在該對象上調用函數,即使調用程序不知對象類型也可進行操作。復制委托對象很安全。你也可以利用值傳遞委托,但這樣操作需要在堆上分配內存,因此通常並不推薦。請盡量通過引用傳遞委托。

同時,根據官方文檔,虛幻引擎支持3種類型的委托:

  1. 單播委托
  2. 多播委托
    1. 事件
  3. 動態委托

之所以說是3種,是因為事件實際上在現在的版本中差不多就是多播委托(當然,實際上還是有些許不同的,主要是函數調用權限和多播不同,但是實際上也沒有措施保證函數被不是擁有者的對象調用,因此讀者只需要理解為多播委托即可)[3]。而且在UE的4.26.2版本源碼中已經標明,事件類型的委托將會在后面更新的版本移除掉:

image-20210903102550677

因此,我們主要重點還是放在單播、多播、動態委托上,事件不會進行詳細說明。

同時,UE4中存在由基本委托組合起來的委托,但是在介紹組合的委托之前我們先看看這3種基本委托。

接下來我們先簡單看看該怎么用。

順帶一提,這里我默認讀者知道如何在C++中實現委托,如果您還不清楚,那么建議閱讀文末參考中列出的的文章[4](了解即可)。

二、用法

2.1 聲明與調用委托

UE4中的委托都通過宏定義來聲明,隨后就可以通過宏定義聲明的委托來聲明對應的委托變量,實際使用的時候會通過將函數綁定到委托變量來使用。

2.1.1 單播委托

  1. 單播委托只能綁定一個函數指針,執行委托的時候也只能觸發一個函數;

  2. 單播委托綁定的函數可以有返回值,這點和多播委托不同;

2.1.1.a 聲明
// 無返回值函數的委托
// 無參數
DECLARE_DELEGATE(DelegateName);
// 1個參數
DECLARE_DELEGATE_OneParam(DelegateName, Param1Type);
// <num>個參數,最多到9個
DECLARE_DELEGATE_<num>Params(DelegateName, Param1Type, Param2Type, ...);

// 有返回值
// 無參數
DECLARE_DELEGATE_RetVal(RetValType, DelegateName);
// 1個參數
DECLARE_DELEGATE_RetVal_OneParam(RetValType, DelegateName, Param1Type);
// 多個參數,最多到9個
DECLARE_DELEGATE_RetVal_<num>Params(RetValType, DelegateName, Param1Type, Param2Type, ...);

一個簡單的聲明單播委托的例子:

// 直接用宏定義在頂部聲明就可以了
DECLARE_DELEGATE(FLearningDelegate);
class XXX_API ALearnDelegateActor : public AActor
{
    GENERATED_BODY()
public:
    // ... 省略
public:
    // 單播委托帶有UPROPERTY宏,不能添加BlueprintAssignable標識符,動態多播才可以聲明BlueprintAssignable
    FTestDelegate FLearningDelegate;
}
2.1.1.b 綁定

在綁定函數之前我們先要聲明委托和委托變量:

// 單播無參數的委托,其他類型的單播委托如此類推
// 這行通常放在頭文件的上方,類定義之外,畢竟是宏
DECLARE_DELEGATE(FSingleDelagateWithNoParam);

// 用上面聲明的委托聲明委托變量
// 這里放在類定義中,作為一個屬性進行定義
FSingleDelagateWithNoParam SingleDelagateWithNoParam;

然后我們就可以綁定函數了,綁定函數的API有很多種,但是最常用的還是BindUObject,因此這里以BindUObject舉例:

// ADelegateListener::EnableLight的定義類似於void ADelegateListener::EnableLight(),沒有參數,也沒有返回值
// 這個綁定假設是在類里面綁定的,所以用了this,實際上可以是別的UObject
SingleDelagateWithNoParam.BindUObject(this, &ADelegateListener::EnableLight)

下面這張圖列舉了除了BindUObject之外還能夠使用什么函數進行綁定,以及在什么情況下使用[2:1]

除了BindUObject之外還有別的綁定函數,這里直接借用官網過時的文檔中的列表:

image-20211005172900827

大概如上,都非常簡單,在使用的時候按照您要綁定的函數來選擇對應的函數來綁定即可。這里簡單補充幾個官網文檔沒有提及的綁定:

函數 描述
BindThreadSafeSP(SharedPtr, &FClass::Function) 用一個弱指針TWeakPtr來綁定一個原生C++類成員函數,當指針SharedPtr指向的對象無效的時候不會執行綁定的回調函數
BindWeakLambda(UObject, Lambda) 綁定一個匿名函數,在傳入的UObject有效,還沒有被回收的時候都可以調用這個匿名函數。這個匿名函數中可以用this,但是其他關鍵詞不一定能用
BindUFunction(UObject, FName("FunctionName")) 用來綁定一個UObject的UFUNCTION函數,原生的與動態的委托都可以用這個函數來綁定回調函數

這里提幾個注意事項[5]

  1. 注意BindRaw綁定的普通C++對象的成員函數,要特別注意執行的時候這個對象有沒有被銷毀。如果被銷毀了那么觸發委托執行綁定的函數會導致報錯;
  2. 注意BindLambda綁定的Lambda表達式捕獲的外部變量,如果在觸發委托的時候捕獲的引用被銷毀,那么會導致報錯;
  3. BindWeakLambdaBindUObjectBindUFunction綁定時會弱引用一個UObject對象,需要預先IsBound()或者ExecuteIfBound來判斷是否該對象還有效再執行委托,否則可能會報錯;
  4. BindSPBindThreadSafeSP綁定時會弱引用一個智能指針對象(UE4的智能指針),執行前需要先IsBound()或者ExecuteIfBound來判斷該對象是不是還存在,否則可能會報錯;
  5. 如果單播委托對象被銷毀,那么析構函數會自動調用UnBind進行解綁;
2.1.1.c 執行委托

執行單播委托需要調用的函數主要是Execute(您要傳入的參數),要注意的是,這個函數並不會檢查您的綁定情況,因此如果委托未綁定,那么直接執行此函數會導致報錯。因此往往推薦在調用Execute(傳入參數)前先用IsBound()來檢查是否已經進行了綁定。當然也可以直接調用ExecuteIfBound(傳入參數),這個函數等效於if(委托.IsBound())進行判斷后再執行Execute(傳入參數)

image-20211005223951296

2.1.1.d PayLoad

首先介紹下PayLoad的功能,PayLoad是委托綁定的時候傳入的額外參數列表,保存在委托對象內。觸發委托的時候PayLoad會跟着Execute(傳入的參數)ExecuteInBound(傳入的參數)傳入的參數之后填充到綁定函數的參數列表中,然后執行。

舉個例子:

DECLARE_DELEGATE_OneParam(FLearnDelegate, float);

static void LearningDelegate(float Bar) {
    UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f ==="), Bar);
}

static void LearningPayload(float Bar, FString Test) {
    UE_LOG(LogTemp, Log, TEXT("=== INFO: FOOO %f, %s ==="), Bar, *Test);
}

// 在GameInstance的初始化函數中或者其他地方
// 正常使用
FLearnDelegate DelegateObj1;
DelegateObj1.BindStatic(LearningDelegate);
DelegateObj1.ExecuteIfBound(23.0f);

// PayLoad
FLearnDelegate DelegateObj2;
// 這里的“TEST”會在調用綁定函數的時候緊接着委托對戲那個傳入的參數傳入
DelegateObj2.BindStatic(LearningDelegate, FString(TEXT("TEST!")));
// “TEST”會接在23.0f后面,所以最后是傳入到Test參數中
DelegateObj2.ExecuteIfBound(23.0f);
2.1.1.e 底層實現
綁定函數指針

相關代碼在DelegateCombination.h以及Delegate.h中。

首先我們需要有個大體的概念,其實本質上就是保存了一個函數指針,在執行的時候直接訪問該函數指針對應的函數即可,如果是成員函數則比較特殊,需要同時知道成員函數所在的類,同時應該有一個指針指向該對象。接下來我們看具體實現。

image-20211005233553067

image-20211005233615088

可以看到,實際上就是通過TDelegate這個類來實現的,所以實際上我們在定義委托的時候就是在調用TDelegate<returntype(一堆您傳入的參數)>創建委托類型,並通過typedef重命名為您給定的名字,方便記憶與閱讀。TDelegateDelegateSignatureImpl.ini中實現。因為內容比較多,因此我們只看關鍵部分。首先我們看到他繼承了TDelegateBase這個類:

image-20211006153548452

簡單掃幾眼,就會發現實際上用來保存指向函數的指針並不在TDelegate中,而應該是放在了父類,也就是TDelegateBase

image-20211006164312068

image-20211006155731780

讀者可能發現了UserPolicy這個參數,這里實際上是4.26版本才新加入的內容[6]

之前的靜態單播的基類是FDelegateBase,這個類沒有變化,但是所有的public接口被改成了protected,無法直接使用了。這一點真是非常糟糕,哪有增加可擴展性的同時把接口都藏起來的,本來所有實現就都是寫到頭文件里的。

最大的不同是接下來的地方,其實現不是通過直接對FDelegateBase的繼承完成的,而是通過一個叫做FDefaultDelegateUserPolicy的結構體進行中轉的。這個結構體中只定義了三個類型的別名,分別是FDelegateInstanceExtrasFDelegateExtrasFMulticastDelegateExtras。其中FDelegateExtras指向的就是FDelegateBase

靜態單播的實現類TDelegateBase(原來叫TBaseDelegate,這詭異的命名)變成了模板類,該類繼承於模板參數中的FDelegateExtras類型。說到這里我想應該已經明白了UE4這個改動的含義。這意味着我們可以通過自己定義一個FDefaultDelegateUserPolicy以外的其他結構體UserPolicy,並在其中定義上述三個類型,就可以釜底抽薪式地把寫在底層的實現替換成我們自定義的實現,這無疑很大地增加了這個模塊的可擴展性。

簡單的說就是FDelegateBase在經過抽象之后,允許用戶單獨創建一個UserPolicy結構體給TDelegateBase來自定義委托,當然如果沒有傳入自己定義的UserPolicy的話,那么會使用默認的FDefaultDelegateUserPolicy(這里用到了C++的模板偏特化特性[7][8],能夠在給定默認值的同時,能夠讓用戶輸入自己希望的值):

image-20211006194851272

因此實際上此處的UserPolicyFDefaultDelegateUserPolicy,那么我們簡單看看FDefaultDelegateUserPolicy這一struct的內容:

struct FDefaultDelegateUserPolicy
{
    // 這里的using是別名指定
    using FDelegateInstanceExtras  = IDelegateInstance;
    // 注意下面這個,另外兩個會在其他委托中用到,先不管
    using FDelegateExtras          = FDelegateBase;
    using FMulticastDelegateExtras = TMulticastDelegateBase<fdefaultdelegateuserpolicy>;
};

回到開始的TDelegate<inretvaltype(paramtypes...), userpolicy=""> : public TDelegateBase<userpolicy>,我們看看TDelegateBase的定義:

image-20211006195733065

所以實際上最終還是繼承了FDefaultDelegateUserPolicy::FDelegateExtras,即FDelegateBase

我們繼續追蹤GetDelegateInstanceProtected(),繼續看TDelegateBase,但是我們會發現,實際上TDelegateBase也沒有保存指針,只是提供了一系列函數(如,是否已經綁定了函數的IsBound()等):

template <typename userpolicy="">
class TDelegateBase : public UserPolicy::FDelegateExtras
{
    template <typename>
    friend class TMulticastDelegateBase;

    // 用using指定別名
    using Super = typename UserPolicy::FDelegateExtras;

public:
    // 省略部分注釋與宏判斷
    FName TryGetBoundFunctionName() const
    {
        // 注意這里,可以看出不是這里保存的函數指針
        if (IDelegateInstance* Ptr = Super::GetDelegateInstanceProtected())
        {
        // 實際上還是調用了委托對象提供的函數來實現具體的功能
        return Ptr->TryGetBoundFunctionName();
        }

    return NAME_None;
    }
    // 省略一系列函數
}

可以看到,實際上即便是TDelegateBase,也是要通過Super::GetDelegateInstanceProtected()來獲取委托對象,這個函數最終調用FDelegateBase類提供的GetDelegateInstanceProtected()來獲取委托對象(注意using Super = typename UserPolicy::FDelegateExtras;,而在FDefaultDelegateUserPolicy中,using FDelegateExtras = FDelegateBase;),最終通過IDelegateInstance類的委托對象提供的函數來實現相關功能。因此我們還得要接着往下面看才能找到真正保存函數指針的地方。

因此,我們看到FDelegateHandle

class FDelegateBase
{
    template <typename>
    friend class TMulticastDelegateBase;

    template <typename>
    friend class TDelegateBase;
  
protected:
    /**
     * Creates and initializes a new instance.
     *
     * @param InDelegateInstance The delegate instance to assign.
    */
    explicit FDelegateBase()
        : DelegateSize(0)
    {
    }

    ~FDelegateBase()
    {
    // 可以看到實際上在被銷毀的時候會自動調用函數取消綁定
        Unbind();
    }
    // 省略部分函數
  
    // 這里是重點
    /**
     * Gets the delegate instance.  Not intended for use by user code.
     *
     * @return The delegate instance.
     * @see SetDelegateInstance
     */
    FORCEINLINE IDelegateInstance* GetDelegateInstanceProtected() const
    {
        return DelegateSize ? (IDelegateInstance*)DelegateAllocator.GetAllocation() : nullptr;
    }
    // 省略函數
private:
    // 這個也是重點
    FDelegateAllocatorType::ForElementType<falignedinlinedelegatetype> DelegateAllocator;
    int32 DelegateSize;
}

上面提到,TDelegateBase最終調用的是FDelegateBase提供的GetDelegateInstanceProtected(),而這里我們可以看到,實際上是返回IDelegateInstance類型的數據(這里先忽略掉DelegateAllocator,只需要理解為一個工具類,用來分配內存,因為與委托不太相關所以先不詳細說明,如果感興趣可以看這篇文章[9]),因此最終函數指針理論上是包裹在IDelegateInstance中的。

但是再想想,實際情況肯定沒有這么簡單,還記得我們前面說到的綁定函數嗎?實際可能傳入的函數指針類型非常多,例如可能傳入一個在UObject對象中的成員函數,可能傳入一個lambda函數等。所以實際上,會包裹在IDelegateInstance為基類的,根據各種傳入函數指針類型進行適配的派生類中。

例如,接着上面往下看,我們可以看到這類型的函數:

/**
     * Static: 用來創建C++全局函數指針的委托
     */
    template <typename... vartypes="">
    UE_NODISCARD inline static TDelegate<retvaltype(paramtypes...), userpolicy=""> CreateStatic(typename TIdentity<retvaltype (*)(paramtypes...,="" vartypes...)="">::Type InFunc, VarTypes... Vars)
    {
        TDelegate<retvaltype(paramtypes...), userpolicy=""> Result;
    // 重點是下面這個,TBaseStaticDelegateInstance的基類就是IDelegateInstance
        TBaseStaticDelegateInstance<functype, userpolicy,="" vartypes...="">::Create(Result, InFunc, Vars...);
        return Result;
    }

    /**
     * Static: 創建lambda函數的委托
     */
    template<typename functortype,="" typename...="" vartypes="">
    UE_NODISCARD inline static TDelegate<retvaltype(paramtypes...), userpolicy=""> CreateLambda(FunctorType&& InFunctor, VarTypes... Vars)
    {
        TDelegate<retvaltype(paramtypes...), userpolicy=""> Result;
        TBaseFunctorDelegateInstance<functype, userpolicy,="" typename="" tremovereference<functortype="">::Type, VarTypes...>::Create(Result, Forward<functortype>(InFunctor), Vars...);
        return Result;
    }
// 還有更多,這里忽略

簡單看下TBaseStaticDelegateInstance

image-20211007155244395

可以很輕松找到保存C++函數指針的變量(這個變量類型是UE4提供的專門用來保存C++函數指針的類型,網上資料很多[10],這里就不進行介紹了)。

同理,相似的,綁定UObject對象成員函數委托創建函數則有:

image-20211007160916551

最終執行的時候的形式就類似於這樣:

// 全劇函數
(*MyDelegate)();
// 對象成員函數
(MyObj->*FuncPtr)();
// 如果是在棧上
(StackObj.*Func1Ptr)();
Payload的實現

當然實際上UE4中會支持Payload,會先把一部分預先輸入的參數拼接到調用委托的時候傳入的參數后面去,形成一個參數列表,最后一起作為參數輸入到綁定函數,但是原理差不多。

以全局函數的執行為例:

image-20211007172150523

Payload實際上是一個TTuple

image-20211007172225486

最終執行:

image-20211007172259116

因為Payload特性前面介紹過,所以這里不贅述。

綁定

但是只有創建是不行的,這時候的委托還沒有綁定上要執行的函數。我們還是以綁定全局函數為例:

/**
     * 綁定一個C++全局函數
     */
    template <typename... vartypes="">
    inline void BindStatic(typename TBaseStaticDelegateInstance<functype, userpolicy,="" vartypes...="">::FFuncPtr InFunc, VarTypes... Vars)
    {
        *this = CreateStatic(InFunc, Vars...);
    }

結合上面的CreateStatic的實現就可以明白了,因為CreateStatic返回的是右值,這里左側的*this=會調用到TDelegate的Move Assigment Operator:

    /**
     * Move assignment operator.
     *
     * @param    OtherDelegate    Delegate object to copy from
     */
    inline TDelegate& operator=(TDelegate&& Other)
    {
        if (&Other != this)
        {
            // this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions
            DelegateInstanceInterfaceType* OtherInstance = Other.GetDelegateInstanceProtected();

            if (OtherInstance != nullptr)
            {
                OtherInstance->CreateCopy(*this);
            }
            else
            {
                Unbind();
            }
        }

        return *this;
    }

最終將創建出來的TDelegate賦值給自身,從而實現綁定函數。

綁定不同的函數指針對應不同的T<函數指針類型>DelegateInstance<...>::Create(...),這里列舉下,實際上看源代碼也可以理解:

創建函數 對應的Delegate Instance創建函數
CreateStatic() TBaseStaticDelegateInstance<...>::Create(...)
CreateLambda() TBaseFunctorDelegateInstance<...>::Create(...)
CreateWeakLambda() TWeakBaseFunctorDelegateInstance<...>::Create(...)
CreateRaw() TBaseRawMethodDelegateInstance<...>::Create(...)
CreateSP() TBaseSPMethodDelegateInstance<...>::Create(...)
CreateThreadSafeSP() TBaseSPMethodDelegateInstance<...>::Create(...)
CreateUFunction() TBaseUFunctionDelegateInstance<...>::Create(...)
CreateUObject() TBaseUObjectMethodDelegateInstance<...>::Create(...)
補充

這里看起來沒有介紹帶參數、返回值的情況,因為實際上帶參數、返回值的也是調用了FUNC_DECLARE_DELEGATE,調用的typedef都一樣,只是傳入到模板的參數數量不一樣(借助了C++11中的可變模板參數實現)。

image-20211006021802515

最終還是:

image-20211006021832263

image-20211007212745935

另外,這里的__VA_ARGS__實際上就是:

image-20211006021854475

比較容易理解,所以這里不作詳細解釋。

2.1.1.f 總結

總而言之,單播委托的使用流程如下圖所示:

graph TD; 開始 --> 使用宏定義委托類型 -->聲明委托對象--> 綁定需要執行的函數指針到委托對象上-->|需要的時候|觸發委托對象並執行指針指向的函數; 觸發委托對象並執行指針指向的函數-->|不再需要綁定的函數|從委托對象中解綁函數-->|不再需要委托對象|銷毀委托對象; 從委托對象中解綁函數-->|綁定新的函數|綁定需要執行的函數指針到委托對象上; 觸發委托對象並執行指針指向的函數-->|指向的函數失效|報錯;

而委托的類層次結構我們可以總結為(其實我不是特別熟悉UML圖,希望沒有錯):

classDiagram FDelegateBase <|-- TDelegateBase TDelegateBase <|-- TDelegate TDelegate ..|> FDefaultDelegateUserPolicy TDelegateBase ..|> FDefaultDelegateUserPolicy FDelegateBase *-- IDelegateInstance IDelegateInstance <|-- IBaseDelegateInstance IBaseDelegateInstance <|-- TCommonDelegateInstanceState TCommonDelegateInstanceState <|-- TBaseStaticDelegateInstance TCommonDelegateInstanceState <|-- TBaseFunctorDelegateInstance TCommonDelegateInstanceState <|-- TWeakBaseFunctorDelegateInstance TCommonDelegateInstanceState <|-- TBaseRawMethodDelegateInstance TCommonDelegateInstanceState <|-- TBaseSPMethodDelegateInstance TCommonDelegateInstanceState <|-- TBaseUFunctionDelegateInstance TCommonDelegateInstanceState <|-- TBaseUObjectMethodDelegateInstance TDelegate *-- TBaseStaticDelegateInstance TDelegate *-- TBaseFunctorDelegateInstance TDelegate *-- TWeakBaseFunctorDelegateInstance TDelegate *-- TBaseRawMethodDelegateInstance TDelegate *-- TBaseSPMethodDelegateInstance TDelegate *-- TBaseUFunctionDelegateInstance TDelegate *-- TBaseUObjectMethodDelegateInstance FDefaultDelegateUserPolicy : +FDelegateInstanceExtras(IDelegateInstance) FDefaultDelegateUserPolicy : +FDelegateExtras(FDelegateBase) FDefaultDelegateUserPolicy : +FMulticastDelegateExtras(TMulticastDelegateBase<fdefaultdelegateuserpolicy>) class TDelegate { +CreateStatic(...) +CreateLambda(...) +CreateWeakLambda(...) +CreateRaw(...) +CreateSP(...) +CreateThreadSafeSP(...) +CreateUFunction(...) +CreateUObject(...) +TDelegate(TDelegate&& Other) +TDelegate& operator=(TDelegate&& Other) +BindStatic(...) +BindLambda(...) +BindWeakLambda(...) +BindRaw(...) +BindSP(...) +BindThreadSafeSP(...) +BindUFunction(...) +BindUObject(...) } class TDelegateBase { -class TMulticastDelegateBase -Super(FDelegateBase) } class FDelegateBase { #FDelegateBase& operator=(FDelegateBase&& Other) #void Unbind() #IDelegateInstance* GetDelegateInstanceProtected() const -void* Allocate(int32 Size) -FDelegateAllocatorType::ForElementType<falignedinlinedelegatetype> DelegateAllocator -int32 DelegateSize } class IDelegateInstance { <<interface>> +FName TryGetBoundFunctionName() +UObject* GetUObject() +const void* GetObjectForTimerManager() const +uint64 GetBoundProgramCounterForTimerManager() const +bool HasSameObject( const void* InUserObject ) const +bool IsCompactable( ) +bool IsSafeToExecute( ) +FDelegateHandle GetHandle() const } class TCommonDelegateInstanceState { #TTuple<vartypes...> Payload #FDelegateHandle Handle +RetValType(InRetValType) }

此外:

  1. 單播委托支持PayLoad功能,但是動態類型的委托並不支持PayLoad;
  2. 單播委托在執行之前必須要IsBound()檢查是否已經綁定,否則會報錯;
  3. 單播委托允許綁定函數帶有返回值;

其實單播委托理解了后面的都不難理解了,因此后面的內容會沒有單播委托這么詳細(畢竟實現都相似的)。


2.1.2 動態(單播)委托

注意:這里討論的是動態單播委托,動態多播委托后面會另外介紹

  1. 動態其實是指能夠被序列化,允許動態綁定,除此之外實際上和單播代理沒有太大區別;

  2. 動態委托也可以有返回值,但是只能有一個返回值;

  3. 動態即能夠被序列化,因此可以在藍圖中使用,也可以添加UPROPERTY

  4. 綁定的時候不需要函數指針,只需要函數名稱字符串,但是只能夠綁定UFUNCTION

  5. 動態委托運行是最慢的,所以如果沒有必要的話就用別的委托;

2.1.2.a 聲明

其實和單播委托的聲明差不多:

DECLARE_DYNAMIC_DELEGATE[_RetVal, ...]( DelegateName );
// 例如無參數,無返回值
DECLARE_DYNAMIC_DELEGATE(FNoRetNoParamDelegate);
// 例如1個參數無返回值,最多支持到9個,注意和前面不同,給定參數類型的同時要加上類型名字,並且綁定參數和委托要保持一致
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnAssetLoaded, class UObject*, Loaded);
// 例如1個返回值一個參數
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(UWidget*, FGenerateWidgetForObject, UObject*, Item); 
2.1.2.b 綁定

4.26的動態委托綁定函數只有一個BindUFunction,並提供UObject對象、函數名字即可。

2.1.2.c 執行委托

和單播委托類似:

image-20211007234315173

2.1.2.d 底層實現

image-20211007234357644

可以看到實際上依托TBaseDynamicDelegate來實現,而且宏定義聲明一個動態委托就是聲明了一個類繼承TBaseDynamicDelegate。宏定義里面也另外定義了ExecuteIfBoundExecute函數,實際執行委托也是通過宏定義里面定義的這兩個函數,同時依托UE4的反射、序列化機制實現的。

TBaseDynamicDelegate內的實現不多,實際上還是得依靠TScriptDelegate

image-20211007234637981

TScriptDelegate才是真正保存函數名字、綁定的對象的弱指針的地方:

image-20211007234804808

簡單看下綁定部分,因為只能綁定UFUNCTION函數,所以只有一個綁定函數:

image-20211007234834434

執行則是依托一開始宏定義里面定義的Execute(傳入參數)

image-20211007234952902

實際執行的時候UE4會根據輸入的函數名字找到對應的函數並執行,這個函數最終會被上面定義的Execute調用:

image-20211008000445308

2.1.2.e 總結

因為比較簡單,所以這里就先不花UML圖來解析了。

  1. 實際上聲明一個動態委托類型就是創建了一個繼承TBaseDynamicDelegate的類,並且類名為動態委托的名字;
  2. 動態委托在執行時需要實時在類中按照給定的函數名字查找對應的函數,因此執行速度很慢,所以如果能用別的不是動態的委托代替就用別的委托[11]
  3. 動態委托能夠被藍圖調用;
  4. 動態委托能夠序列化,因此可以被存儲到硬盤上;
  5. 綁定的時候不需要函數指針,只需要函數名稱字符串,但是只能夠綁定UFUNCTION

2.1.3 多播委托

吐個槽,官方文檔真的是一言難盡,只是multicast delegate這個詞在中文頁面上都有2種不同的翻譯。更加關鍵的是,多播委托的官方文檔居然還有低級錯誤,在《多播委托》頁面最上面寫明了“多播委托不能使用返回值”,下面給的聲明多播委托示例就帶了個返回值。

  1. 多播委托能綁定多個函數指針,委托被執行的時候也會觸發多個函數;
  2. 多播委托執行的時候,執行綁定該委托的函數的順序實際上是沒有規定的(因此可能最后綁定的函數最先被執行)
  3. 多播委托不允許有返回值。實際上底層是一個保存了所有綁定了這個委托的函數的FDelegateBase數組,執行委托的時候會遍歷數組並調用綁定的函數
2.1.3.a 聲明
DECLARE_MULTICAST_DELEGATE<參數數量>( DelegateName, ParamsTypes );
// 例如0個參數
DECLARE_MULTICAST_DELEGATE( DelegateName );
// 例如1個參數
DECLARE_MULTICAST_DELEGATE_OneParam( DelegateName, Param1Type );

比較簡單,和前面的委托都差不多。

2.1.3.b 綁定

先以綁定UObject對象的成員函數為例:

UDelegatepTestClass* UObjMC = NewObject<udelegateptestclass>(this, UDelegatepTestClass::StaticClass());
// 先傳入UObject,然后傳入成員函數指針
CharacterDelegateMulticast7.AddUObject(UObjMC, &UDelegatepTestClass::DelegateProc1);

其他的綁定方式差不太多,故在此不贅述。

所有的綁定方式如下:

函數名 用途
FDelegateHandle Add(const FDelegate& InNewDelegate) 將某個函數委托添加到該多播委托的調用列表中
FDelegateHandle AddStatic(...) 添加原始C++指針全局函數委托
FDelegateHandle AddLambda(...) 添加匿名函數委托
FDelegateHandle AddWeakLambda(...) 添加弱引用對象的匿名函數委托,會對對象弱引用
FDelegateHandle AddRaw(...) 添加原始C++指針委托。原始指針不使用任何類型的引用,因此如果從委托下面刪除了對象,則調用此函數可能不安全。調用Execute()時請小心!
FDelegateHandle AddSP(...) 添加基於共享指針的(快速、非線程安全)成員函數委托,共享指針委托保留對對象的弱引用
FDelegateHandle AddThreadSafeSP(...) 添加基於共享指針的成員函數委托(相對較慢,但是線程安全),會對對象弱引用
FDelegateHandle AddUFunction(...) 添加UFunction類型的成員函數,會對輸入的對象弱引用
FDelegateHandle AddUObject(...) 添加UObject對象的成員函數,會對輸入的對象弱引用
2.1.3.c 執行

委托.Broadcast()即可,即便在沒有任何綁定的時候都可以用這個函數來觸發委托執行。不過需要注意的是,綁定函數的執行順序是未定義的,執行順序很可能與綁定順序不同(畢竟多播委托可能會多次添加、移除委托)。

image-20211007222642171

2.1.3.d 底層實現
保存的綁定函數數組

先看宏定義:

image-20211007224453206

接着往下看:

image-20211007224518744

可以看到實際上是TMulticastDelegate,看看它的定義:

image-20211007224553521

和單播委托一樣,通過偏特化的方式保證UserPolicy在有默認值的同時能夠讓用戶輸入自己定義的UserPolicy。也是和單播委托一樣,實際上保存指針的數組並不在TMulticastDelegate中,要在基類中查找,我們先看上一級的UserPolicy::FMulticastDelegateExtras,即TMulticastDelegateBase<fdefaultdelegateuserpolicy>

image-20211007224840340

可以看到,實際上就是一個TDelegateBase數組(FMulticastInvocationListAllocatorType先不用管,主要是和內存分配有關,與我們關注的重點不太相關)。

其實說到這里基本上可以和單播委托那邊的分析結合起來看,但是首先,我們先接着看綁定的實現。

綁定的實現

首先我們看看常用的AddUObject是怎么實現的:

template <typename userclass,="" typename...="" vartypes="">
    inline FDelegateHandle AddUObject(UserClass* InUserObject, typename TMemFunPtrType<false, userclass,="" void="" (paramtypes...,="" vartypes...)="">::Type InFunc, VarTypes... Vars)
    {
        static_assert(!TIsConst<userclass>::Value, "Attempting to bind a delegate with a const object pointer and non-const member function.");

      // 這里實際上調用了上面提到的FDelegate::CreateUObject,不理解的話看上面的內容即可
        return Add(FDelegate::CreateUObject(InUserObject, InFunc, Vars...));
    }

可以看到實際上還是依靠了另一個函數Add,並且實際上用到了上面提到的單播委托的FDelegate::CreateUObject來創建一個委托對象。那么我們接着看看Add的實現:

/**
     * Adds a delegate instance to this multicast delegate's invocation list.
     *
     * @param Delegate The delegate to add.
     */
    FDelegateHandle Add(FDelegate&& InNewDelegate)
    {
        FDelegateHandle Result;
        if (Super::GetDelegateInstanceProtectedHelper(InNewDelegate))
        {
            Result = Super::AddDelegateInstance(MoveTemp(InNewDelegate));
        }

        return Result;
    }

這里的Super其實是:

image-20211007230828611

TMulticastDelegateBase<fdefaultdelegateuserpolicy>,因此最終會調用TMulticastDelegateBase的:

image-20211007230934442

InvocationList的定義是:

image-20211007231039524

即用到上面定義的類型:

image-20211007231102427

所以實際上就是先創建一個單播委托,然后添加到了自己維護的TArray數組中。

執行委托的實現
/**
     * Broadcasts this delegate to all bound objects, except to those that may have expired.
     *
     * The constness of this method is a lie, but it allows for broadcasting from const functions.
     */
    void Broadcast(ParamTypes... Params) const
    {
        bool NeedsCompaction = false;

        Super::LockInvocationList();
        {
            const InvocationListType& LocalInvocationList = Super::GetInvocationList();

            // call bound functions in reverse order, so we ignore any instances that may be added by callees
            for (int32 InvocationListIndex = LocalInvocationList.Num() - 1; InvocationListIndex >= 0; --InvocationListIndex)
            {
                // this down-cast is OK! allows for managing invocation list in the base class without requiring virtual functions
                const FDelegate& DelegateBase = (const FDelegate&)LocalInvocationList[InvocationListIndex];

                IDelegateInstance* DelegateInstanceInterface = Super::GetDelegateInstanceProtectedHelper(DelegateBase);
                if (DelegateInstanceInterface == nullptr || !((DelegateInstanceInterfaceType*)DelegateInstanceInterface)->ExecuteIfSafe(Params...))
                {
                    NeedsCompaction = true;
                }
            }
        }
        Super::UnlockInvocationList();

        if (NeedsCompaction)
        {
            const_cast<tmulticastdelegate*>(this)->CompactInvocationList();
        }
    }

可以看到實際上就是遍歷一遍數組然后一個個調用ExecuteIfSafe(傳入參數)。注意ExecuteIfSafe,如果委托無法被執行,那么就會返回false

ExecuteIfSafe的實現隨着不同類型的綁定函數而不同,例如如果綁定的是全局函數,實際上實現是:

bool ExecuteIfSafe(ParamTypes... Params) const final
{
  // Call the static function
  checkSlow(StaticFuncPtr != nullptr);

  (void)this->Payload.ApplyAfter(StaticFuncPtr, Params...);

  return true;
}

可以看到無論如何都會執行,但是如果是別的,例如綁定的是weaklambda,那么:

bool ExecuteIfSafe(ParamTypes... Params) const final
{
  if (ContextObject.IsValid())
  {
    (void)this->Payload.ApplyAfter(Functor, Params...);
    return true;
  }

  return false;
}

會判斷弱引用的對象是不是還有效,如果已經被銷毀了就不會執行並且返回false

這樣就可以保證無論何時調用Broadcast()都是安全的。

2.1.3.e 總結
  1. 實際上多播委托就是維護了一個由單播委托組成的數組,依托單播委托實現的;

  2. 無論何時調用Broadcast()都是安全的。


2.1.4 事件

事件和多播委托相似(實際上就是多播,只是多了個friend class OwningType,用來辨別調用者是不是代理擁有者),功能都差不多,只是限定死了部分函數的權限:只有聲明事件的類可以調用事件的BroadcastIsBoundClear函數。這就保證了只有事件的擁有者能夠觸發事件。

事件綁定的函數也是不能夠有返回值的。

// 和組播類似
// 注意首個參數,用來指定事件擁有者
DECLARE_EVENT( OwningType, EventName );
// 1個參數
DECLARE_EVENT_OneParam( OwningType, EventName, Param1Type );
// 2個參數
DECLARE_EVENT_TwoParams( OwningType, EventName, Param1Type, Param2Type );
// 多個參數
DECLARE_EVENT_<num>Params( OwningType, EventName, Param1Type, Param2Type, ...);

事件和多播基本一致,而且因為后面的版本中事件類型會被移除,因此這里不進行詳細說明。

2.1.5 動態多播委托

實際上上面已經詳細說明了動態委托、多播委托,如果上面的內容理解了的話那么這里的內容也是很容易能夠理解的了。

2.1.5.a 聲明
// 動態多播不能有返回值,所以只列舉有參數、無參數的例子
// 無參數
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOpenViewDelegate_DynamicMulticast);
// 1個參數,和前面不同的是要加上參數名字
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FCharacterDelegate_DynamicMulticast, int, nCode); 
2.1.5.b 綁定
綁定函數 使用場景
Add 添加一個函數委托
AddUnique 添加一個函數委托,但是只有在這個函數委托不存在維護的數組中的時候才添加(根據委托的簽名是否已經存在數組中進行判斷)
AddDynamic 用來綁定一個UObject類型的成員函數到委托中(這個接口實際上通過宏重定向到__Internal_AddDynamic
AddUniqueDynamic 與上面的AddDynamic一樣,但是會根據函數的簽名確保不重復添加
2.1.5.c 執行

直接調用Broadcast(輸入參數)即可,任何時候都可以調用這個函數,與多播委托一樣。

2.1.5.d 底層實現
/** Declares a blueprint-accessible broadcast delegate that can bind to multiple native UFUNCTIONs simultaneously */
#define DECLARE_DYNAMIC_MULTICAST_DELEGATE( DelegateName ) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_DELEGATE) FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE( FWeakObjectPtr, DelegateName, DelegateName##_DelegateWrapper, , FUNC_CONCAT( *this ), void )

可以看出實際上是調用了FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE:

/** Declare user's dynamic multi-cast delegate, with wrapper proxy method for executing the delegate */
#define FUNC_DECLARE_DYNAMIC_MULTICAST_DELEGATE(TWeakPtr, DynamicMulticastDelegateClassName, ExecFunction, FuncParamList, FuncParamPassThru, ...) \
class DynamicMulticastDelegateClassName : public TBaseDynamicMulticastDelegate<tweakptr, __va_args__=""> \
    { \
    public: \
        /** Default constructor */ \
        DynamicMulticastDelegateClassName() \
        { \
        } \
        \
        /** Construction from an FMulticastScriptDelegate must be explicit.  This is really only used by UObject system internals. */ \
        explicit DynamicMulticastDelegateClassName( const TMulticastScriptDelegate<>& InMulticastScriptDelegate ) \
            : TBaseDynamicMulticastDelegate<tweakptr, __va_args__="">( InMulticastScriptDelegate ) \
        { \
        } \
        \
        /** Broadcasts this delegate to all bound objects, except to those that may have expired */ \
        void Broadcast( FuncParamList ) const \
        { \
            ExecFunction( FuncParamPassThru ); \
        } \
    };

可以看到,實際上和動態委托類似,會變成一個繼承TBaseDynamicMulticastDelegate的類:

image-20211008002920696

TBaseDynamicMulticastDelegate提供了__Internal_AddDynamic的實現:

/**
     * Binds a UObject instance and a UObject method address to this multi-cast delegate.
     *
     * @param    InUserObject        UObject instance
     * @param    InMethodPtr            Member function address pointer
     * @param    InFunctionName        Name of member function, without class name
     *
     * NOTE:  Do not call this function directly.  Instead, call AddDynamic() which is a macro proxy function that
     *        automatically sets the function name string for the caller.
     */
    template< class UserClass >
    void __Internal_AddDynamic( UserClass* InUserObject, typename FDelegate::template TMethodPtrResolver< UserClass >::FMethodPtr InMethodPtr, FName InFunctionName )
    {
        check( InUserObject != nullptr && InMethodPtr != nullptr );

        // NOTE: We're not actually storing the incoming method pointer or calling it.  We simply require it for type-safety reasons.

        FDelegate NewDelegate;
        NewDelegate.__Internal_BindDynamic( InUserObject, InMethodPtr, InFunctionName );

        this->Add( NewDelegate );
    }

最終調用的Add則由基類TMulticastScriptDelegate實現:

image-20211008003104051

而且最終保存的數組實際上也保存在TMulticastScriptDelegate中:

image-20211008003145768

可以看到,實際上就是一個數組,里面保存了一系列的動態委托。而Broadcast(傳入參數)最終會調用到TMulticastScriptDelegate的:

/**
     * Executes a multi-cast delegate by calling all functions on objects bound to the delegate.  Always
     * safe to call, even if when no objects are bound, or if objects have expired.  In general, you should
     * never call this function directly.  Instead, call Broadcast() on a derived class.
     *
     * @param    Params                Parameter structure
     */
    template <class uobjecttemplate="">
    void ProcessMulticastDelegate(void* Parameters) const
    {
        if( InvocationList.Num() > 0 )
        {
            // Create a copy of the invocation list, just in case the list is modified by one of the callbacks during the broadcast
            typedef TArray< TScriptDelegate<tweakptr>, TInlineAllocator< 4 > > FInlineInvocationList;
            FInlineInvocationList InvocationListCopy = FInlineInvocationList(InvocationList);
    
            // Invoke each bound function
            for( typename FInlineInvocationList::TConstIterator FunctionIt( InvocationListCopy ); FunctionIt; ++FunctionIt )
            {
                if( FunctionIt->IsBound() )
                {
                    // Invoke this delegate!
                    FunctionIt->template ProcessDelegate<uobjecttemplate>(Parameters);
                }
                else if ( FunctionIt->IsCompactable() )
                {
                    // Function couldn't be executed, so remove it.  Note that because the original list could have been modified by one of the callbacks, we have to search for the function to remove here.
                    RemoveInternal( *FunctionIt );
                }
            }
        }
    }

與多播委托類似,也是會在調用前先用FunctionIt->IsBound()進行判斷,確保執行安全。當然,前面提到了動態委托運行速度很慢,所以您可以猜到動態多播會是本文中所有的委托中執行最慢的。

參考

注意:因為文章經過多次修改,因此實際上這里的順序與文中提及的順序不一致。LaTeX對引用順序的處理就很好,所以后面我可能會考慮改用LaTeX來做這類筆記


  1. UE 4.26源代碼 ↩︎

  2. 官方文檔:委托:嚴重過時的官方文檔,請以最新源代碼內容為准 ↩︎ ↩︎

  3. 關於各類委托之間的不同點的討論 ↩︎

  4. C++中實現委托:如果好奇在純C++代碼中如何實現委托,那么可以參考這篇文章 ↩︎

  5. 全面理解UE4委托 ↩︎

  6. UE4:4.26版本對Delegate模塊的改進 ↩︎

  7. C++ 模板,特化,與偏特化 ↩︎

  8. 泛化之美--C++11可變模版參數的妙用 ↩︎

  9. UE4-深入委托Delegate實現原理:這篇文章可以說是幫了大忙,不過本文部分內容實際上參考了這里的分析。但是文中有一部分內容已經對應不上4.26及以后的版本的源代碼了。不過,還是非常值得一看,強烈推薦 ↩︎

  10. FFuncPtr官方文檔:這個官方文檔和往常一樣,寫得和沒寫一樣,建議看別的 ↩︎

  11. 【UE4筆記】各種Delegate委托的區別和應用 ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM