[UE C++] Delegate使用详解

[UE C++] Delegate使用详解

前言:

本文介绍了Delegate的使用方法,内容我认为比较全面,认真读完绝对会有收获,但并没有对其实现原理进行深入剖析,读者可以查阅这些文章对Delegate的原理进行深入了解

  • 一文理解透UE委托Delegate
  • UE4-深入委托Delegate实现原理

1. 概念

  • UE 的Delegate是不同对象传递消息的重要方法,其优点在于可以降低对象之间的耦合性,即委托的触发者不与监听者有直接关联,两者通过委托对象间接的建立联系
  • Delegate的本质是一个特殊的类对象(TBaseDelegate),里面存储了一个或多个函数指针、调用参数、返回值。委托触发时,会依次调用每个函数指针,输入参数,最后得到返回值传递给触发者
  • Delegate的类型大致有五类:Unicast(单播),Multicast(多播),Dynamic Unicast(动态单播),Dynamic Multicast(动态多播),Events(事件)
  • 每一类Delegate又可从下面几个维度组合形成不同的类型
    • 返回值
    • 声明为 const 函数。
    • 最多4个 payload 变量。
    • 最多8个函数参数,(可能是9个?)
  • Delegate的使用流程可分为4步
    • 使用DECLARE_*宏声明一个自定义delegate类型FDelegateXXX
    • 声明一个FDelegateXXX类型的代理对象
    • 绑定需要执行的函数指针到代理对象上
    • 执行代理对象
  • 复制委托对象很安全,可以利用值传递委托,但这样的操作需要在堆上分配内存,因此通常并不推荐,尽量通过引用传递委托

2. Unicast(单播)

2.1 特点

  • 只能绑定一个函数指针,实现一对一通知
  • 支持返回值
  • 支持最多4个payload参数
  • 最多8个函数参数
  • 不支持反射以及序列化,不能加UPROPERTY()

2.2 定义Delegate类型

//无返回值,无函数参数
DECLARE_DELEGATE(FTestDelegate);

//有返回值,无函数参数
DECLARE_DELEGATE_RetVal(int32,FTestDelegate);

//无返回值,1个函数参数
DECLARE_DELEGATE_OneParam(FTestDelegate, int32);

//有返回值,1个函数参数
DECLARE_DELEGATE_RetVal_OneParam(bool, FTestDelegate, int32);

注意事项:

  • 增加函数参数个数,只需将OneParam换为TwoParams,ThreeParams ···· 就可。注意OneParam无 s ,两个及以上有 s
  • ParamType后面没有ParamName,不然会报错(动态委托ParamType后面必须跟着ParamName)

2.3 绑定和执行委托

声明和定义委托及代理对象

DECLARE_DELEGATE_RetVal_OneParam(bool, FTestDelegate, int32);

FTestDelegate MyTestDelegate;
2.3.1 BindStatic

作用:绑定类的static函数,全局函数

//静态函数
bool ADelegateTest::StaticTest(int32 num)
{
	UE_LOG(LogTemp, Warning, TEXT("Num:%d"), num);
	return true;
}

//绑定和执行
MyTestDelegate.BindStatic(&ThisClass::StaticTest);
bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(10);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);

注意拥有返回值的Delegate无法使用ExecuteIfBound,需要手动调用IsBound来检测有效性,不然有可能会触发内存违规操作

这里再解释payload参数的意思,后续不会再举例子

  • Payload为代理绑定时传递的额外参数变量列表,这些参数会存储在代理对象内部;
  • 在触发代理时,Payload会紧跟着Execute、ExecuteIfBound或Broadcast传入的参数之后,填充到绑定函数指针的参数列表中,然后执行
//静态函数
bool ADelegateTest::StaticTestPayload(int32 num, FString text)
{
	UE_LOG(LogTemp, Warning, TEXT("Num:%d   text:%s"), num, *text);
	return true;
}

//绑定和执行
MyTestDelegate.BindStatic(&ThisClass::StaticTestPayload, FString("BindStatic PatloadTest"));
bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(10);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);
2.3.2 BindRaw

