【UE5 C++课程系列笔记】08——单播委托的基本使用

发布于:2024-12-18 ⋅ 阅读:(130) ⋅ 点赞:(0)

目录

概念

一. 单播委托

二. 多播委托

三. 动态单播/多播

单播委托——申明委托 

一、DECLARE_DELEGATE

二、DECLARE_DELEGATE_OneParam

三、DECLARE_DELEGATE_TwoParams

四、DECLARE_DELEGATE_Params(通用多参数形式)

五、 DECLARE_DELEGATE_RetVal

六、DECLARE_DELEGATE_RetVal_OneParam 

七、DECLARE_DELEGATE_RetVal_TwoParams 

八、DECLARE_DELEGATE_RetVal_Params(通用多参数有返回值形式)

单播委托——绑定委托

一、Bind

二、BindStatic

三、BindRaw

四、BindLambda

五、 BindSP

六、BindUObject

七、 Unbind

应用

一、 无参无返回值的单播委托 

二、 一个参数和返回值的单播委托

三、单播载荷数据


概念

一. 单播委托

        单播委托绑定到单个对象的单个函数上,就像是一对一的映射关系。当调用该委托时,只会执行与之绑定的那一个函数。

二. 多播委托

        多播委托可以绑定多个不同对象的函数(也可以是同一个对象的多个不同函数)。当调用多播委托时,会按照绑定的顺序依次执行所有与之绑定的函数,适合于需要通知多个地方执行相关操作的场景,比如游戏中某个事件发生了,需要多个不同的系统做出响应。

三. 动态单播/多播

        动态单播委托基于虚幻的反射系统,可以在蓝图中进行绑定等操作,这使得它在蓝图与 C++ 交互方面非常有用。动态单播只绑定一个函数,而动态多播可以绑定多个函数,与非动态的单播/多播相比,动态单播/多播提供了蓝图可访问性,使其能在蓝图中进行添加绑定、移除绑定以及调用等操作。

单播委托——申明委托 

单播委托中用于申明委托相关宏如下: 

一、DECLARE_DELEGATE

用于声明一个简单的无参数、无返回值的委托类型。它定义了一种可以绑定函数(成员函数、静态函数、普通函数等,根据后续具体绑定方式决定)的机制,使得在合适的时候能够调用这些绑定的函数,常用于实现事件响应等简单的回调场景。

在声明一个名为 FMySimpleDelegate 的委托类型后,可以像下面这样使用

#include "CoreMinimal.h"

DECLARE_DELEGATE(FMySimpleDelegate);

// 定义一个普通函数用于绑定
void SimpleFunction()
{
    UE_LOG(LogTemp, Warning, TEXT("Simple function called"));
}

void UseDelegate()
{
    FMySimpleDelegate DelegateInstance;
    DelegateInstance.BindRaw(SimpleFunction);
    DelegateInstance.Execute();
}

二、DECLARE_DELEGATE_OneParam

声明一个带一个参数的委托类型。这个参数在委托被调用时需要传入相应的值,常用于需要根据某个特定输入来触发相应函数逻辑的情况,比如根据不同的游戏事件参数来执行不同的处理函数。

 声明名为 FMyDelegateWithOneParam 的委托类型,它带有一个 int 类型的参数。示例使用如下

#include "CoreMinimal.h"

DECLARE_DELEGATE_OneParam(FMyDelegateWithOneParam, int);

// 定义一个成员函数用于绑定(假设在某个类中)
class AMyClass
{
public:
    void FunctionWithParam(int Value)
    {
        UE_LOG(LogTemp, Warning, TEXT("Function with param called, value: %d"), Value);
    }
};

void UseDelegateWithParam()
{
    FMyDelegateWithOneParam DelegateInstance;
    AMyClass* MyClassInstance = new AMyClass();
    DelegateInstance.BindRaw(MyClassInstance, &AMyClass::FunctionWithParam);
    DelegateInstance.Execute(42);
    delete MyClassInstance;
}

