委托
简单地说,委托就像是一个“函数指针”,但它更加安全和灵活。它允许程序在运行时动态地调用不同的函数。
(1)解耦对象间的关联
委托允许对象之间以松散耦合的方式进行通信。通过委托,一个对象可以在不直接引用另一个对象的情况下,通知其执行特定的操作。这有助于降低对象之间的依赖性和耦合度,从而提高代码的灵活性和可维护性。
(2)事件驱动编程
委托是实现事件驱动编程的关键机制之一。在事件驱动编程中,对象的行为是基于事件的发生来触发的。委托允许对象在事件发生时通知其他对象,并允许这些对象对事件做出响应。这种机制使得代码更加模块化,并易于扩展和维护。
(3)泛型且类型安全
委托提供了一种泛型但类型安全的方式,在C++对象上调用成员函数。通过委托,你可以动态地绑定到任意对象的成员函数,即使调用程序不知道对象的具体类型也可以进行操作。这增加了代码的灵活性和可重用性,同时保证了类型安全。
(4)异步通信
委托还支持异步通信。在虚幻引擎中,许多操作可能需要花费一些时间才能完成,如加载资源、执行物理模拟等。通过使用委托,你可以在不阻塞主线程的情况下,通知其他对象在异步操作完成后执行特定的操作。这有助于提高应用程序的响应性和性能。
(5)广播和多播
虚幻引擎中的委托还支持广播和多播机制。广播允许一个委托通知所有绑定的对象,而多播则允许一个委托通知多个指定的对象。这种机制使得在多个对象之间传递消息变得更加简单和高效。
在虚幻引擎的应用场景中,选择哪种通讯模式取决于具体的需求和场景。例如,在需要向多个玩家同步游戏状态时,多播(组播)可能是一个更好的选择;而在需要向所有玩家广播游戏开始或结束等全局信息时,广播则更为合适。(具体后面再详讲)
(6)蓝图可视化编程支持
在虚幻引擎的蓝图可视化编程环境中,委托也扮演着重要的角色。通过委托,蓝图脚本可以轻松地与C++代码进行交互,从而实现更加复杂和灵活的游戏逻辑。此外,蓝图还支持动态多播委托的声明和使用,这使得在蓝图中处理事件和消息变得更加方便。
(7)复制和安全性
委托对象在复制时是很安全的。你可以通过值或引用来传递委托,但需要注意的是,通过值传递需要在堆上分配内存,这通常不是最佳实践。通过引用传递则更加高效且安全。
但传递引用,在异步的情况下会涉及到生命周期问题,容易崩溃,虚幻的委托大部分时候拷贝是安全的,因为它会存一个执行委托对象的弱引用,如果这对象消亡了,那么这个委托被调用的时候就不会执行。
委托的大致使用流程
- 声明委托类型
- 定义委托类型变量
- 通过委托变量绑定委托函数
- 执行委托
- 解绑委托函数(可根据自身情况而实施)
1.单播
先创建一个继承于Actor的类,并在其头文件包含头文件(此处文件命名为LearnSingleDelegateActor)
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "LearnSingleDelegateActor.generated.h"
并在源文件添加GameplayStatics.h头文件
#include"Kismet/GameplayStatics.h"
(1)声明委托类型
DECLARE_DELEGATE(FLearnSingleDelegatePrintLocation);
DECLARE_DELEGATE_RetVal_OneParam(FVector,FLearnSingleDelegateGetLocation,FString);
//DECLARE_DELEGATE_RetVal_OneParam(ReturnType, DelegateName, ParamType);
//ReturnType:委托将要返回的值的类型。
//DelegateName:委托的名称,这个名称将用于在代码中引用这个委托类型。
//ParamType:委托将要接受的参数的类型。
如需声明委托,请使用下文所述的宏。请根据与委托相绑定的函数(或多个函数)的函数签名来选择宏。每个宏都为新的委托类型名称、函数返回类型(如果不是 void
函数)及其参数提供了参数。当前,支持以下使用任意组合的委托签名:
- 返回一个值的函数。
- 声明为常函数。
- 最多4个"载荷"变量。
- 最多8个函数参数。
注意:委托函数支持与UFunctions相同的说明符,但使用 UDELEGATE
宏而不是 UFUNCTION
。
(2)定义委托类型变量
在ALearnSingleDelegateActor类内定义变量
public:
FLearnSingleDelegatePrintLocation SingleDelegatePrintLocation;
FLearnSingleDelegateGetLocation SingleDelegateGetLocation;
(3)通过委托变量绑定委托函数
为方便在ALearnSingleDelegateActor类头文件再创建一个类,此处命名为LearnLocationActor(继承于Actor)
UCLASS()
class ALearnLocationActor : public AActor
{
GENERATED_BODY()
public:
ALearnLocationActor();
protected:
virtual void BeginPlay() override;
void PrintLocation();
FVector GetLocation(FString InStr);//此处类型看委托声明处,与其相对应.
};
在此默认函数中创建根组件来便于观察委托操作 ,并对其他函数进行定义
ALearnLocationActor::ALearnLocationActor()
{
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("LocationRoot"));
}
void ALearnLocationActor::PrintLocation()
{
FVector MyLocation = GetActorLocation();
UE_LOG(LogTemp, Warning, TEXT("[%s]__PrintMyLocation:[%s]"), *FString(__FUNCTION__), *MyLocation.ToString());//用于被绑定时观察步骤实现
}
FVector ALearnLocationActor::GetLocation(FString InStr)
{
FVector MyLocation = GetActorLocation();
UE_LOG(LogTemp, Warning, TEXT("[%s]__GetMyLocation:[%s]"), *FString(__FUNCTION__), *MyLocation.ToString());
return MyLocation;
}
此处绑定函数
void ALearnLocationActor::BeginPlay()
{
AActor *ActorPtr = UGameplayStatics::GetActorOfClass(this, ALearnSingleDelegateActor::StaticClass());
if (ALearnSingleDelegateActor* SingleDelegateActorPtr=Cast<ALearnSingleDelegateActor>(ActorPtr))
{
//通过SingleDelegatePrintLocation此委托变量使用BindUObject函数模板绑定委托并调用一次绑定函数
SingleDelegateActorPtr->SingleDelegatePrintLocation.BindUObject(this,&ALearnLocationActor::PrintLocation);
SingleDelegateActorPtr->SingleDelegateGetLocation.BindUObject(this, &ALearnLocationActor::GetLocation);
}
}
官方模板函数
(4) 执行委托
为了观察实现步骤,我在LearnSingleDelegateActor类声明函数暴露给蓝图
UFUNCTION(BlueprintCallable, Category = "Learn")
void CallLocationActorPrint();
UFUNCTION(BlueprintCallable, Category = "Learn")
void CallLocationActorGet();
并对其进行定义
void ALearnSingleDelegateActor::CallLocationActorPrint()
{
//第一种写法
if (SingleDelegatePrintLocation.IsBound())
{
SingleDelegatePrintLocation.Execute();
//SingleDelegatePrintLocation.Unbind();解绑操作
}
//第二种写法(建议用此写法)
SingleDelegatePrintLocation.ExecuteIfBound();
}
void ALearnSingleDelegateActor::CallLocationActorGet()
{
FVector MyRecetivedLocation = SingleDelegateGetLocation.Execute(TEXT("My Single Delegate Actor"));
UE_LOG(LogTemp, Warning, TEXT("[MyRecetivedLocation]__MyLocation:[%s]"), *MyRecetivedLocation.ToString());
}
注意:对于无返回值的委托,可调用ExecuteIfBound()函数,但需注意输出参数可能未初始化。
(5)观察实现步骤(只做演示)
1、将两个类拖入场景中
2、在关卡蓝图处使用函数(此处是使用键盘1、2分别去激活函数)
3、启动关卡,并在输出日志观察
2.多播
多播委托拥有大部分与单播委托相同的功能。它们只拥有对对象的弱引用,可以与结构体一起使用,可以四处轻松复制等等。就像常规委托一样,多播委托可以远程加载/保存和触发;但多播委托函数不能使用返回值。它们最适合用来 四处轻松传递一组委托。
多播使用方法与单播使用方法大致一样,此处就不一一细讲,只讲区别之处。
声明多播委托类型
绑定多播委托
多播委托可以绑定多个函数,当委托触发时,将调用所有这些函数。因此,绑定函数在语义上与数组更加类似。
Remove():
执行委托
多播委托允许您附加多个函数委托,然后通过调用多播委托的"Broadcast()"函数一次性同时执行它们。多播委托签名不得使用返回值。
在多播委托上调用"Broadcast()"总是安全的,即使是在没有任何绑定时也是如此。唯一需要注意的是,如果您使用委托来初始化输出变量,通常会带来非常不利的后果。
调用"Broadcast()"时绑定函数的执行顺序尚未定义。执行顺序可能与函数的添加顺序不相同。
动态委托
与静态委托区别
声明与定义
- 静态委托:通常使用固定的宏来声明,例如
DECLARE_DELEGATE
,并且它们不需要在运行时动态地解析函数地址。静态委托的绑定通常是编译时确定的。 - 动态委托:声明时通常带有
_DYNAMIC
后缀,例如DECLARE_DYNAMIC_DELEGATE
。动态委托需要在运行时动态地解析函数地址,因此它们支持更多的灵活性,例如可以暴露给蓝图进行绑定。
绑定与解绑
- 静态委托:绑定函数时,通常使用
Bind
系列函数,如BindStatic
、BindUObject
等。解绑函数则使用Unbind
。 - 动态委托:绑定函数时,对于单播委托使用
BindDynamic
,对于多播委托使用AddDynamic
。解绑函数则使用RemoveDynamic
或RemoveAllDynamic
。
序列化与蓝图支持
- 静态委托:不支持序列化,因此不能通过蓝图进行绑定。
- 动态委托:支持序列化,因此可以被蓝图绑定。通过
UPROPERTY(BlueprintAssignable)
宏,动态多播委托可以在蓝图中被引用和绑定。
执行速度
- 静态委托:由于函数地址在编译时就已经确定,因此执行速度通常比动态委托快。
- 动态委托:需要在运行时解析函数地址,因此执行速度相对较慢。
返回值与参数
- 静态委托和动态委托:都支持多参数传入,并且引擎支持最多9个参数。但是,只有单播委托和动态单播委托可以有返回值,多播委托不支持返回值。
使用场景
- 静态委托:适用于那些函数绑定在编译时就已经确定,且不需要在运行时动态更改的场景。
- 动态委托:适用于需要在运行时动态绑定和解绑函数,或者需要将委托暴露给蓝图进行绑定的场景。
动态委托绑定宏
执行动态委托
3.动态单播
注意:动态委托不支持载荷!
先创建一个继承于Actor的类,此处命名为LearnDynamicSingleActor
(1)声明委托类型
DECLARE_DYNAMIC_DELEGATE_RetVal_OneParam(int32,FLearnDynamicRetOne,FString, InName);
//用于创建一个返回值为 int32 类型,并且接受一个 FString 类型参数的委托。
(2)定义委托类型变量
FLearnDynamicRetOne LearnDynamicRetOneDelegate;
(3)绑定、调用委托
//绑定
UFUNCTION(BlueprintCallable, Category = "Learn")
void InitLearnDynamicOneDelegate(FLearnDynamicRetOne InDelegate);
//调用
UFUNCTION(BlueprintCallable, Category = "Learn")
void CallLearnDynamicOneDelegate(FString InStr);
//释放
UFUNCTION(BlueprintCallable, Category = "Learn")
void ReleaseLearnDynamicOneDelegate();
对函数进行定义
void ALearnDynamicSingleActor::InitLearnDynamicOneDelegate(FLearnDynamicRetOne InDelegate)
{
LearnDynamicRetOneDelegate = InDelegate;
}
void ALearnDynamicSingleActor::CallLearnDynamicOneDelegate(FString InStr)
{
LearnDynamicRetOneDelegate.Execute(InStr);
}
void ALearnDynamicSingleActor::ReleaseLearnDynamicOneDelegate()
{
LearnDynamicRetOneDelegate.Clear();
}
在编辑器进行绑定事件(注意观察蓝图函数接口与代码之间的命名)
创建LearnDynamicSingleActor子类蓝图并拖入关卡中
对此子类蓝图进行绑定事件
此处99这个Value值是返回值int32,与委托声明类型有关
最后在关卡蓝图进行调用演示(与单播调用方法一样,此处就不详讲)
4.动态多播
先创建一个继承于Actor的类,此处命名为LearnDynamicMulityDelegateActor
(1)声明委托类型
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FLearnDynamicMultiThree, FString, InName, int32, InHealth, int32, InMana);
(2)定义委托类型变量
在LearnDynamicMulityDelegateActor类中定义变量,此变量用来在蓝图进行动态委托
UPROPERTY(BlueprintAssignable)
FLearnDynamicMultiThree LearnDynamicMultiThreeRight;
(3)绑定、调用委托
//执行函数
UFUNCTION(BlueprintCallable, Category = "Learn")
void CallExcuteDynamicMultiThreeRight(FString InName, int32 InHealth, int32 InMana);
//被绑定函数
UFUNCTION()
void WorkDynamicNative(FString InName, int32 InHealth, int32 InMana);
void ALearnDynamicMulityDelegateActor::CallExcuteDynamicMultiThreeRight(FString InName, int32 InHealth, int32 InMana)
{
//使用广播进行演示
LearnDynamicMultiThreeRight.Broadcast(InName, InHealth, InMana);
}
void ALearnDynamicMulityDelegateActor::WorkDynamicNative(FString InName, int32 InHealth, int32 InMana)
{
UE_LOG(LogTemp, Warning, TEXT("WorkDynamicNative"));
}
// Called when the game starts or when spawned
void ALearnDynamicMulityDelegateActor::BeginPlay()
{
Super::BeginPlay();
//绑定函数
LearnDynamicMultiThreeRight.AddDynamic(this, &ALearnDynamicMulityDelegateActor::WorkDynamicNative);
}
(4)编辑器演示
创建LearnDynamicMulityDelegateActor的子类蓝图, 并绑定事件(此处是In Name输出)
在关卡蓝图进行绑定并调用输出In Health
注意观察输出日志和显示的数字,
由此可见在关卡蓝图中绑定事件输出In Health字符串执行一次后就解绑,在第二次调用没有输出次字符串,而In Name是绑定在LearnDynamicMulityDelegateActor的子类蓝图上的,且没有解绑,所以可以重复调用,对于In Mana,因为没有绑定事件中没有对此参数的调用,所以没有显示变化。