作用:绑定普通C++对象的成员函数

//普通C++对象
class DelegateTestClass 
{
public:
	bool BindRawTest(int32 num, FString text)
	{
		UE_LOG(LogTemp, Warning, TEXT("Num:%d   text:%s"), num, *text);
		return true;
	}
};

//绑定和执行
DelegateTestClass TestObject;
MyTestDelegate.BindRaw(&TestObject, &DelegateTestClass::BindRawTest,FString("BindRawTest"));

bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(10);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);

注意事项:

  • BindRaw函数用于绑定普通c++对象的成员函数,若该c++对象已被销毁,触发代理执行该对象的成员函数,将会导致内存违规操作
  • 由于绑定的是普通C++对象,UE反射系统无法获知这个C++对象的状态,这个违规操作就无法通过 IsBound 或者 ExecuteIfBound 来检测出来,只有通过if(object)来阻止

若将代码改造成下面的样子,则会出现内存违规,text后面会跟着一串乱码

//普通C++对象
class DelegateTestClass 
{
	FString text = "DelegateTestClass";
public:
	bool BindRawTest(int32 num)
	{
		UE_LOG(LogTemp, Warning, TEXT("Num:%d   text:%s"), num, *text);
		return true;
	}
};

//绑定和执行
DelegateTestClass* TestObject = new DelegateTestClass();
MyTestDelegate.BindRaw(TestObject, &DelegateTestClass::BindRawTest);

delete TestObject;

bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(10);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);
2.3.3 BindLambda

作用:绑定Lambda函数(可捕获参数)

MyTestDelegate.BindLambda(
    [](int num) -> bool
    {
        UE_LOG(LogTemp, Warning, TEXT("Num:%d"), num);
        return true;
    }
);

bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(50);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);

注意事项: 若lambda表达式捕获外部变量已被销毁,触发代理执行lambda表达式,将会导致内存违规操作。和BindRaw同理无法通过 IsBound 或者 ExecuteIfBound 来避免

2.3.4 BindWeakLambda

作用:绑定Lambda函数(可捕获参数),但是会将其与一个UObject对象进行弱引用关联(不影响该对象被gc回收),若这个UObject对象被gc回收,执行调用IsBoundExecuteIfBound会检测到该UObject无效,则可避免内存违规操作

ADelegateTest* UObjectTest = NewObject(this, ADelegateTest::StaticClass());
MyTestDelegate.BindWeakLambda(
    UObjectTest,
    [](int num) -> bool
    {
        UE_LOG(LogTemp, Warning, TEXT("Num:%d"), num);
        return true;
    }
);

//UObjectTest->Destroy(); 如果UObject被销毁的话,不会打印num的数值

bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(50);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);
2.3.5 BindUObject

作用:绑定UObject的成员函数,执行调用IsBoundExecuteIfBound会检测该UObject的有效性,避免内存违规操作

//成员函数
bool ADelegateTest::PrintTest(int32 num)
{
	UE_LOG(LogTemp, Warning, TEXT("Num:%d"), num);
	return true;
}

//绑定和执行
MyTestDelegate.BindUObject(this, &ADelegateTest::PrintTest);
bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(50);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);

这个成员函数加不加UFUNCTION()都可以绑定

2.3.6 BindUFunction

作用:绑定UObject的UFUNCTION()成员函数,执行调用IsBoundExecuteIfBound会检测该UObject的有效性,避免内存违规操作

BindUFunction需要用到UE4的反射机制,因此回调函数需要用UFUNCTION()宏包住,否则UE无法通过FunctionName查找到对应的函数,会导致绑定失败

//函数定义
UFUNCTION()
bool PrintTestUFunction(int32 num)
{
	UE_LOG(LogTemp, Warning, TEXT("PrintTestUFunction Num:%d"), num);
	return true;
}

//绑定和执行
MyTestDelegate.BindUFunction(this, FName("PrintTestUFunction"));

bool RetValue = false;
if (MyTestDelegate.IsBound())
{
    RetValue = MyTestDelegate.Execute(50);
}
UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);