三、DECLARE_DELEGATE_TwoParams

声明一个带有两个参数的委托类型,能满足需要同时传入两个不同参数来触发相应函数逻辑的场景,例如在游戏中处理两个不同维度的输入数据(如坐标的 x 和 y 值等)对应的操作时可以使用。

声明 FMyDelegateWithTwoParams 委托类型,它带有一个 int 类型和一个 float 类型的参数。使用示例如下

#include "CoreMinimal.h"

DECLARE_DELEGATE_TwoParams(FMyDelegateWithTwoParams, int, float);

// 定义一个普通函数用于绑定
void FunctionWithTwoParams(int Arg1, float Arg2)
{
    UE_LOG(LogTemp, Warning, TEXT("Function with two params called, arg1: %d, arg2: %f"), Arg1, Arg2);
}

void UseDelegateWithTwoParams()
{
    FMyDelegateWithTwoParams DelegateInstance;
    DelegateInstance.BindRaw(FunctionWithTwoParams);
    DelegateInstance.Execute(10, 3.14f);
}

四、DECLARE_DELEGATE_<Num>Params(通用多参数形式)

这是一种通用的声明带有多个参数的委托类型的方式,<Num> 处可以替换为具体的参数个数数字,用于在需要更多参数(超过两个)来传递信息并触发相应函数逻辑的复杂场景下声明委托。比如在处理游戏角色复杂状态变化(包含位置、生命值、技能状态等多个参数变化)时可按需声明相应参数个数的委托。

如下代码是在游戏角色属性更新场景中的应用。首先在头文件中进行委托声明与相关类定义。

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyCharacter.generated.h"

// 声明一个带有三个参数的委托类型,分别表示角色的生命值、魔法值、体力值变化
DECLARE_DELEGATE_ThreeParams(FCharacterStatsUpdateDelegate, float, float, float);

UCLASS()
class MYGAME_API AMyCharacter : public AActor
{
    GENERATED_BODY()

public:
    // 当前角色的生命值、魔法值、体力值属性
    float Health;
    float Mana;
    float Stamina;

    // 用于更新角色属性并触发委托的函数
    void UpdateStats(float NewHealth, float NewMana, float NewStamina)
    {
        Health = NewHealth;
        Mana = NewMana;
        Stamina = NewStamina;

        // 触发委托,通知其他关注角色属性变化的模块
        OnStatsUpdated.ExecuteIfBound(Health, Mana, Stamina);
    }

    // 定义委托实例
    FCharacterStatsUpdateDelegate OnStatsUpdated;
};

在源文件中使用委托进行属性变化监听的类示例 

#include "CoreMinimal.h"
#include "MyCharacter.h"
#include "StatsObserver.generated.h"

UCLASS()
class MYGAME_API UStatsObserver : public UObject
{
    GENERATED_BODY()

public:
    // 用于绑定到角色属性更新委托的函数,在这里可以进行属性变化后的相关处理,比如打印日志等
    void OnCharacterStatsUpdated(float NewHealth, float NewMana, float NewStamina)
    {
        UE_LOG(LogTemp, Warning, TEXT("Character stats updated: Health = %f, Mana = %f, Stamina = %f"), NewHealth, NewMana, NewStamina);
    }

    void BindToCharacter(AMyCharacter* Character)
    {
        if (Character)
        {
            // 将本类的 OnCharacterStatsUpdated 函数绑定到角色的属性更新委托上
            Character->OnStatsUpdated.BindUObject(this, &UStatsObserver::OnCharacterStatsUpdated);
        }
    }
};

五、 DECLARE_DELEGATE_RetVal

声明一个没有参数但有返回值的委托类型。这种委托常用于需要获取某个特定值或者状态,并且希望以一种灵活的函数绑定和调用机制来实现的场景,比如获取游戏角色当前的生命值、当前所在位置等信息。

声明了名为 FMyDelegateWithReturnValue 的委托类型,它返回 int 类型的值。示例用法如下

