虚幻(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 是必需的。