注意事项: 请注意FunctionName的内容格式

  • 正确FName("PrintTestUFunction")
  • 正确STATIC_FUNCTION_FNAME(TEXT("ADelegateTest::PrintTestUFunction"))
  • 错误FName("ADelegateTest::PrintTestUFunction")

格外小心FunctionName的内容是否正确,错误的格式或者没加 UFUNCTION() 宏都会使编辑器崩溃

2.3.7 BindSP

作用:绑定被UE共享引用所引用的普通C++对象的成员函数,解决BindRow可能触发的内存违规操作

我们这里新建一个函数用于测试Delegate绑定函数的有效性

UFUNCTION(BlueprintCallable)
void PrintAndTestDelegate(int32 num)
{
    bool RetValue = false;
	if (MyTestDelegate.IsBound())
	{
		RetValue = MyTestDelegate.Execute(num);
	}
	UE_LOG(LogTemp, Warning, TEXT("RetValue:%d"), RetValue);
}

在BeginPlay()中绑定执行Delegate并测试

void ADelegateTest::BeginPlay()
{
	Super::BeginPlay();

	TSharedRef objectSP = MakeShareable(new DelegateTestClass());
	MyTestDelegate.BindSP(objectSP, &DelegateTestClass::BindSPTest);

	PrintAndTestDelegate(10);
}

开始会调用一次PrintAndTestDelegate,然后等进入游戏之后,我们手动调用一次PrintAndTestDelegate

这里利用了UE 共享引用的功能,当执行完毕BeginPlay后,objectSP所引用的对象会被销毁,当第二次调用PrintAndTestDelegate理应得到 RetValue:0,说明BindSP可以执行调用IsBoundExecuteIfBound检测引用对象的有效性,避免内存违规操作

2.3.8 BindThreadSafeSP

与BindSP相比,只是使用了线程安全的共享引用而已

TSharedRef objectSP = MakeShareable(new DelegateTestClass());
MyTestDelegate.BindThreadSafeSP(objectSP, &DelegateTestClass::BindSPTest);

PrintAndTestDelegate(10);

2.4 解除绑定

MyTestDelegate.Unbind();

2.5 Create委托

还存在一系列CreateThreadSafeSPCreateStatic等,它们的使用方法和BindXXX大致相同。区别在于CreateXXX是static函数,且返回了一个Delegate,而不是直接赋值给 *this

可以观察BindXXX的原理,就是调用了CreateXXX罢了

template 
inline void BindThreadSafeSP(const TSharedRef& InUserObjectRef, typename TMemFunPtrType::Type InFunc, VarTypes... Vars)
{
    static_assert(!TIsConst::Value, "Attempting to bind a delegate with a const object pointer and non-const member function.");

    *this = CreateThreadSafeSP(InUserObjectRef, InFunc, Vars...);
}

若使用CreateXXX,这样做就等效于MyTestDelegate.BindThreadSafeSP(objectSP, &DelegateTestClass::BindSPTest)

MyTestDelegate = FTestDelegate::CreateThreadSafeSP(objectSP, &DelegateTestClass::BindSPTest);

3. Multicast(多播)

3.1 特点

  • 可以绑定多个函数指针
  • 无返回值
  • 支持最多4个payload参数
  • 最多8个函数参数
  • 不支持反射以及序列化,不能加UPROPERTY()

3.2 定义Delegate类型

ParamType后面没有ParamName,不然会报错 (动态委托ParamType后面必须跟着ParamName)

//无函数参数
DECLARE_MULTICAST_DELEGATE(FTestDelegate);

//2个函数参数
DECLARE_MULTICAST_DELEGATE_TwoParams(FTestDelegate, int32, FString);

3.3 绑定和执行委托

声明和定义委托及代理对象

DECLARE_MULTICAST_DELEGATE_TwoParams(FTestMulDelegate, int32, FString);

FTestMulDelegate MyMulDelegate;

