虚幻(UnrealEngine)全局事件插件(UnrealEngine.GlobalEvents)

https://github.com/bodong1987/UnrealEngine.GlobalEvents

学习Unreal的练手代码,主要用途是提供一个全局级别的消息广播与消息监听,目的是解决直接引用对象带来的强依赖的问题。

详情可见github首页。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

介绍

这是虚幻引擎的插件。其主要目的是在Unreal中实现一个简单的消息发送系统,解决使用函数调用、委托等手段带来的强耦合问题。通过这个插件,你可以向消息服务注册一个消息监听器(观察者),还可以通过消息服务发送消息(发布者)。

该插件支持以下功能:

  • 在蓝图、C++ 或其他脚本中注册事件回调。
  • 使用蓝图、C++ 或其他脚本语言发送事件。
  • C++级别有两种注册模式:静态和动态。静态模式会在编译时检查消息发送和消息接收代码是否匹配。
  • 其他处于动态模式。如果发送者和接收者之间的签名不兼容,则会打印错误日志并且消息发送将会将失败。
  • 提供相应的蓝图节点用于调度事件和接收事件回调
  • 需要虚幻引擎 4.25 或更高版本

如何使用

      首先,您的虚幻引擎应该是 4.25 或更新版本。我在Windows下使用Unreal 5.3.1和4.25测试,在MacOS下使用5.3.1测试。
      然后将此项目下的Plugins/GlobalEvents目录复制到您项目的Plugins目录中。如果要运行测试代码,也可以将GlobalEventsTests目录一起复制。
      编译成功后,即可使用并测试。

注册消息回调

      要在 C++ 中注册回调,您只需调用 UGameEventSubsystem::Register。该接口支持将以下对象绑定到事件。这些目标与 UnrealEngine 的 Delegate 类似:

  • 全局函数
  • 类的静态成员函数
  • RawPointer + 类的成员函数
  • TSharedPtr + 类的成员函数
  • UObject* + 类的成员函数
  • UObject* + UFunction 的名称
  • 拉姆达表达式

      需要注意的是,为了兼容蓝图,并不是所有的C++类型都可以作为事件参数。理论上只能支持UFUNCTION支持的参数。

      每个Register接口都会返回一个FDelegateHandle作为句柄来代表你注册的回调。具体可以参考项目中的示例文件:

void UGameEventTestsSubsystem::RegisterDebugEvent()
{
    UGameEventSubsystem* EventCenter = UGameEventSubsystem::GetInstance(this);
    check(EventCenter != nullptr);

    // [dynamic mode]register a global function 
    EventCenter->Register(FDebugEvent::GetEventName(), TestGlobalCFunctionObserver);

    // [dynamic mode]register a class static member function
    EventCenter->Register(FDebugEvent::GetEventName(), &UTestObject::TestUObjectStaticMemberFunctionObserver);

    // [dynamic mode]register a UObject member function in C++ mode
    EventCenter->Register(FDebugEvent::GetEventName(), TestsObj, &UTestObject::TestUObjectMemberFunctionObserver);

    // [dynamic mode]register a UObject UFunction in script mode(same as dynamic delegate)
    EventCenter->Register(FDebugEvent::GetEventName(), TestsObj, GET_FUNCTION_NAME_CHECKED(UTestObject, TestUObjectUFunctionObserver));

    // [static mode]register a global function 
    EventCenter->Register<FDebugEvent>(TestGlobalCFunctionObserver);

    // [static mode]register a class static member function
    EventCenter->Register<FDebugEvent>(&UTestObject::TestUObjectStaticMemberFunctionObserver);

    // [static mode]register a normal class's member function on raw pointer
    EventCenter->Register<FDebugEvent>(&RawObj, &FRawTestsObject::TestRawMemberFunctionObserver);

    // [static mode]register a normal class's member function on TSharedPtr
    EventCenter->Register<FDebugEvent>(RawObjPtr, &FRawTestsObject::TestRawMemberFunctionObserver);
}

  要在蓝图中注册事件,请使用Register Global Event节点。您需要提供事件名称、目标对象和函数名称作为参数:

       请注意:默认情况下,事件的签名由首先注册该事件的回调函数确定。当然,如果你想强制进行消息签名,可以通过接口:UGameEventSubsystem::BindSignature来实现。

取消注册消息回调

       在C++中,只需要将接口从Register改为UnRegister,其余不变。像这样:

void UGameEventTestsSubsystem::UnRegisterDebugEvent()
{
    UGameEventSubsystem* EventCenter = UGameEventSubsystem::GetInstance(this);
    check(EventCenter != nullptr);

    const FName EventName = FDebugEvent::GetEventName();

    EventCenter->UnRegister<FDebugEvent>(&RawObj, &FRawTestsObject::TestRawMemberFunctionObserver);
    EventCenter->UnRegister<FDebugEvent>(&UTestObject::TestUObjectStaticMemberFunctionObserver);
    EventCenter->UnRegister<FDebugEvent>(RawObjPtr, &FRawTestsObject::TestRawMemberFunctionObserver);

    EventCenter->UnRegister(FDebugEvent::GetEventName(), TestGlobalCFunctionObserver);
    EventCenter->UnRegister(FDebugEvent::GetEventName(), &UTestObject::TestUObjectStaticMemberFunctionObserver);
    EventCenter->UnRegister(FDebugEvent::GetEventName(), TestsObj, &UTestObject::TestUObjectMemberFunctionObserver);
    EventCenter->UnRegister(FDebugEvent::GetEventName(), TestsObj, GET_FUNCTION_NAME_CHECKED(UTestObject, TestUObjectUFunctionObserver));
}

  大多数注册的消息回调都支持原样取消注册,这样可以降低理解成本。您也不需要保存 FDelegateHandle 来取消注册。当然,唯一的例外是使用 Lambda 表达式作为回调时。这时只能记录FDelegateHandle,当不再需要的时候注销它,如:

    // register by common interface
    //     auto lambda = [](__TestParams) {
    //         strRef = TEXT("StrRef2");
    //         vecRef = FVector(7, 8, 9);
    //         };
    // FDelegateHandle Handle = EventCenter->Register<decltype(lambda), __TestParamsType>(FDebugEvent::GetEventName(), MoveTemp(lambda));

    // register by typesafe interface
    FDelegateHandle Handle = EventCenter->Register<FDebugEvent>([](__TestParams) {
        strRef = TEXT("StrRef2");
        vecRef = FVector(7, 8, 9);
        });

    // do something

    // unregister by handle
    EventCenter->UnRegister(FDebugEvent::GetEventName(), Handle);

  当然,其他情况也支持使用Handle注销。

  蓝图中,只需要使用UnRegister Global Event节点来取消注册,其参数与注册时相同。

 

调度事件(广播事件)

       在C++中发送事件需要使用UGameEventSubsystem的Broadcast、BroadcastDynamic等接口。
       对于Broadcast来说,有两种接口,一种是静态类型安全;一种是静态类型安全。另一个是动态类型安全:

    // If the parameter type does not match the event definition, a compilation error will be triggered
    EventCenter->Broadcast<FDebugEvent>(
        bValue,
        u8Value,
        i32Value,
        i64Value,
        enumValue,
        StrValue,
        StrRef,
        StrConstRef,
        NameValue,
        NameRef,
        VecValue,
        VecRef,
        VecConstRef,
        ObjectValue,
        IntArrayValue,
        IntArrayRef,
        StringArrayValue,
        StringArrayRef,
        ObjectArrayValue,
        ObjectArrayRef,
        IntSetValue,
        NameSetValue,
        NameStringMapValue,
        IntStringMapRef,
        IntObjectMap,
        StringObjectMap
    );

    // Parameter mismatch will not cause a compilation error, but an error message will be output at run time.
    // Template parameters are optional, but most of the time, in order to ensure that the parameter type is correctly inferred, template parameters should be provided. For example, C++ template inference cannot infer const TCHAR* to FString.
    EventCenter->Broadcast<__TestParamsType>(
        FDebugEvent::GetEventName(),
        bValue,
        u8Value,
        i32Value,
        i64Value,
        enumValue,
        StrValue,
        StrRef,
        StrConstRef,
        NameValue,
        NameRef,
        VecValue,
        VecRef,
        VecConstRef,
        ObjectValue,
        IntArrayValue,
        IntArrayRef,
        StringArrayValue,
        StringArrayRef,
        ObjectArrayValue,
        ObjectArrayRef,
        IntSetValue,
        NameSetValue,
        NameStringMapValue,
        IntStringMapRef,
        IntObjectMap,
        StringObjectMap
    );

  所谓静态安全就是我提供一个宏来定义一个事件。当定义这个事件时,它会强制约束一个事件和这个事件的参数列表,这样当你注册、注销或调度一个事件时参数不匹配(数量或类型),就会导致编译错误。您可以使用此宏定义静态事件:DEFINE_TYPESAFE_GLOBAL_EVENT :

#define __TestParamsType \
    bool, \
    uint8, \
    int32, \
    int64, \
    EGlobalEventParameterType, \
    FString, \
    FString&, \
    const FString&, \
    FName, \
    const FName&, \
    FVector, \
    FVector&, \
    const FVector&, \
    UObject*, \
    TArray<int>, \
    const TArray<int>&, \
    TArray<FString>, \
    const TArray<FString>&, \
    TArray<UObject*>, \
    const TArray<UObject*>&, \
    TSet<int>, \
    const TSet<FName>&, \
    TMap<FName, FString>, \
    const TMap<int, FString>&, \
    TMap<int, UObject*>, \
    const TMap<FString, UObject*>&
    