#include "CoreMinimal.h"

DECLARE_DELEGATE_RetVal(int, FMyDelegateWithReturnValue);

// 定义一个成员函数用于绑定(假设在某个类中),返回 int 值
class AMyClass
{
public:
    int GetValue()
    {
        return 42;
    }
};

void UseDelegateWithReturnValue()
{
    FMyDelegateWithReturnValue DelegateInstance;
    AMyClass* MyClassInstance = new AMyClass();
    DelegateInstance.BindRaw(MyClassInstance, &AMyClass::GetValue);
    int Result = DelegateInstance.Execute();
    UE_LOG(LogTemp, Warning, TEXT("Result value: %d"), Result);
    delete MyClassInstance;
}

六、DECLARE_DELEGATE_RetVal_OneParam 

 声明一个带有一个参数且有返回值的委托类型,结合了参数传递和返回值获取的功能,适用于根据输入参数进行相应计算或查询后返回特定结果的场景,例如根据游戏物品的 ID 参数来返回该物品的属性值等情况。

声明 FMyDelegateWithReturnValueAndParam 委托类型,它带一个 FString 类型的参数且返回 int 类型的值。使用示例如下

#include "CoreMinimal.h"

DECLARE_DELEGATE_RetVal_OneParam(int, FMyDelegateWithReturnValueAndParam, FString);

// 定义一个成员函数用于绑定(假设在某个类中),接收 FString 参数并返回 int 值
class AMyClass
{
public:
    int GetValueBasedOnString(FString Input)
    {
        if (Input == "Test")
        {
            return 100;
        }
        return 0;
    }
};

void UseDelegateWithReturnValueAndParam()
{
    FMyDelegateWithReturnValueAndParam DelegateInstance;
    AMyClass* MyClassInstance = new AMyClass();
    DelegateInstance.BindRaw(MyClassInstance, &AMyClass::GetValueBasedOnString);
    int Result = DelegateInstance.Execute("Test");
    UE_LOG(LogTemp, Warning, TEXT("Result value: %d"), Result);
    delete MyClassInstance;
}

七、DECLARE_DELEGATE_RetVal_TwoParams 

声明一个带有两个参数且有返回值的委托类型,用于更复杂的需要两个输入参数来进行运算或查询并返回结果的场景,例如根据游戏中两个角色的坐标参数来计算它们之间的距离并返回等情况。

 声明 FMyDelegateWithTwoReturnValueParams 委托类型,它带有 int 和 float 类型的两个参数且返回 int 类型的值。使用示例如下

#include "CoreMinimal.h"

DECLARE_DELEGATE_RetVal_TwoParams(int, FMyDelegateWithTwoReturnValueParams, int, float);

// 定义一个普通函数用于绑定,接收两个参数并返回 int 值
void FunctionWithTwoReturnValueParams(int Arg1, float Arg2)
{
    return (int)(Arg1 + Arg2);
}

void UseDelegateWithTwoReturnValueParams()
{
    FMyDelegateWithTwoReturnValueParams DelegateInstance;
    DelegateInstance.BindRaw(FunctionWithTwoReturnValueParams);
    int Result = DelegateInstance.Execute(5, 3.14f);
    UE_LOG(LogTemp, Warning, TEXT("Result value: %d"), Result);
}

八、DECLARE_DELEGATE_RetVal_<Num>Params(通用多参数有返回值形式)

这是一种通用的声明带有多个参数且有返回值的委托类型的方式,和前面的 <Num> 代表参数个数的概念类似,用于在非常复杂的、需要多个输入参数来处理并返回结果的场景下声明委托,例如在复杂的游戏系统模拟中,根据多个不同的状态参数来综合计算并返回一个关键指标值等情况。

假设声明一个带三个参数(分别为 intfloatFString 类型)且返回 int 类型值的委托类型,代码如下