使用方法和单播十分相似,下面列出区别点

  • 执行代理方式 ----> Broadcast
  • BindXXX ----> AddXXX
  • Broadcast会自动检测绑定的UObject或智能引用对象的有效性,再来执行代理。但像AddRawAddLambda这类绑定方式还是可能触发内存违规操作

新提供了一个Add API,查看其源码

FDelegateHandle Add(FDelegate&& InNewDelegate)
{
    FDelegateHandle Result;
    if (Super::GetDelegateInstanceProtectedHelper(InNewDelegate))
    {
        Result = Super::AddDelegateInstance(MoveTemp(InNewDelegate));
    }
    return Result;
}

发现需要传递一个FDelegate,继续跟踪其定义

/** Type definition for unicast delegate classes whose delegate instances are compatible with this delegate. */
using FDelegate = TDelegate;

发现FDelegate就是一个参数类型和这个多播相同的一个单播,从而推测出多播就是保存了多个单播,进而实现了可以绑定多个函数指针

而Add的使用方法就变得很简单了

//定义静态函数
static void StaticTestMul(int32 num, FString text)
{
    UE_LOG(LogTemp, Warning, TEXT("StaticTestMul Num:%d   text:%s"), num, *text);
}

//定义一个单播代理对象
FTestMulDelegate::FDelegate TempDelegate = FTestMulDelegate::FDelegate::CreateStatic(&ADelegateTest::StaticTestMul);
//Add
MyMulDelegate.Add(TempDelegate);

MyMulDelegate.Broadcast(20, FString("MyMulDelegateTest"));

查看AddXXX源码发现,就是通过Add来实现的

template 
inline FDelegateHandle AddStatic(typename TBaseStaticDelegateInstance::FFuncPtr InFunc, VarTypes... Vars)
{
    return Add(FDelegate::CreateStatic(InFunc, Vars...));
}

3.4 解除绑定

多播的AddXXX包括Add都会返回一个FDelegateHandle,这是一个句柄用于识别绑定在多播上的对象或者函数,可以通过它来解除绑定

3.4.1 Remove

作用:解除FDelegateHandle对应的函数绑定

FTestMulDelegate::FDelegate TempDelegate = FTestMulDelegate::FDelegate::CreateStatic(&ADelegateTest::StaticTestMul);
FDelegateHandle StaticHandle = MyMulDelegate.Add(TempDelegate);
MyMulDelegate.Remove(StaticHandle);
3.4.2 RemoveAll

作用:解除特定UObject对象所有的绑定,注意只能是UObject,普通C++对象不会被解除,比如下面这种情况,还是会打印信息

TSharedRef objectSP = MakeShareable(new DelegateTestClass());
MyMulDelegate.AddSP(objectSP, &DelegateTestClass::AddSPTest);
MyMulDelegate.RemoveAll(&objectSP);

MyMulDelegate.Broadcast(100, FString("MyMulDelegateTest"));

还需要注意一个地方,请看下面一段代码,预测结果,其中ADelegateTest::StaticTestMul是本类的一个static函数

//函数定义
static void StaticTestMul(int32 num, FString text)
{
    UE_LOG(LogTemp, Warning, TEXT("StaticTestMul Num:%d   text:%s"), num, *text);
}

MyMulDelegate.AddStatic(&ADelegateTest::StaticTestMul);
MyMulDelegate.RemoveAll(this);
MyMulDelegate.Broadcast(100, FString("MyMulDelegateTest"));

可能有些人会认为不会打印StaticTestMul Num:100 text:MyMulDelegateTest,static函数也属于该UObject,会被RemoveAll解除绑定。但是答案相反,会打印信息。类的static函数是属于类的,不是属于某个特定对象,RemoveAll不能解除类的绑定

3.4.3 Clear

作用:清除多播的所有绑定

MyMulDelegate..Clear();

3.5 其它API

这些API在单播中也存在,放在这里一起讲了

3.5.1 IsBound

作用:判断是否绑定了至少一个函数指针

MyMulDelegate.IsBound()
3.5.2 IsBoundToObject

作用:判断是否绑定了特定UObject对象的函数指针

MyMulDelegate.IsBoundToObject(this)

