From 9c5b8fe57fc4bc202a3ba3b0c94b19a2fbc525b4 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 19 Feb 2021 05:48:16 -0500 Subject: [PATCH] This was quite a fight; at least it compiles now... (no idea if works) Ugh, the type system in C++ is bad. It's hard to fit these things together. I'll have a ton of cleanup to do when done. --- README.md | 8 ++-- .../Private/EventSourceActor.cpp | 48 ++++++++++++++++++- .../Public/EventHandlerInterface.h | 46 ++++++++++++++++++ .../Public/EventSourceActor.h | 31 +++++++++++- server.py | 4 +- 5 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 Source/EventCenterPlugin/Public/EventHandlerInterface.h diff --git a/README.md b/README.md index 688fc9c..95f982d 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ I tried Redis PubSub first, but the plugin I found was buggy. I'm trying WebSock ## Installation -Here are the plugins I use: +Install the [BlueprintWebSocket](https://www.unrealengine.com/marketplace/en-US/product/blueprintwebsocket) +plugin (paid/commercial) from the Epic/Unreal Marketplace. It's not expensive and has great documentation. -* [SDRedis](https://www.unrealengine.com/marketplace/en-US/product/sdredis) (paid/commercial) - this is flaky and disconnects. Attempts to reconnect segfault. -* [BlueprintWebSocket](https://www.unrealengine.com/marketplace/en-US/product/blueprintwebsocket) (paid/commercial) - much better docs. Untested. +## Usage + +It's expected to wire events up in the `Level Blueprint`. ## Python testing diff --git a/Source/EventCenterPlugin/Private/EventSourceActor.cpp b/Source/EventCenterPlugin/Private/EventSourceActor.cpp index 4598514..0ce94c4 100644 --- a/Source/EventCenterPlugin/Private/EventSourceActor.cpp +++ b/Source/EventCenterPlugin/Private/EventSourceActor.cpp @@ -183,8 +183,22 @@ void AEventSourceActor::OnMessageSent(const FString& Message) { GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "OnMessageSent"); UE_LOG(EventSourceLog, Log, TEXT("OnMessageSent")); - UE_LOG(EventSourceLog, Log, TEXT("We just sent %s to the server."), *Message); + + + FString channel, payload; + bool result = Message.Split(TEXT("|"), &channel, &payload); + + if (!result) { + return; + } + + if (!delegateSubscriptions.Contains(channel)) { + return; + } + + delegateSubscriptions[channel].Execute(channel, payload); + } void AEventSourceActor::SetupWebSockets() @@ -206,3 +220,35 @@ void AEventSourceActor::SetupWebSockets() WebSocket->Connect(TEXT("ws://localhost:8765/"), TEXT("ws")); } +/*void AEventSourceActor::AssignEvent(FString& eventName, TScriptInterface handler) +{ + GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, "AssignEvent"); + UE_LOG(EventSourceLog, Log, TEXT("AssignEvent")); + + //TScriptDelegate ourDelegate = TScriptDelegate(); + + //ourDelegate.BindUFunction(handler.GetObject(), &IEventHandlerInterface::HandleEvent); + + IEventHandlerInterface* iface = Cast(handler.GetObject()); + + + //MyDelegate2.AddDynamic(iface, &IEventHandlerInterface::HandleEvent2); + //MyDelegate2.AddDynamic((UObject*)iface, &IEventHandlerInterface::HandleEvent2); + + //MyDelegate2.AddDynamic(handler.GetObject(), &IEventHandlerInterface::HandleEvent2); + //MyDelegate2.AddDynamic(handler.GetObject(), "HandleEvent2"); + + UObject* uobject = Cast(handler.GetObject()); + + //MyDelegate2.AddDynamic(handler->_getUObject(), &IEventHandlerInterface::HandleEvent2); +}*/ + +/*void AEventSourceActor::AssignEvent2(FString& eventName, UObject& object, FMySubscriberRefDelegate& theDelegate) +{ + +}*/ + +void AEventSourceActor::BindSingleDelegateForChannel(const FString& channel, const FMySubscriberRefDelegate& delegateDef) +{ + delegateSubscriptions.Add(channel, delegateDef); +} diff --git a/Source/EventCenterPlugin/Public/EventHandlerInterface.h b/Source/EventCenterPlugin/Public/EventHandlerInterface.h new file mode 100644 index 0000000..efaa1bb --- /dev/null +++ b/Source/EventCenterPlugin/Public/EventHandlerInterface.h @@ -0,0 +1,46 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "UObject/Interface.h" + + + +#include "EventHandlerInterface.generated.h" + +/** + * Interface for all objects that can handle events. + * + * My C++ foo isn't strong enough to accomplish this with templates, lambdas, + * or reuse of unreal's existing delegation system. + * + * Assuming this works, see: + * https://docs.unrealengine.com/en-US/ProgrammingAndScripting/GameplayArchitecture/Interfaces/index.html + */ +UINTERFACE(BlueprintType) +class EVENTCENTERPLUGIN_API UEventHandlerInterface : public UInterface +{ + GENERATED_BODY() + +/*public: + UEventHandlerInterface() {} + virtual ~UEventHandlerInterface() {}*/ + +}; + + +class EVENTCENTERPLUGIN_API IEventHandlerInterface +{ + GENERATED_BODY() + +public: + // Virtual functions cannot have UFUNCTION specifiers. + //virtual void HandleEvent(FString channel, FString message) = 0; + + // ^ NB(bt): Designation of "= 0" makes this a pure virtual method, + // and it also makes the class abstract. + + // Event callback for events dispatched with EventCenterPlugin. + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Event Callback") + void HandleEvent2(const FString& channel, const FString& message); +}; diff --git a/Source/EventCenterPlugin/Public/EventSourceActor.h b/Source/EventCenterPlugin/Public/EventSourceActor.h index 0295fad..c1cff3e 100644 --- a/Source/EventCenterPlugin/Public/EventSourceActor.h +++ b/Source/EventCenterPlugin/Public/EventSourceActor.h @@ -7,6 +7,9 @@ // RedisPlugin #include "RedisObject.h" +// Event system +#include "EventHandlerInterface.h" + // BlueprintWebSocketPlugin // Docs: https://github.com/Pandoa/BlueprintWebSocket #include "BlueprintWebSocketWrapper.h" @@ -16,6 +19,8 @@ DECLARE_LOG_CATEGORY_EXTERN(EventSourceLog, Log, All); +// TODO(bt): Make this not an actor. Consider UObject. Also consider a singleton. + /** * AEventSourceActor * @@ -24,6 +29,8 @@ DECLARE_LOG_CATEGORY_EXTERN(EventSourceLog, Log, All); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FMySubscriberDelegate, FString, Channel, FString, Message); +DECLARE_DYNAMIC_DELEGATE_TwoParams(FMySubscriberRefDelegate, const FString&, Channel, const FString&, Message); + // NB(bt): "Blueprintable" seems to remove the ability to add BP event nodes. UCLASS() class EVENTCENTERPLUGIN_API AEventSourceActor : public AActor @@ -63,14 +70,31 @@ public: UFUNCTION(BlueprintCallable, Category = "EventCenter") void Unsubscribe(const FString& Channel); - /** Again, what do. */ + /** This allows event binding from blueprint. */ UPROPERTY(BlueprintAssignable) FMySubscriberDelegate MyDelegate; + /** + * Bind a delegate to a channel. Note that we can't multicast. + */ + UFUNCTION(BlueprintCallable, Category = "EventCenter") + void BindSingleDelegateForChannel(const FString& channel, const FMySubscriberRefDelegate& delegateDef); + /** Set up websockets.*/ UFUNCTION(BlueprintCallable, Category = "EventCenter") void SetupWebSockets(); + /** This is an experiment */ + //UFUNCTION(BlueprintCallable, Category = "EventCenter") + //void AssignEvent(FString& eventName, TScriptInterface handler); + + + // This won't work because we don't know what the function is + //void AssignEvent2(FString& eventName, UObject& object, FMySubscriberRefDelegate& theDelegate); + + //FMySubscriberRefDelegate MyDelegate2; + + protected: // Called when the game starts or when spawned virtual void BeginPlay() override; @@ -91,6 +115,11 @@ private: UPROPERTY() UBlueprintWebSocket* WebSocket; + + // TODO(bt): Value type can be wrapped in a class to add additional routing. + // Multiple delegation signatures, "pause/muting", etc. + TMap delegateSubscriptions; + // Handle callbacks. // NB(bt): Declared 'UFUNCTION()' so reflection enables as a delegate callback. UFUNCTION() diff --git a/server.py b/server.py index 4aa41a0..ca670dd 100644 --- a/server.py +++ b/server.py @@ -76,7 +76,7 @@ async def server(websocket, path): #await websocket.send("Got message: {}".format(msg)) print('enqueuing websocket message', flush=True) - queue.put_nowait(msg) + queue.put_nowait("{}|{}".format(channel, msg)) asyncio.ensure_future(reader(mpsc)) @@ -98,7 +98,7 @@ async def server(websocket, path): await websocket.send("Websocket ping") if not queue.empty(): - msg = queue.get_nowait() # TODO THROWS EXCEPTION + msg = queue.get_nowait() # TODO THROWS EXCEPTION IF EMPTY (data race?) print('sending pubsub msg over websocket: {}'.format(msg), flush=True) await websocket.send(msg)