学习虚幻C++开发日志——委托

委托

官方文档:Delegates and Lamba Functions in Unreal Engine | 虚幻引擎 5.5 文档 | Epic Developer Community | Epic Developer Community

简单地说,委托就像是一个“函数指针”,但它更加安全和灵活。它允许程序在运行时动态地调用不同的函数。

(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.多播

官方文档:Multicast Delegates in Unreal Engine | 虚幻引擎 5.5 文档 | Epic Developer Community | Epic Developer Community

多播委托拥有大部分与单播委托相同的功能。它们只拥有对对象的弱引用,可以与结构体一起使用,可以四处轻松复制等等。就像常规委托一样,多播委托可以远程加载/保存和触发但多播委托函数不能使用返回值。它们最适合用来 四处轻松传递一组委托。

多播使用方法与单播使用方法大致一样,此处就不一一细讲,只讲区别之处。

声明多播委托类型

绑定多播委托

多播委托可以绑定多个函数,当委托触发时,将调用所有这些函数。因此,绑定函数在语义上与数组更加类似。

 

Remove():

 执行委托 

多播委托允许您附加多个函数委托,然后通过调用多播委托的"Broadcast()"函数一次性同时执行它们。多播委托签名不得使用返回值。

在多播委托上调用"Broadcast()"总是安全的,即使是在没有任何绑定时也是如此。唯一需要注意的是,如果您使用委托来初始化输出变量,通常会带来非常不利的后果。

调用"Broadcast()"时绑定函数的执行顺序尚未定义。执行顺序可能与函数的添加顺序不相同。

 

动态委托

与静态委托区别

声明与定义

  • 静态委托:通常使用固定的宏来声明,例如DECLARE_DELEGATE,并且它们不需要在运行时动态地解析函数地址。静态委托的绑定通常是编译时确定的。
  • 动态委托:声明时通常带有_DYNAMIC后缀,例如DECLARE_DYNAMIC_DELEGATE。动态委托需要在运行时动态地解析函数地址,因此它们支持更多的灵活性,例如可以暴露给蓝图进行绑定。

绑定与解绑

  • 静态委托:绑定函数时,通常使用Bind系列函数,如BindStaticBindUObject等。解绑函数则使用Unbind
  • 动态委托:绑定函数时,对于单播委托使用BindDynamic,对于多播委托使用AddDynamic。解绑函数则使用RemoveDynamicRemoveAllDynamic

序列化与蓝图支持

  • 静态委托:不支持序列化,因此不能通过蓝图进行绑定。
  • 动态委托:支持序列化,因此可以被蓝图绑定。通过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,因为没有绑定事件中没有对此参数的调用,所以没有显示变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值