需要注意的是,和RemoveAll同理,绑定类的静态函数,不认为是绑定了该类的实例对象,如下面一段代码

MyMulDelegate.AddStatic(&ADelegateTest::StaticTestMul);
bool bBoundOb = MyMulDelegate.IsBoundToObject(this);
bool bBound = MyMulDelegate.IsBound();

UE_LOG(LogTemp, Warning, TEXT("bBoundOb:%d\tbBound:%d"), bBoundOb, bBound);

会打印 bBoundOb:0 bBound:1

其实按Delegate绑定static函数的原理来说,我的解答是错误的,大家不要在意,理解意思即可

4. Dynamic Unicast(动态单播)

单播和多播可以统称为静态委托,它们都不支持反射以及序列化。而接下来介绍的动态单播和动态多播都是动态委托,支持反射以及序列化。动态委托主要就是集成了UObject的反射系统,让其可以注册蓝图实现的函数及支持序列化存储到本机。当然也可以绑定原生C++函数代码,前提是这个函数被UFUNCTION标记(因为UFUNCTION标记过,这个函数就可以进行反射与序列化了)。通常情况动态委托执行速度比静态委托慢

同时需要注意动态委托不支持Payload,因此,绑定函数与代理对象的参数、返回值必须完全一致。

4.1 特点

  • 只能绑定一个函数指针,实现一对一通知
  • 支持返回值
  • 不支持payload参数
  • 最多8个函数参数
  • 支持反射以及序列化,可以加UPROPERTY(),但不能BlueprintAssignable

4.2 定义Delegate类型

动态委托ParamType后面必须跟着ParamName

//无返回值,无函数参数
DECLARE_DYNAMIC_DELEGATE(FTestDYDelegate);

//无返回值,2个函数参数
DECLARE_DYNAMIC_DELEGATE_TwoParams(FTestDYDelegate, int32, num, FString, text);

//有返回值,无函数参数
DECLARE_DYNAMIC_DELEGATE_RetVal(bool, FTestDYDelegate);

//有返回值,2个函数参数
DECLARE_DYNAMIC_DELEGATE_RetVal_TwoParams(bool, FTestDYDelegate, int32, num, FString, text);

4.3 绑定和执行委托

声明和定义委托

DECLARE_DYNAMIC_DELEGATE_RetVal_TwoParams(bool, FTestDYDelegate, int32, num, FString, text);

UPROPERTY(BlueprintReadWrite)
FTestDYDelegate MyDYDelegate;
4.3.1 BindUFunction

作用:绑定UObject的UFUNCTION函数

//函数声明和定义
UFUNCTION()
bool DYnamicPrintTest(int32 num, FString text)
{
    UE_LOG(LogTemp, Warning, TEXT("DYnamicPrintTest Num:%d   text:%s"), num, *text);
    return true;
}

MyDYDelegate.BindUFunction(this, FName("DYnamicPrintTest"));

if (MyDYDelegate.IsBound())
{
    MyDYDelegate.Execute(100, FString("MyDYDelegate Execute"));
}

注意必须加UFUNCTION()

4.3.2 BindDynamic宏

作用:绑定UObject的UFUNCTION函数

BindDynamic是UE给我们定义好的宏,就是根据函数指针解析函数名字

#define BindDynamic( UserObject, FuncName ) __Internal_BindDynamic( UserObject, FuncName, STATIC_FUNCTION_FNAME( TEXT( #FuncName ) ) )
//绑定和执行
MyDYDelegateTwo.BindDynamic(this, &ThisClass::DYnamicPrintTestTwo);

MyDYDelegate.BindDynamic(this, &ThisClass::DYnamicPrintTest);
if (MyDYDelegate.IsBound())
{
    MyDYDelegate.Execute(100, FString("MyDYDelegate Execute"));
}

4.4 解除绑定

MyDYDelegate.Clear();//提供给Dynamic Multicast的接口
MyDYDelegate.Unbind();

5. Dynamic Multicast(动态多播)

