UE 中的 Delegate(代理)
搬运翻译,原文:https://benui.ca/unreal/delegates-intro/
代理在写事件驱动的代码时非常有用。从概念上讲他们相对直白:允许函数去订阅到一个“代理”,并且当代理被调用是,所有这些函数也被调用!就像一个发邮件列表。希望这样说有所帮助,因为在 Unreal 中的实现有一些复杂。
首先让我们想一个常用的代理:当玩家状态中的某些东西改变时通知 UI。例如,当玩家的分数改变时,我们想要 UI 去更新显示新分数。这更具体的看起来应该是什么样呢?
- 我们添加一个新的代理变量到 APlayerState 类,我们称之为 OnScoreChangedDelegate。
- 其他事务可以订阅到一个 APlayerState 实例的 OnScoreChangedDelegate。订阅意思是告诉代理一个方法去调用,当它被执行时。
- 当 APlayerState 改变它的分数时,它可以执行或调用OnScoreChangedDelegate。
- 任何已订阅的人会被通知!
在继续之前,确保这些概念对你是有意义的。这些概念应该给你一个锚点,伴随我们一步步让它在 Unreal 中运行起来。
总览
在 Unreal 中,建立和使用代理有四步。
- 声明代理的签名:就像一个方法,你的代理需要什么参数?它是否有一个返回类型?
- 创建一个你的代理的变量:这些是你的代理的实例,其他的方法可以订阅到其上。
- 订阅到代理:你需要关联当代理被调用时 你希望被调用的任何方法。
- 执行代理:已订阅的方法会被调用
在这个基础的教程中,我们会创建一个 动态多播代理。它是最通用的代理,希望能让你感受到什么是代理,在继续学习高级代理教程之前。
一个动态多播代理是:
- 动态:从目的来讲,这仅意味着它和蓝图兼容。
- 多播:超过一个的方法可以同时订阅到代理。
1. 声明一个代理
第一步是想好希望被代理调用的方法的签名。你想传什么信息到这个方法?
返回我们的分数例子,我们可能至少需要玩家的分数 int32 NewScore
。我们之后会用到更多参数。所以一个被我们的代理调用的函数可能看起来像这样:
void OnScoreChanged(int32 NewScore);
为了声明一个代理类型,我们需要使用 DECLARE...DELEGATE
宏。有许多不同类型但是我们只关注动态多播代理。
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewScore);
信息比较多,这些是在做什么?让我们把它分解下:
DECLARE_DYNAMIC_MULTICAST_DELEGATE | OK,我们声明了一个新的动态多播代理。这说得通! |
OneParam | 代理去调用的函数只有一个参数。注意是单数的 OneParam, 不是 OneParams。 |
FOnScoreChangedSignature | 这是我们的新代理类型的名字。Unreal 中的标准前缀是 F,并且我喜欢加上 Signature 后缀 |
int32, NewScore | 我们的参数类型是 int32 名字是 NewScore。注意在我们的第一个参数类型和它的名字之间有逗号 |
我们可以把这个声明放到增加我们代理实例的地方。对于我们的分数例子来说,在我们的 APlayerState 子类的头文件的顶部某处。如果不清楚的话看下一步。
增加另一个参数
这不是是完全可选的,但是如果我们有一个多玩家游戏,并且我们需要知道那个玩家的分数发生了变化?我们还可以将 APlayerState* PlayerState 提供给委托调用的函数:
void OnScoreChanged(int32 NewScore, APlayerState* OwningPlayer);
我们的新的代理声明看起来将会是:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnScoreChangedSignature, int32, NewScore, APlayerState*, OwningPlayer);
注意这个宏是 TwoParams 而不是 OneParam。
不过我们现在还是暂时继续我们的例子,用一个参数。
2. 创建委托类型的变量
我们已经声明了委托的签名,提供给被调用的函数的参数。我们现在需要添加代理到一个类或结构体的某处,这样其他想被通知的人才可以订阅到他们。起什么名字随你喜欢,但是我喜欢我的代理都有 Delegate 后缀。
继续我们的玩家分数和 UI 的例子,让我们把新的 OnScoreChangedDelegate 添加到 APlayerState 子类。
// BUIPlayerState.h
#pragma once
#include "GameFramework/PlayerState.h"
#include "BUIPlayerState.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewScore);
UCLASS()
class ABUIPlayerState : public APlayerState
{
GENERATED_BODY()
public:
// We want this public so our UI can access it to subscribe to it
// Also adding BlueprintAssignable makes it accessible by blueprints
UPROPERTY(BlueprintAssignable)
FOnScoreChangedSignature OnScoreChangedDelegate;
};
3. 订阅到代理
现在我们已经声明了代理,并在我们的已有代码中创建了一个实例,现在可以关联一个或多个方法到他们,这样一来这些方法会在代理被调用时调用。
有几种不同的方法去订阅到代理,但是这个教程中我们会使用最直接的一个方法,AddUniqueDynamic。
// BUIPlayerScoreWidget.cpp
#include "BUIPlayerScoreWidget.h"
#include "BUIPlayerState.h"
void UBUIPlayerScoreWidget::Initialize()
{
ABUIPlayerState* PlayerState = GetOwningPlayerState<ABUIPlayerState>();
PlayerState->OnScoreChangedDelegate.AddUniqueDynamic(this,
&UBUIPlayerScoreWidget::OnScoreChanged);
}
void UBUIPlayerScoreWidget::OnScoreChanged(int32 NewScore)
{
// Update the state of the UI
}
4. 调用代理
有了之前的步骤后,这一步会相对简单。我们只需要在代理上调用 Boradcase,并给它提供参数。
在我们的分数例子中,看起来会是这样:
// BUIPlayerState.cpp
#include "BUIPlayerState.h"
void ABUIPlayerState::AddPoints(int32 Points)
{
PlayerScore += Points;
OnScoreChangedDelegate.Broadcast(PlayerScore);
}
任何已订阅到代理的函数,现在应该会在代理广播时执行。你可以在 OnScoreChanged 函数出打断点来验证(或者 print 些日志看)。
结语
已经说了不少内容了!代理在概念上很简单,”调用一个方法并且所有的订阅者会被通知“,但是在 Unreal 中有些复杂。
当你熟悉了使用动态多播代理后,可以移步到高级代理教程!在那边我们会说到:
- 更多的代理类型:单一代理,非动态代理
- 增加返回值
- 事件
C++ 中的高级代理
搬运翻译,原文:https://benui.ca/unreal/delegates-advanced/
这是 Unreal 引擎代理系列的第二部分。在第一部分中,我们讲解了如何定义并使用一个动态多播代理。如果你刚了解代理那么先去看下那部分吧!
在前一个教程中,我们使用了一个玩家分数变化通知 UI 的例子。我们将会在这个教程中继续使用。
就像之前,我们将看下在 Unreal 中建立使用一个代理需要的四步骤,但这次我们将会讨论更多可供的选择和他们之间的差别。
- 定义代理的签名:就像一个函数,你的代理需要什么参数?它是否需要一个返回值?
- 创建你的新代理的变量:这些是你的代理的实例,其他方法可以订阅到它们。
- 订阅到代理:你需要链接任何你需要在代理被调用时被调用的方法。
- 执行代理:任何订阅的方法将会被调用
1. 声明一个代理
选择一个代理类型
在之前的教程中我们仅使用了一个动态多播代理,但是 Unreal 其实有四种不同类型的代理:
- 单播
- 多播
- 动态单播
- 动态多播
选择哪个取决于你希望用你的代理做什么,下面这个表可以看出哪个最适合你
Single | Multicast | Dynamic Single | Dynamic Multicast | |
---|---|---|---|---|
多少可以订阅? | 一个 | 许多 | 一个 | 许多 |
是否可以从蓝图使用? | 否 | 否 | 是 | 是 |
性能 | - | - | 稍慢? | 稍慢? |
在下一小节,注意声明动态和非动态的语法是不同的。
代理签名工具 (一个原文作者写的网页工具来生成代理签名)
非动态代理语法
非动态代理的声明是宏开头的,然后跟着你定义的代理签名的名字。UE 代码仓使用前缀 F 代表代理签名。我喜欢加一个 Signature 后缀来区分代理签名和代理实例变量。
// 可以声明一个无参的代理
DECLARE_DELEGATE(FOnScoreChangedSignature);
// 匹配函数: void OnScoreChanged();
// 注意我们不需要参数名字,但是加上是一个最佳实践
DECLARE_DELEGATE_OneParam(FOnScoreChangedWithScoreSignature, int32 /* NewScore */);
// 匹配函数: void OnScoreChangedWithScore(int32 NewScore);
// 注意 "params" 是复数形式
DECLARE_DELEGATE_TwoParams(FOnScoreChangedWithOwnerSignature, int32 /* NewScore */, class APlayerState* /* OwningPlayer */);
// 匹配函数: void OnScoreChangedWithOwner(int32 NewScore, class APlayerState* OwningPlayer);
// 你可以把声明分隔到多行
// 注意 "params" 是复数形式
DECLARE_DELEGATE_TwoParams(FOnMultilineExampleSignature, \
int32 /* NewScore */,\
class APlayerState* /* OwningPlayer */);
// 匹配函数: void OnScoreChangedWithOwner(int32 NewScore, class APlayerState* OwningPlayer);
动态代理语法
// 可以声明一个无参的动态代理
DECLARE_DYNAMIC_DELEGATE(FOnScoreChangedSignature);
// 匹配函数: void OnScoreChanged();
// 注意在参数类型和参数名之间需要逗号
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewScore);
// 匹配函数: void OnScoreChanged(int32 NewScore);
// 注意 "params" 是复数形式
DECLARE_DYNAMIC_DELEGATE_TwoParams(FOnScoreChangedSignature, int32, NewScore, class APlayerState*, OwningPlayer);
// 匹配函数: void OnScoreChanged(int32 NewScore, APlayerState* OwningPlayer);
返回值
非多播代理也可以包含一个自定义返回值,而不是默认的 void。只需要加一个 “RetVal” 并且把返回类型插入到宏定义的开头
DECLARE_DELEGATE_RetVal_TwoParams(bool, FOnDogSucceededWoofing, class ADog* /* Dog */, FString /* WoofWord */);
// 匹配函数: bool OnDogWoof(ADog* Dog, FString WoofWord);
DECLARE_DYNAMIC_DELEGATE_RetVal_TwoParams(bool, FOnDogSucceededWoofing, class ADog*, Dog, FString, WoofWord);
// 匹配函数: bool OnDogWoof(ADog* Dog, FString WoofWord);
多播代理
多播代理的签名几乎一样,只是在 “DELEGATE” 之前加了 “MULTICAST_”。可以与它们绑定的函数的签名是相同的。但是绑定到他们的方法有些不同,我们稍后会看。
注意多播代理并不支持返回值
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewScore);
2.创建代理类型的变量
我们已经选择了想要的代理类型和它的签名,并且它的签名定义了它支持调用的方法的参数。我们现在需要把这些代理加到一个类或结构体某处,这样其他想被通知的人才能订阅到它们。
无论我们声明的代理类型是什么,把它添加到其他类是相同的。但是注意只有动态代理可以通过蓝图暴露。
// BUIPlayerState.h
#pragma once
#include "GameFramework/PlayerState.h"
#include "BUIPlayerState.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewScore);
UCLASS()
class ABUIPlayerState : public APlayerState
{
GENERATED_BODY()
public:
// 我们希望这个是 public 的所有我们的 UI 可以访问并订阅它
// 也添加了 BlueprintAssignable 来让它是可以通过蓝图访问的
UPROPERTY(BlueprintAssignable)
FOnScoreChangedSignature OnScoreChangedDelegate;
};
3. 订阅到一个代理
现在我们已经声明了我们的代理,并且在代码某处创建了它的一个成员变量,现在我们可以连接一个或多个函数到它们,这样这些函数将会在代理被调用的时候被调用。
像定义代理一样,订阅到代理也是不同的,取决于它是否是 单播/多播 和 非动态/动态
非动态单播代理
非动态单播代理有一些列你可以使用去绑定一个需要执行的函数或 lambda 的函数。我不会讲解它们全部但是这是我经常使用的一些
- BindLambda
- BindRaw
- BindStatic
- BindSP
- BindUFUnction
- BindUObject
- BindWeakLambda
- BindThreadSafeSP
DECLARE_DELEGATE_OneParam(FOnScoreChangedSignature, int32 /* NewScore */);
// BindUObject requires that the target be a UObject
OnScoreChangedDelegate.BindUObject(this, &ThisClass::OnScoreChanged);
// BindRaw is for if the target is not a UObject
OnScoreChangedDelegate.BindRaw(SomeSlateThing, &SSlomeSlateThing::OnScoreChangedRaw);
// BindLambda is useful for simpler anonymous functions
OnScoreChangedDelegate.BindLambda([](int32 NewScore)
{
// Do something with score
});
非动态多播代理
订阅到非动态多播代理和订阅到单播非常相似。函数前缀换成了 “Add” 而不是 “Bind”,因为多个函数可以被绑定到代理
- AddLambda
- AddRaw
- AddStatic
- AddSP
- AddUFunction
- AddUObject
- AddWeakLambda
- AddThreadSafeSP
DECLARE_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, int32 /* NewScore */);
// AddUObject requires that the target be a UObject
OnScoreChangedDelegate.AddUObject(this, &ThisClass::OnScoreChanged);
// AddRaw is for if the target is not a UObject
OnScoreChangedDelegate.AddRaw(SomeSlateThing, &SSlomeSlateThing::OnScoreChangedRaw);
// AddLambda is useful for simpler anonymous functions
OnScoreChangedDelegate.AddLambda([](int32 NewScore)
{
// Do something with score
});
动态单播代理
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewScore);
// 如果我们有一个 UFUNCTION()-标记的函数 `OnScoreChanged(int32 NewScore)
// 我们可以使用 BindDynamic 和 ThisClass 宏来订阅
OnScoreChangedDelegate.BindDynamic(this, &ThisClass::OnScoreChanged);
4. 调用代理
多亏有了前置的步骤,这一步相对直接
- 对于单播代理:.Execute() 或 .ExecuteIfBound()
- 对于多播代理:.Broadcast()
进阶主题
这些是代理的更进阶或更席位的差别
重用代理签名
有些情况许多不同事件可以共享相同的签名。一个比较蠢的例子,试想对于我们的 “ADog”类可做的不同事我们有代理:
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
{Delegate type} OnDogJumpedDelegate;
{Delegate type} OnDogWoofedDelegate;
{Delegate type} OnDogSatDownDelegate;
};
{Delegate type} 应该是什么?我们应该定义 3 个有相同参数的代理吗?
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDogJumpedSignature, ADog*, Dog);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDogWoofedSignature, ADog*, Dog);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDogSatDownSignature, ADog*, Dog);
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
FOnDogJumpedSignature OnDogJumpedDelegate;
FOnDogWoofedSignature OnDogWoofedDelegate;
FOnDogSatDownSignature OnDogSatDownDelegate;
};
或只有一个,并重用相同的事件?
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(OnDogEventSignature, ADog*, Dog);
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
FOnDogEventSignature OnDogJumpedDelegate;
FOnDogEventSignature OnDogWoofedDelegate;
FOnDogEventSignature OnDogSatDownDelegate;
};
我认为这是一个比较蠢的例子,第二种更合理但是如果事件真的是不同目的的并且只是有机会碰巧有相同的签名,那么你可以声明不同的代理类型应对不同的目的。
稀疏代理
稀疏代理是一类特别的代理类型应该被使用在如下场景:
- 当代理很少被绑定
- 当包含代理的对象的内存使用需要考虑。比如有许多对象的实例因此减少每个实例的大小是需要的。
只有在动态多播中,并且不支持返回值。你可以看到在 “Actor.h” 和 “PrimitiveComponent.h” 使用了,并且定义在 “SparseDelegate.h” 包含如下注释:稀疏代理可以给不经常绑定的的代理使用,对象可以仅使用 1 byte 的存储而不是包含完整的委托调用清单。来自代理的调用、添加、移除之类的开销比直接使用代理要高,因此节约内存和代理被绑定的频率需要进行平衡考量
它们的宏签名和其他代理非常不同所以要留意:
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE(SparseDelegateClassName, OwningClass, DelegateName);
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(SparseDelegateClassName, OwningClass, DelegateName, Param1Type, Param1Name);
// Dog.h
#pragma once
#include "GameFramework/Actor.h"
#include "Dog.generated.h"
// Note you have to forward declare here, you *cannot* do it within the
// delegate declaration
class ADog;
DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(FOnDogWoofSignature, ADog, OnDogWoofDelegate, FString, WoofText);
UCLASS()
class ADog : public AActor
{
GENERATED_BODY()
// We only very rarely need to subscribe to woofs
// And we have a *lot* of dogs
FOnDogWoofSignature OnDogWoofDelegate;
};
也有一个很方便的控制台指令 “SparseDelegateReport” 可以输出哪个稀疏代理是被绑定的。
使用有逗号的数据类型
某些时候你可能会尝试声明一个包含 “TMap<K,V>” 的代理,就像这样:
// 无法运行!
DECLARE_DELEGATE_OneParam(FOnScoresForPlayersChangedSignature, TMap<FName, int32> /* ScoreMap */)
Unreal 的 DECLARE 宏会因为 TMap<FName, int32> 中的逗号感到困惑,有几种解决的方法
使用 typedef
一种方法是给 TMap<FName, int32> 创建一个新的 typedef
typedef TMap<FName, int32> ScoreMap;
DECLARE_DELEGATE_OneParam(FOnScoresForPlayersChangedSignature, ScoreMap /* NewMap */)
使用 TDelegate 而不是宏
另一个方法就是干脆不使用宏,并且和代理一行声明类型。注意这些只能从 C++ 访问,无法从蓝图访问
// Using TDelegate instead of DECLARE_DELEGATE
UCLASS()
class ABUIPlayerState : public APlayerState
{
GENERATED_BODY()
public:
// We don't need the DECLARE_DELEGATE_... macro
TDelegate<void(TMap<FName, int32>)> OnScoreChangedDelegate;
};
包在结构体中
如果想使用一个动态代理并且在蓝图中使用,我会把哪个数据类型包裹在一个结构体中,并且作为代理的参数使用。使用结构体同样是一个最佳实践,当你开始需要更多的参数或者你认为将来会需要更多参数。把所有需要的信息包裹在一个结构体中避免了签名变化和重改写函数和代理
// Using a struct to wrap a TMap<K,V>
USTRUCT(BlueprintType)
struct FScoreData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite)
TMap<FName, int32> ScoreMap;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, FScoreData, ScoreData);
UCLASS()
class ABUIPlayerState : public APlayerState
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintAssignable)
FOnScoreChangedSignature OnScoreChangedDelegate;
};
序列化
只有动态代理支持序列化,但是为什么我们要考虑它?
在我们的例子,绑定玩家状态 OnScoreChangedDelegate 和 UI 控件,通常我们不需要序列化那个绑定。我们可以在游戏开始控件创建时执行。
但是试想我们在做一个城市建设游戏,我们有 AWorker actor 的实例,它们跑来跑去并且绑定到任务完成时的代理。在这种情况我们也许想序列化这些订阅这样它们就可以在游戏重新加载时被恢复。
当游戏保存状态时,我们想保持这些关联。所有为了这些我们需要使用动态代理。
事件 Events
事件在 4.27 文档中被提到,但是 Dylan 在 5.0 引擎的源码中找到了这个小信息:
/**
* Declares a multicast delegate that is meant to only be activated from OwningType
* NOTE: This behavior is not enforced and this type should be considered deprecated for new delegates, use normal multicast instead
* 声明了一个多播代理应当只从 OwingType 被激活
* 注意:这个行为不是强制的并且这个类型应该考虑被新的代理弃用,使用正常的多播代替
*/
#define DECLARE_EVENT(OwningType, EventName) FUNC_DECLARE_EVENT(OwningType, EventName, void)
所以我考虑 5.0 弃用了事件。使用多播代理代替。