DECLARE_DELEGATE_RetVal_ThreeParams(int, FMyDelegateWithThreeReturnValueParams, int, float, FString);

单播委托——绑定委托

 绑定单播委托可使用以下7个绑定函数

一、Bind

Bind 用于将一个成员函数绑定到委托(Delegate)上,使得委托在被调用时能够执行对应的成员函数。它主要用于绑定 UObject(UE 中多数游戏对象都是基于 UObject 派生而来)的成员函数,并且能够自动处理对象的生命周期相关问题,比如在对象被销毁时自动解除绑定,避免出现悬空指针等错误情况。

假设我们有一个 ACharacter 类的派生类 AMyCharacter,并且定义了一个委托类型 FMyDelegate(使用合适的 DECLARE_DELEGATE 系列宏声明),以及 AMyCharacter 类中有一个符合委托参数和返回值要求的成员函数 void DoSomething(int Value),示例代码如下:

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"

DECLARE_DELEGATE_OneParam(FMyDelegate, int);

UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    void DoSomething(int Value)
    {
        // 这里可以添加具体要执行的代码逻辑,比如打印信息等
        UE_LOG(LogTemp, Warning, TEXT("Doing something with value: %d"), Value);
    }

    void SetupDelegate()
    {
        FMyDelegate DelegateInstance;
        // 使用Bind绑定成员函数到委托实例上,this表示当前对象(AMyCharacter实例)
        DelegateInstance.Bind(this, &AMyCharacter::DoSomething);

        // 之后可以调用委托实例来执行绑定的函数,传入相应参数
        DelegateInstance.Execute(42);
    }
};

二、BindStatic

BindStatic 用于绑定静态函数到委托上。静态函数是属于类本身的函数,不需要类的实例对象就可以调用,它不依赖于特定的对象实例状态。使用 BindStatic 可以方便地将类中的静态函数与委托关联起来,供后续委托调用机制使用。

假设我们有一个类 MyUtilityClass,里面定义了一个静态函数 static void StaticFunction(int Input),以及相应的委托 FAnotherDelegate(同样通过合适的委托声明宏声明),示例代码如下:

#include "CoreMinimal.h"
#include "MyUtilityClass.generated.h"

DECLARE_DELEGATE_OneParam(FAnotherDelegate, int);

UCLASS()
class MYGAME_API MyUtilityClass
{
    GENERATED_BODY()

public:
    static void StaticFunction(int Input)
    {
        UE_LOG(LogTemp, Warning, TEXT("Executing static function with input: %d"), Input);
    }
};

// 在其他地方使用示例
void UseBindStatic()
{
    FAnotherDelegate StaticDelegate;
    StaticDelegate.BindStatic(&MyUtilityClass::StaticFunction);
    StaticDelegate.Execute(10);
}

三、BindRaw

BindRaw 用于绑定普通的 C++ 函数(非 UObject 相关的成员函数或静态函数)或者函数指针到委托上。与 Bind 和 BindStatic 不同,它不会去处理 UObject 的生命周期等额外机制,只是单纯地建立函数与委托之间的调用关联,所以在使用时需要开发者自己确保所绑定函数的有效性以及相关资源(如函数所属对象的生命周期,如果是类成员函数的话)的正确管理。

假设我们有一个普通的全局函数 void GlobalFunction(int Num) 和一个委托 FGeneralDelegate(通过委托声明宏声明),示例代码如下

#include "CoreMinimal.h"

DECLARE_DELEGATE_OneParam(FGeneralDelegate, int);

// 全局函数定义
void GlobalFunction(int Num)
{
    UE_LOG(LogTemp, Warning, TEXT("Running global function with num: %d"), Num);
}

// 在其他地方使用示例
void UseBindRaw()
{
    FGeneralDelegate RawDelegate;
    RawDelegate.BindRaw(GlobalFunction);
    RawDelegate.Execute(20);
}