Dynamic Multicast(动态多播)与Dynamic Unicast(动态单播)相比最大的差别就是可以BlueprintAssignable,在蓝图中进行绑定和解绑操作

5.1 特点

  • 可以绑定多个函数指针
  • 不支持返回值
  • 不支持payload参数
  • 最多8个函数参数
  • 支持反射以及序列化,可以加UPROPERTY(),可以BlueprintAssignable

5.2 定义Delegate类型

//无函数参数
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FTestDYMulDelegate);

//1个函数参数
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTestDYMulDelegate, int32, num);

5.3 绑定和执行委托

在介绍绑定之前,需要注意一件事情:UFUNCTION函数被重复绑定情况

  • 动态多播:重复绑定会出现运行时错误
  • 静态多播:只用AddUObject绑定UFUNCTION标记的函数,最后函数会多次执行(不会报错),而一旦使用AddUFunction编辑器会直接崩溃

声明和定义委托及代理对象

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTestDYMulDelegate, int32, num);

UPROPERTY(BlueprintAssignable)
FTestDYMulDelegate MyDYMulDelegate;
5.3.1 Add

作用:根据FScriptDelegate对象添加绑定

在使用之前,首先回忆静态多播的使用方法,发现动态多播内也存在FTestDYMulDelegate::FDelegate变量,这个变量存放的是和动态多播变量参数一样的动态单播,但是动态单播不存在CreatXXX这种静态函数可以创建并返回一个动态单播对象实例,所以这种思路是不可行的。

我们查看Add源码,发现传入的参数也不是FTestDYMulDelegate::FDelegate类型的变量,而是TScriptDelegate类变量。

void Add( const TScriptDelegate& InDelegate )
{
    // First check for any objects that may have expired
    CompactInvocationList();

    // Add the delegate
    AddInternal( InDelegate );
}

TScriptDelegate类的默认成员有两个,一个是UObject的弱引用智能指针,一个是FunctionName即我们要绑定的UFUNCTION函数名字
,我们就可以通过它的BindUFunction绑定UObject的UFUNCTION函数了

void BindUFunction( UObject* InObject, const FName& InFunctionName )
{
    Object = InObject;
    FunctionName = InFunctionName;
}

所以使用Add的示例如下:

//函数定义
UFUNCTION()
void DynamicMulPrint(int32 num)
{
    UE_LOG(LogTemp, Warning, TEXT("DynamicMulPrnt Num:%d"), num);
}

TScriptDelegate<> TempDyMulDelegate;
TempDyMulDelegate.BindUFunction(this, FName("DynamicMulPrint"));

MyDYMulDelegate.Add(TempDyMulDelegate);
MyDYMulDelegate.Add(TempDyMulDelegate);//重复绑定,运行时报错
MyDYMulDelegate.Broadcast(55);

可以使用FScriptDelegate替换TScriptDelegate<>,UE帮我们进行了typedef TScriptDelegate<> FScriptDelegate;

注意事项:

  • 编译时不会对FScriptDelegate绑定的函数指针类型进行检查,也就是说如果FScriptDelegate对象绑定的函数指针参数与动态多播类型不一致是不会报错的
  • 经测试动态多播会把参数依次填充到绑定的函数上面,如果出现参数类型不一致Editor就会崩溃,若一致不会崩溃,但若出现空指针引用依然会导致Editor崩溃,若绑定的函数有返回值不会导致Editor崩溃。
  • 重复绑定UFUNCTION函数会导致运行时错误
  • 综上,不推荐使用Add对动态多播进行绑定,若FunctionName输入错误,会导致不确定因素发生
  • FScriptDelegate不存在__Internal_BindDynamic成员,所以也不能用BindDynamic进行绑定
5.3.2 AddUnique

作用:根据FScriptDelegate对象添加绑定,但会检测是否重复

UFUNCTION()
void DynamicMulPrint(int32 num)
{
    UE_LOG(LogTemp, Warning, TEXT("DynamicMulPrnt Num:%d"), num);
}

TScriptDelegate<> TempDyMulDelegate;
TempDyMulDelegate.BindUFunction(this, FName("DynamicMulPrint"));