DEFINE_TYPESAFE_GLOBAL_EVENT(DebugEvent, __TestParamsType);

       要在蓝图中调度事件,您需要使用“Broadcast Global Event”节点,并且需要使用该节点上的“Add Argument”按钮来添加正确的参数。您需要确保参数的数量和类型与目标消息所需的参数数量相匹配。类型匹配,否则你派发的消息将无法正确投递,之前注册的回调函数也不会被触发。

      蓝图通过调用BroadcastDynamic接口发送消息,这也是其他脚本语言发送事件的方法。

常问问题

为什么4.25之前的版本不支持?

       因为我使用了FProperty相关的代码,不想再考虑UProperty兼容性的问题,于是我问ChatGPT:
       问:在 Unreal 的哪个版本中,FProperty 取代了 UProperty?从哪个版本开始,FProperty 停止支持 32 位?

       答:在虚幻引擎4中,从4.25版本开始,FProperty取代了UProperty。此更改是为了改进虚幻引擎的元数据系统和代码生成器。从版本 4.25 开始,您应该使用 FProperty 而不是 UProperty。此外,从虚幻引擎 4.25 开始,官方不再支持 32 位操作系统。虚幻引擎4.25及更高版本仅支持64位操作系统。如果您需要在 32 位操作系统上运行虚幻引擎,可以尝试使用 4.24 或更早版本。但请注意,旧版本可能缺少一些新功能和性能优化,并且可能不再获得官方支持和更新。为了获得最佳性能和功能,建议您在 64 位操作系统上使用最新版本的虚幻引擎。
       当然,如果您希望代码兼容较旧的引擎版本,并改进了相关代码,我会很高兴看到您的 Pull Request。

如何在其它脚本(例如 Type Script)中监视这些事件?

       UGameEventSubsystem 有一个公共全局回调 OnReceiveGlobalEvent。此回调是一个虚幻引擎的动态委托,因此大多数脚本应该能够访问此委托。你只需要注册这个委托,然后当收到全局消息时就会触发这个委托。该委托的参数是事件名称和 UDynamicEventContext 对象。通过前者可以获取消息名称,通过后者可以获取参数类型和参数数据。

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGlobalEventDelegateType, const FName&, EventName, UDynamicEventContext*, Context);

public:
    UPROPERTY(BlueprintAssignable)
    FGlobalEventDelegateType           OnReceiveGlobalEvent;

       这样就可以在脚本引擎中进一步触发相关代码。要发送消息,请使用 UDynamicEventFunctionLibrary::BroadcastEvent。您需要自己构造 UDynamicEventContext 对象并将其用作参数。

UFUNCTION(BlueprintCallable, Category = "Global Events", meta=(BlueprintInternalUseOnly = "true"))
static bool BroadcastEvent(FName EventName, UDynamicEventContext* Context);

       当然,您也可以将自己的 C++ 函数绑定到脚本并使用它来发送消息。
       至于在脚本内部注册消息回调,可以自己在脚本内部维护一个字典,当相关消息触发时,提取UDynamicEventContext对象中的参数来调用相应的脚本函数。

这个系统可以用在其它类中吗?     

       是的。通过查看源码可以发现,注册、注销、消息派发的接口其实都是通过include方法添加到UGameEventSubSystem中的。当然,你可以通过同样的方法将它们添加到你自己的类中。

UCLASS()
class GLOBALEVENTS_API UGameEventSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
    
public:
    /** Implement this for initialization of instances of the system */
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;

    /** Implement this for deinitialization of instances of the system */
    virtual void Deinitialize() override;

    static UGameEventSubsystem* GetInstance(const UObject* InContext);

#define ENABLE_EVENT_CENTER_ON_RECEIVE_GLOBAL_EVENT

public:
    UPROPERTY(BlueprintAssignable)
    FGlobalEventDelegateType           OnReceiveGlobalEvent;

public:
#if CPP
#include "Inline/EventCenterDataInterfacesInline.inl"
#include "Inline/EventCenterCommonInterfacesInline.inl"
#include "Inline/EventCenterTypeSafeInterfacesInline.inl"
#include "Inline/EventCenterDynamicInterfacesInline.inl"
#include "Inline/EventCenterSignatureInterfacesInline.inl"
#endif

#undef ENABLE_EVENT_CENTER_ON_RECEIVE_GLOBAL_EVENT
};

      如果不需要某些接口,可以不包含相应的inl文件。但是,EventCenterDataInterfacesInline.inl 是必需的。

 

posted @ 2023-10-18 20:36  bodong  阅读(109)  评论(0编辑  收藏  举报