四、BindLambda

 BindLambda 允许使用 C++ 的 Lambda 表达式来绑定到委托上。Lambda 表达式可以方便地定义匿名函数,能够在需要函数对象的地方简洁地编写临时的逻辑代码块,通过 BindLambda 就可以把这个临时定义的函数逻辑与委托关联起来,让委托调用时执行这个 Lambda 表达式里的代码。

以下示例展示了如何使用 BindLambda 将一个 Lambda 表达式绑定到委托上

#include "CoreMinimal.h"

DECLARE_DELEGATE_OneParam(FLambdaDelegate, int);

// 在某个函数中使用示例
void UseBindLambda()
{
    FLambdaDelegate LambdaDelegate;
    LambdaDelegate.BindLambda([](int Arg) {
        UE_LOG(LogTemp, Warning, TEXT("Executing lambda function with arg: %d"), Arg);
    });
    LambdaDelegate.Execute(30);
}

五、 BindSP

BindSP 主要用于绑定共享指针(TSharedPtr)指向的对象的成员函数到委托上。在 UE 中,共享指针常用于管理对象的生命周期,尤其是对于一些需要在多个地方共享使用但又要合理控制其销毁时机的对象。BindSP 结合共享指针可以确保在对象的引用计数归零时正确处理与委托的绑定关系,避免出现悬空指针等问题。

假设我们有一个类 MySharedClass,其成员函数 void DoSharedFunction(int Data),以及对应的委托 FSharedDelegate(通过委托声明宏声明),示例代码如下

#include "CoreMinimal.h"
#include "MySharedClass.generated.h"

DECLARE_DELEGATE_OneParam(FSharedDelegate, int);

UCLASS()
class MYGAME_API MySharedClass
{
    GENERATED_BODY()

public:
    void DoSharedFunction(int Data)
    {
        UE_LOG(LogTemp, Warning, TEXT("Doing shared function with data: %d"), Data);
    }
};

// 在其他地方使用示例
void UseBindSP()
{
    TSharedPtr<MySharedClass> SharedPtr = MakeShared<MySharedClass>();
    FSharedDelegate SharedDelegate;
    SharedDelegate.BindSP(SharedPtr, &MySharedClass::DoSharedFunction);
    SharedDelegate.Execute(50);
}

六、BindUObject

BindUObject 和前面提到的 Bind 功能类似,也是用于绑定 UObject 实例的成员函数到委托上,但它在语法上有一些细微区别,并且在一些特定场景下使用起来可能更方便清晰。它同样会处理 UObject 的生命周期相关情况,保证委托调用的安全性和有效性。

以下示例重新展示了类似 Bind 部分示例中的情况,但使用 BindUObject 来绑定成员函数到委托上

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyCharacter.generated.h"

DECLARE_DELEGATE_OneParam(FMyDelegate, int);

UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    void DoSomething(int Value)
    {
        UE_LOG(LogTemp, Warning, TEXT("Doing something with value: %d"), Value);
    }

    void SetupDelegate()
    {
        FMyDelegate DelegateInstance;
        // 使用BindUObject绑定成员函数到委托实例上,this表示当前对象(AMyCharacter实例)
        DelegateInstance.BindUObject(this, &AMyCharacter::DoSomething);

        // 之后可以调用委托实例来执行绑定的函数,传入相应参数
        DelegateInstance.Execute(42);
    }
};

七、 Unbind

Unbind 用于解除已经绑定到委托上的函数。当某个委托不再需要执行之前绑定的函数时(比如对应的功能逻辑已经结束,或者对象要被销毁等情况),就可以使用 Unbind 操作来断开函数与委托之间的关联,避免后续可能出现的不必要的函数调用或者错误情况。

假设我们之前绑定了函数到委托上,现在要解除绑定,示例代码如下

#include "CoreMinimal.h"

DECLARE_DELEGATE_OneParam(FMyDelegate, int);

void SomeFunction(int Arg)
{
    UE_LOG(LogTemp, Warning, TEXT("This is a function that was bound."));
}