MyDYMulDelegate.Add(TempDyMulDelegate);
MyDYMulDelegate.AddUnique(TempDyMulDelegate);//检测到重复绑定,不执行绑定
MyDYMulDelegate.Broadcast(55);

注意事项: 使用AddUnique,当FScriptDelegate对象绑定的函数指针参数与动态多播类型不一致时,Broadcast会使编辑器直接崩溃。若FScriptDelegate对象绑定的函数指针有返回值,不会使编辑器崩溃

5.3.3 AddDynamic宏

作用:根据函数指针和UObject添加绑定

MyDYMulDelegate.AddDynamic(this, &ThisClass::DynamicMulPrint);
MyDYMulDelegate.AddDynamic(this, &ThisClass::DynamicMulPrint);//重复绑定,运行时错误

AddDynamic在编译时就会检测函数指针参数类型是否一致,避免出错,推荐使用(确保不重复绑定下)

5.3.4 AddUniqueDynamic宏

作用:根据函数指针和UObject添加绑定,但会检测是否重复

MyDYMulDelegate.AddDynamic(this, &ThisClass::DynamicMulPrint);
MyDYMulDelegate.AddUniqueDynamic(this, &ThisClass::DynamicMulPrint);//检测到重复绑定,不执行绑定
5.3.5 蓝图

动态多播的最大好处就是可以在蓝图中进行绑定和解绑:
[UE C++] Delegate使用详解_第1张图片

5.4 解除绑定

5.4.1 Remove

拥有两个重载:根据FScriptDelegate对象或者FunctionName解除绑定

//通过Object和FunctionName解除绑定
void Remove( const UObject* InObject, FName InFunctionName );

//通过FScriptDelegate对象解除绑定
void Remove( const TScriptDelegate& InDelegate )

示例

MyDYMulDelegate.AddDynamic(this, &ThisClass::DynamicMulPrint);
MyDYMulDelegate.Remove(this, FName("DynamicMulPrint"));
FScriptDelegate TempDyMulDelegate;
TempDyMulDelegate.BindUFunction(this, FName("DynamicMulPrint"));
MyDYMulDelegate.Add(TempDyMulDelegate);

MyDYMulDelegate.Remove(TempDyMulDelegate);

Remove输入的FunctionName无效不会触发error

5.4.2 RemoveDynamic宏

作用:根据UObject和函数指针解除绑定

MyDYMulDelegate.RemoveDynamic(this, &ThisClass::DynamicMulPrint);
5.4.3 RemoveAll

作用:解除特定UObject的所有绑定

MyDYMulDelegate.RemoveAll(this);
5.4.4 Clear

作用:解除动态多播的所有绑定

MyDYMulDelegate.Clear();
5.4.5 IsAlreadyBound宏

作用:判断特定UObject的函数指针是否绑定

MyDYMulDelegate.AddDynamic(this, &ThisClass::DynamicMulPrint);
bool bIsBound = MyDYMulDelegate.IsAlreadyBound(this, &ThisClass::DynamicMulPrint);//true=1

UE_LOG(LogTemp, Warning, TEXT("%d"), bIsBound);
5.4.6 Contains

作用:根据FScriptDelegate对象或者FunctionName判断是否绑定

FScriptDelegate TempDyMulDelegate;
TempDyMulDelegate.BindUFunction(this, FName("DynamicMulPrint"));
MyDYMulDelegate.Add(TempDyMulDelegate);

bool bIsBound = MyDYMulDelegate.Contains(TempDyMulDelegate);
UE_LOG(LogTemp, Warning, TEXT("%d"), bIsBound);
MyDYMulDelegate.AddDynamic(this, &ThisClass::DynamicMulPrint);
bool bIsBound = MyDYMulDelegate.Contains(this, FName("DynamicMulPrint"));
UE_LOG(LogTemp, Warning, TEXT("%d"), bIsBound);

6. Events(事件)

Events(事件)本质上是一个静态多播委托,但只有声明事件的类可以调用事件 的 BroadcastIsBoundClear 函数。这意味着事件对象可在公共接口中公开,而无需让外部类访问这些敏感度函数。