void UnbindExample()
{
    FMyDelegate DelegateInstance;
    DelegateInstance.BindRaw(SomeFunction);

    // 解除绑定操作
    DelegateInstance.Unbind();

    // 此时再调用委托将不会执行之前绑定的函数了
    DelegateInstance.Execute(10);
}

 

应用

一、 无参无返回值的单播委托 

1. 创建4个Actor类,分别用于编写单播、多播、动态单播、动态多播

2. 在头文件中,先申明一个名为“FSingleDelegate_PrintLocation”的单播委托类型

再声明了一个“FSingleDelegate_PrintLocation”类型的委托成员变量,用于后续绑定函数并进行调用

再申明一个Actor类“ALocationActor”

在类“ALocationActor”中添加一个委托函数“PrintLocation”,用于打印自身位置

 

3. 在源文件中添加“PrintLocation”函数的实现,该函数实现了打印函数名称和打印Actor位置 

4.  在“ALocationActor”的构造函数中创建根组件,然后在BeginPlay中先获取ASingleDelegateActor的对象引用,若获取对象引用成功就将当前 ALocationActor 的 PrintLocation 函数绑定到 SingleDelegateActorPtr 所指向的 ASingleDelegateActor 实例的 SingleDelegate_PrintLocation 单播委托上。

5. 定义一个函数“CallLocationActorPrint()”

        CallLocationActorPrint()函数在确定委托已经绑定了函数的情况下,调用 SingleDelegate_PrintLocation 委托的 Execute 方法来触发执行与之绑定的函数

6. 下面开始测试,将“LocationActor”和“SingleDelegateActor”拖入场景中

 在关卡蓝图中通过按键调用“SingleDelegateActor”的“CallLocationActorPrint()”函数

可以看到当调用“CallLocationActorPrint()”函数后执行了绑定的委托函数“PrintLocation”。此时就实现了无参无返回值的单播委托。

下面继续实现有一个参数和返回值的单播委托。

二、 一个参数和返回值的单播委托

         声明一个名为 FSingleDelegate_GetLocation 的委托类型,该委托有一个参数和返回值,参数的类型是 FString,返回值类型为 FVector

再在“ASingleDelegateActor”类中声明一个“FSingleDelegate_GetLocation”类型的委托成员变量,用于后续绑定函数并进行调用

在另一个类“ALocationActor”中添加一个委托函数“GetLocation”,用于获取自身位置

在源文件中,当“ALocationActor”开始运行时获取“ASingleDelegateActor”的引用,若获取对象引用成功就将当前 ALocationActor 的 GetLocation 函数绑定到 SingleDelegate_GetLocation 单播委托上

实现“GetLocation”如下,在打印传入的字符串参数和自身位置后将位置返回

在“ASingleDelegateActor”中定义一个蓝图调用方法“CallLocationActorGet”

当方法“CallLocationActorGet”被调用后会执行委托SingleDelegate_GetLocation,并给委托传入一个字符串参数,然后接收FVector类型的返回值,最后将返回值打印出来。

在关卡蓝图中通过按键调用“SingleDelegateActor”的“CallLocationActorGet()”函数

可以看到当调用“CallLocationActorGet()”函数后执行了绑定的委托函数“GetLocation”。此时就实现了一个参数有返回值的单播委托。

三、单播载荷数据

官网对载荷数据的定义如下:

下面介绍在不更换单播委托类型的情况下传递载荷数据。

在“ALocationActor”中新定义一个委托函数“GetLocationWithPayLoad”,该函数包含三个输入参数

在源文件中实现委托函数,该函数打印了传入的三个参数

在“ALocationActor”事件开始运行后将当前 ALocationActor 的 GetLocationWithPayLoad函数绑定到 SingleDelegate_GetLocation 单播委托上,并将两个参数传入

此时运行可以看到通过委托成功在CallLocationActorGet被调用时,触发委托函数GetLocationWithPayLoad,并将传入的参数值Instr, Health, Money打印出来。