事件使用情况有:

  • 在纯抽象类中包含回调
  • 限制外部类调用 Broadcast、IsBound 和 Clear 函数。

6.1 特点

和静态多播一样

  • 可以绑定多个函数指针
  • 无返回值
  • 支持最多4个payload参数
  • 最多8个函数参数
  • 不支持反射以及序列化,不能加UPROPERTY()

6.2 定义简单的Event

Event的声明和静态多播委托声明方式几乎相同,唯一的区别是它们使用Event特有的宏变体

//不带参数
DECLARE_EVENT(ADelegateTest, FTestEvent);
//1个参数
DECLARE_EVENT_OneParam(ADelegateTest, FTestEvent, int32);

注意事项:

  • DECLARE_EVENT宏的首个参数是"拥有"此Event的类,因此它可调用 Broadcast() 函数
  • 为什么宏可以限制外部类访问敏感函数,原因在于UE会把宏的首个参数声明为Event的友元类,只有友元类可以访问private成员
    class FTestEvent : public TBaseMulticastDelegate
    {
        friend class ADelegateTest;
    }
    
  • 一般会把Event的宏类型定义放在类中(即访问该类型时需要带上所在类的名称前缀,如ADelegateTest::FTestEvent),这样可以有效减少Event类型名称冲突
  • 同时将Event设为private,防止类外直接访问到,起到安全作用
  • Event的访问器应该依照 OnXXX 模式,而非常规的 GetXXX 模式
  • 调用 Broadcast() 函数时,被绑定函数的执行顺序尚未定义。有可能不按照函数的添加顺序执行

一个简单的Event定义如下:

UCLASS()
class STUDY427TEST_API ADelegateTest : public AActor
{
	GENERATED_BODY()
	
public:	

	DECLARE_EVENT_OneParam(ADelegateTest, FTestEvent, int32);

	FTestEvent& OnTestEvent()
	{
		return MyTestEvent;
	}

private:

	FTestEvent MyTestEvent;
}

6.3 Event的抽象继承定义

基础类

/** Register/Unregister a callback for when assets are added to the registry */
DECLARE_EVENT_OneParam( IAssetRegistry, FAssetAddedEvent, const FAssetData&);
virtual FAseetAddedEvent& OnAssetAdded() = 0;

派生类

DECLARE_DERIVED_EVENT( FAssetRegistry, IAssetRegistry::FAssetAddedEvent, FAssetAddedEvent);
virtual FassetAddedEvent& OnAssetAdded() override { return AssetAddedEvent; }

注意事项: 在派生类中声明一个派生事件时,不要在 DECLARE_DERIVED_EVENT 宏中重复函数签名(参数const FAssetData&)。此外,DECLARE_DERIVED_EVENT 宏的最后一个参数是事件的新命名,通常与基础类型相同。

6.4 Event的继承访问方式

友元关系不会继承,允许派生类Broadcast其事件的基础类需要将公开Broadcast事件的封装函数设为protected

基础类:

public:
    /** Broadcasts whenever the layer changes */
    DECLARE_EVENT( FLayerViewModel, FChangedEvent )
    FChangedEvent& OnChanged() { return ChangedEvent; }

protected:
    void BroadcastChanged()
    {
        ChangedEvent.Broadcast();
    }

private:
    /** Broadcasts whenever the layer changes */
    FChangedEvent ChangedEvent;

6.5 Event使用

Event的绑定方法,解除绑定方法都和静态多播委托一样,这里就不举例子了。只需要注意一点,只有Event的友元类才可以直接进行 BroadcastIsBoundClear 等函数

最后

Delegate的使用方法总结终于告一段落,文章比较长,若有错误,欢迎指出。学疏才浅,还请见谅

参考资料

  • 一文理解透UE委托Delegate
  • UE4-深入委托Delegate实现原理
  • UE4 delegate初探
  • 委托-官方文档

你可能感兴趣的:(UE,C++,基础内容,c++,ue4,ue5)