├── .gitignore ├── LICENSE ├── README.md ├── Resources └── Icon128.png ├── SimpleEventPlugin.uplugin └── Source └── SimpleEventPlugin ├── AsyncActions └── WaitForSimpleEvent │ ├── WaitForSimpleEvent.cpp │ └── WaitForSimpleEvent.h ├── Module ├── SimpleEventPlugin.cpp └── SimpleEventPlugin.h ├── SimpleEventPlugin.Build.cs ├── SimpleEventSubsystem.cpp ├── SimpleEventSubsystem.h └── SimpleEventTypes.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | *.ipa 33 | 34 | # These project files can be generated by the engine 35 | *.xcodeproj 36 | *.xcworkspace 37 | *.sln 38 | *.suo 39 | *.opensdf 40 | *.sdf 41 | *.VC.db 42 | *.VC.opendb 43 | 44 | # Precompiled Assets 45 | SourceArt/**/*.png 46 | SourceArt/**/*.tga 47 | 48 | # Binary Files 49 | Binaries/* 50 | Plugins/**/Binaries/* 51 | 52 | # Builds 53 | Build/* 54 | 55 | # Whitelist PakBlacklist-.txt files 56 | !Build/*/ 57 | Build/*/** 58 | !Build/*/PakBlacklist*.txt 59 | 60 | # Don't ignore icon files in Build 61 | !Build/**/*.ico 62 | 63 | # Built data for maps 64 | *_BuiltData.uasset 65 | 66 | # Configuration files generated by the Editor 67 | Saved/* 68 | 69 | # Compiled source files for the engine to use 70 | Intermediate/* 71 | Plugins/**/Intermediate/* 72 | SimpleEventPlugin/Binaries/* 73 | SimpleEventPlugin/Intermediate/* 74 | 75 | # Cache files for the editor to use 76 | DerivedDataCache/* 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ahmed Elgoni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Event Plugin 2 | 3 | **Simple Event Plugin** is a lightweight Unreal Engine plugin that provides a subsystem to send and listen for events. 4 | Events can come with a payload that can be any UStruct you create in C++ or Blueprints (via [InstancedStruct](https://github.com/mattyman174/GenericItemization?tab=readme-ov-file#intro)). 5 | Anything that has access to the GameInstance (e.g Widgets, Pawns, PlayerControllers) can listen for and send events. 6 | 7 | ## API Summary 8 | 9 |
10 | Sending an event 11 | 12 | ![image](https://github.com/user-attachments/assets/dcf46f05-8053-463f-b7f3-3d448137650b) 13 | 14 |
15 | 16 | Events are identified using two [gameplay tags](https://www.tomlooman.com/unreal-engine-gameplaytags-data-driven-design/):
17 | - `EventTag` e.g `Events.UI.ButtonClicked` 18 | - `DomainTag` e.g `EventDomains.UI.PauseMenu` 19 | 20 | Events also have optional parameters: 21 | - `Payload`: You can use any struct as a payload to provide extra context to the event by passing it through the `MakeInstancedStruct` node. 22 | - `Sender`: This is an Actor input used to identify who sent the event. Later, when listening for an event you can filter by senders. 23 | 24 |
25 | 26 |
27 | Why use two tags instead of one? 28 | 29 |
30 | 31 | Using a domain tag can help cut down on repetitive tag names.
32 | 33 | For example, imagine we have a manager object that wants to know when any UI button gets clicked.
34 | 35 | Using a `DomainTag` we can use the following to identify the events of interest 36 | EventTag = `UI.ButtonClicked` 37 | DomainTags = `[EventDomains.MainMenu, EventDomains.OptionsMenu, EventDomains.PauseMenu]` 38 | 39 | Without a `DomainTag` we would need to listen for: 40 | EventTags = `[UI.MainMenu.ButtonClicked, UI.OptionsMenu.ButtonClicked, UI.PauseMenu.ButtonClicked]` 41 | 42 | Without a `DomainTag` we have 3 "ButtonClicked" tags and this looks messy in my opinion as we start to add more tags during development. 43 |
44 |
45 | 46 |
47 | Listening for an event 48 | 49 | ![image](https://github.com/user-attachments/assets/9d5ca9b4-fe8d-4a3e-9eeb-dcb85babc3b9) 50 | 51 | You can listen for multiple events and domains at the same time.
52 | If you leave `EventFilter` or `DomainFilter` empty then the delegate will be triggered for all events/domains. 53 | 54 | `OnlyTriggerOnce` will deregister the event listening actor after receving the event once. 55 | 56 | There are some additional arguments that are hidden by default in the `ListenForEvent` node:
57 | - `PayloadFilter`: The event won't trigger unless a payload exists and it is one of the specified struct types. If left empty the event will accept any payload. Default value is empty. 58 | - `SenderFilter`: The event won't trigger unless it was sent by one of the specified actors. If left empty the event will accept events from any actor. Default behaviour is empty. 59 | - `OnlyMatchExactEvent` & `OnlyMatchExactDomain`: if set to true, "A.B" will only match "A.B" and won't match "A.B.C" tags. By default they are set to only match tags exactly. 60 | 61 | The `ListenForEvent` node returns an ID representing the subscription which you can store and later reference to stop listening for this event. 62 |
63 | 64 |
65 | Receiving an event 66 | 67 | ![image](https://github.com/user-attachments/assets/82c3bbdb-f08a-4939-9d06-6fcffed21657) 68 | 69 |
70 | 71 | Use the `GetInstancedStructValue` node to cast to the type you expect.
72 | The output is initially a wildcard and you break your expected struct to cast the output type. 73 |
74 | 75 |
76 | Stop listening for an event 77 | 78 | There are three ways to stop listening for events: 79 | 80 | 81 | `StopListeningForAllEvents`: Remove all event subscriptions from a specified actor. 82 | 83 | `StopListeningForEventsByFilter`: Stop listening for a subset of events on a specified actor. 84 | 85 | `StopListeningForEventSubscriptionByID`: Stops listening for a specific event subscription. The event subscription ID is obtained from the output of the `ListenForEvent` node. 86 | 87 |
88 | 89 | ![image](https://github.com/user-attachments/assets/984b8db5-c0c3-4acb-8ca5-c3bcda7ae7c4) 90 | 91 | 92 |
93 | 94 |
95 | Utility Nodes 96 | 97 |
98 | 99 | `WaitForSimpleEvent`: This async node allows you to wait for an event and respond to it in the same place. 100 | 101 | ![image](https://github.com/user-attachments/assets/7666fe3d-6d7a-44a4-9dbc-0ffcf59d2a05) 102 | 103 |
104 | 105 |
106 | A note on replication 107 | 108 |
109 | 110 | The `SendEvent` function is not replicated i.e calling `SendEvent` on the client won't trigger a listener on the server and vice versa. 111 | `InstancedStruct`, which is the type of `Payload` in `SendEvent`, **can** be replicated though! So you can pass the payload though an RPC and the underlying wrapped struct will also replicate. e.g The server calls a multicast event which calls `SendEvent` on all connected clients 112 | 113 |
114 | 115 | ![image](https://github.com/user-attachments/assets/1d04328e-a038-4ea2-8035-c4115eda914e) 116 |
117 | 118 | ## Requirements 119 | 120 | - Unreal Engine 5.2* or higher. 121 | *instanced structs were introduced in 5.0 with the StructUtils plugin and got [replication support around 5.2](https://github.com/EpicGames/UnrealEngine/pull/9280). Thus, this plugin may work with UE < 5.2 but expect issues with replication. 122 | 123 | ## Installation Steps 124 | 125 | 1. Download or clone the SimpleEventPlugin folder from this repo into your Unreal Engine project under your project's [Plugins folder](https://dev.epicgames.com/documentation/en-us/unreal-engine/plugins-in-unreal-engine#pluginfolders), create the Plugins folder if it doesn't exist. (e.g. If your project folder is `C:\Projects\SimpleEventTest` then place the cloned `SimpleEventSubsystemPlugin` in `C:\Projects\SimpleEventTest\Plugins`) 126 | 2. Rebuild your project. 127 | 3. Enable the plugin in your Unreal Engine project by navigating to **Edit > Plugins** and searching for "SimpleEventPlugin". (it should be enabled by default) 128 | 129 | 130 | --- 131 | 132 | ### C++ Examples 133 | 134 | #### Sending Events 135 | 136 | ```cpp 137 | #include "SimpleEventSubsystem.h" 138 | #include "GameplayTagContainer.h" 139 | #include "InstancedStruct.h" 140 | 141 | void YourFunctionToSendEvent(UWorld* World) 142 | { 143 | if (USimpleEventSubsystem* EventSubsystem = World->GetGameInstance()->GetSubsystem()) 144 | { 145 | FGameplayTag EventTag = FGameplayTag::RequestGameplayTag(TEXT("Game.PlayerDied")); 146 | FGameplayTag DomainTag = FGameplayTag::RequestGameplayTag(TEXT("Domains.Game")); 147 | FInstancedStruct Payload = FInstancedStruct::Make(FVector::UpVector); 148 | 149 | EventSubsystem->SendEvent(EventTag, DomainTag, Payload, this); 150 | } 151 | } 152 | ``` 153 | 154 | #### Listening for Events 155 | 156 | ```cpp 157 | #include "SimpleEventSubsystem.h" 158 | #include "GameplayTagContainer.h" 159 | 160 | void YourFunctionToListenForEvent(UObject* Listener, UWorld* World) 161 | { 162 | if (USimpleEventSubsystem* EventSubsystem = World->GetGameInstance()->GetSubsystem()) 163 | { 164 | FGameplayTagContainer EventTags; 165 | EventTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Game.PlayerDied"))); 166 | 167 | FGameplayTagContainer DomainTags; 168 | DomainTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("Domains.Game"))); 169 | 170 | FSimpleEventDelegate EventDelegate; 171 | EventDelegate.BindDynamic(this, &YourClass::YourCallbackFunction); 172 | 173 | TArray PayloadFilter; 174 | // Note: StaticStruct is only available for structs defined with the USTRUCT() macro 175 | PayloadFilter.Add(FYourCustomStruct::StaticStruct()); 176 | 177 | TArray SenderFilter; 178 | 179 | EventSubsystem->ListenForEvent(Listener, false, EventTags, DomainTags, EventDelegate, PayloadFilter, SenderFilter); 180 | } 181 | } 182 | 183 | void YourClass::YourCallbackFunction(FGameplayTag EventTag, FGameplayTag Domain, FInstancedStruct Payload) 184 | { 185 | // Test if an instanced struct is a vector 186 | if (const FVector* TestVector = Payload.GetPtr()) 187 | { 188 | // Do something with the payload vector 189 | } 190 | 191 | UE_LOG(LogTemp, Log, TEXT("Event received: %s in domain: %s"), *EventTag.ToString(), *Domain.ToString()); 192 | } 193 | ``` 194 | 195 | #### Unsubscribing from Events 196 | 197 | ```cpp 198 | #include "SimpleEventSubsystem.h" 199 | 200 | void YourFunctionToUnsubscribe(UObject* Listener, UWorld* World, FGuid EventSubscriptionID) 201 | { 202 | if (USimpleEventSubsystem* EventSubsystem = World->GetGameInstance()->GetSubsystem()) 203 | { 204 | EventSubsystem->StopListeningForAllEvents(Listener); 205 | EventSubsystem->StopListeningForEventSubscriptionByID(Listener, EventSubscriptionID); 206 | 207 | FGameplayTagContainer EventFilter; 208 | FGameplayTagContainer DomainFilter; 209 | 210 | EventSubsystem->StopListeningForEventsByFilter(Listener, EventFilter, DomainFilter); 211 | } 212 | } 213 | ``` 214 | 215 | ## License 216 | 217 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 218 | 219 | --- 220 | 221 | ## Contributions 222 | 223 | Feel free to submit a pull request or file an issue on GitHub. Contributions are always welcome! 224 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strayTrain/SimpleEventSubsystemPlugin/9660bc12e27a23b14687b09b21949007ed85df05/Resources/Icon128.png -------------------------------------------------------------------------------- /SimpleEventPlugin.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "SimpleEventPlugin", 6 | "Description": "", 7 | "Category": "Other", 8 | "CreatedBy": "@strayTrain", 9 | "CreatedByURL": "https://github.com/strayTrain", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "SimpleEventPlugin", 20 | "Type": "Runtime", 21 | "LoadingPhase": "PreDefault" 22 | } 23 | ], 24 | "Plugins": [ 25 | { 26 | "Name": "StructUtils", 27 | "Enabled": true 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/AsyncActions/WaitForSimpleEvent/WaitForSimpleEvent.cpp: -------------------------------------------------------------------------------- 1 | #include "WaitForSimpleEvent.h" 2 | #include "SimpleEventPlugin/SimpleEventSubsystem.h" 3 | 4 | UWaitForSimpleEvent* UWaitForSimpleEvent::WaitForSimpleEvent(UObject* WorldContextObject, UObject* Listener, bool OnlyTriggerOnce, 5 | FGameplayTagContainer EventFilter, FGameplayTagContainer DomainFilter, TArray PayloadFilter, 6 | TArray SenderFilter, bool OnlyMatchExactEvent, bool OnlyMatchExactDomain) 7 | { 8 | //Create the task instance via NewObject 9 | UWaitForSimpleEvent* Task = NewObject(); 10 | 11 | Task->Listener = Listener; 12 | Task->OnlyTriggerOnce = OnlyTriggerOnce; 13 | Task->EventFilter = EventFilter; 14 | Task->DomainFilter = DomainFilter; 15 | Task->PayloadFilter = PayloadFilter, 16 | Task->SenderFilter = SenderFilter; 17 | Task->OnlyMatchExactEvent = OnlyMatchExactEvent; 18 | Task->OnlyMatchExactDomain = OnlyMatchExactDomain; 19 | Task->EventCallbackDelegate = FSimpleEventDelegate(); 20 | 21 | if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)) 22 | { 23 | Task->WorldContext = World; 24 | Task->RegisterWithGameInstance(World); 25 | } 26 | 27 | return Task; 28 | } 29 | 30 | void UWaitForSimpleEvent::Activate() 31 | { 32 | if (USimpleEventSubsystem* EventSubsystem = WorldContext->GetGameInstance()->GetSubsystem()) 33 | { 34 | EventCallbackDelegate.BindDynamic(this, &UWaitForSimpleEvent::OnSimpleEventReceived); 35 | EventID = EventSubsystem->ListenForEvent(Listener, OnlyTriggerOnce, EventFilter, DomainFilter, EventCallbackDelegate, PayloadFilter, SenderFilter); 36 | EventSubsystem->OnEventSubscriptionRemoved.AddDynamic(this, &UWaitForSimpleEvent::OnEventSubscriptionRemoved); 37 | } 38 | else 39 | { 40 | SetReadyToDestroy(); 41 | } 42 | } 43 | 44 | void UWaitForSimpleEvent::OnSimpleEventReceived(FGameplayTag AbilityTag, FGameplayTag DomainTag, FInstancedStruct Payload) 45 | { 46 | OnEventReceived.Broadcast(AbilityTag, DomainTag, Payload, EventID); 47 | 48 | if (OnlyTriggerOnce) 49 | { 50 | SetReadyToDestroy(); 51 | } 52 | } 53 | 54 | void UWaitForSimpleEvent::OnEventSubscriptionRemoved(FGuid SubscriptionID) 55 | { 56 | if (EventID == SubscriptionID) 57 | { 58 | SetReadyToDestroy(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/AsyncActions/WaitForSimpleEvent/WaitForSimpleEvent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "GameplayTagContainer.h" 5 | #include "InstancedStruct.h" 6 | #include "Kismet/BlueprintAsyncActionBase.h" 7 | #include "SimpleEventPlugin/SimpleEventTypes.h" 8 | #include "WaitForSimpleEvent.generated.h" 9 | 10 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams( 11 | FSimpleEventReceivedDelegate, 12 | FGameplayTag, EventTag, 13 | FGameplayTag, Domain, 14 | FInstancedStruct, Payload, 15 | FGuid, EventSubscriptionID 16 | ); 17 | 18 | UCLASS() 19 | class SIMPLEEVENTPLUGIN_API UWaitForSimpleEvent : public UBlueprintAsyncActionBase 20 | { 21 | GENERATED_BODY() 22 | 23 | public: 24 | UFUNCTION(BlueprintCallable, meta=(WorldContext = "WorldContextObject", BlueprintInternalUseOnly=true, AdvancedDisplay=5, AutoCreateRefTerm = "PayloadFilter,SenderFilter")) 25 | static UWaitForSimpleEvent* WaitForSimpleEvent( 26 | UObject* WorldContextObject, 27 | UObject* Listener, 28 | bool OnlyTriggerOnce, 29 | FGameplayTagContainer EventFilter, 30 | FGameplayTagContainer DomainFilter, 31 | TArray PayloadFilter, 32 | TArray SenderFilter, 33 | bool OnlyMatchExactEvent = true, 34 | bool OnlyMatchExactDomain = true); 35 | 36 | UPROPERTY(BlueprintAssignable) 37 | FSimpleEventReceivedDelegate OnEventReceived; 38 | 39 | virtual void Activate() override; 40 | 41 | protected: 42 | UFUNCTION() 43 | void OnSimpleEventReceived(FGameplayTag AbilityTag, FGameplayTag DomainTag, FInstancedStruct Payload); 44 | 45 | UFUNCTION() 46 | void OnEventSubscriptionRemoved(FGuid SubscriptionID); 47 | 48 | UWorld* WorldContext; 49 | UObject* Listener; 50 | FGuid EventID; 51 | FEventSubscription EventSubscription; 52 | bool OnlyTriggerOnce; 53 | FGameplayTagContainer EventFilter; 54 | FGameplayTagContainer DomainFilter; 55 | TArray PayloadFilter; 56 | TArray SenderFilter; 57 | bool OnlyMatchExactEvent = true; 58 | bool OnlyMatchExactDomain = true; 59 | FSimpleEventDelegate EventCallbackDelegate; 60 | }; 61 | -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/Module/SimpleEventPlugin.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "SimpleEventPlugin.h" 4 | 5 | #define LOCTEXT_NAMESPACE "FSimpleEventPluginModule" 6 | 7 | void FSimpleEventPluginModule::StartupModule() 8 | { 9 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 10 | } 11 | 12 | void FSimpleEventPluginModule::ShutdownModule() 13 | { 14 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 15 | // we call this function before unloading the module. 16 | } 17 | 18 | #undef LOCTEXT_NAMESPACE 19 | 20 | IMPLEMENT_MODULE(FSimpleEventPluginModule, SimpleEventPlugin) -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/Module/SimpleEventPlugin.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | class FSimpleEventPluginModule : public IModuleInterface 9 | { 10 | public: 11 | 12 | /** IModuleInterface implementation */ 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | }; -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/SimpleEventPlugin.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class SimpleEventPlugin : ModuleRules 6 | { 7 | public SimpleEventPlugin(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange( 12 | new string[] 13 | { 14 | "Core", 15 | "GameplayTags", 16 | "StructUtils" 17 | } 18 | ); 19 | 20 | 21 | PrivateDependencyModuleNames.AddRange( 22 | new string[] 23 | { 24 | "CoreUObject", 25 | "Engine", 26 | "Slate", 27 | "SlateCore", 28 | } 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/SimpleEventSubsystem.cpp: -------------------------------------------------------------------------------- 1 | #include "SimpleEventSubSystem.h" 2 | 3 | void USimpleEventSubsystem::SendEvent(FGameplayTag EventTag, FGameplayTag DomainTag, FInstancedStruct Payload, AActor* Sender) 4 | { 5 | // If we come across an invalid listener on a subscription (e.g the listener was garbage collected and forgot to unsubscribe) 6 | // we'll remove that subscription. We store the indexes of the invalid listeners here. 7 | TArray InvalidListenerIndexes; 8 | 9 | // We check subscriptions from the most recently added to the oldest 10 | for (int32 i = EventSubscriptions.Num() - 1; i >= 0; --i) 11 | { 12 | FEventSubscription& Subscription = EventSubscriptions[i]; 13 | 14 | const UObject* Listener = Subscription.ListenerObject.Get(); 15 | 16 | if (!Listener) 17 | { 18 | InvalidListenerIndexes.Add(i); 19 | continue; 20 | } 21 | 22 | if (!Subscription.EventFilter.IsEmpty()) 23 | { 24 | if (Subscription.OnlyMatchExactEvent) 25 | { 26 | if (!Subscription.EventFilter.HasTagExact(EventTag)) 27 | { 28 | continue; 29 | } 30 | } 31 | else 32 | { 33 | if (!Subscription.EventFilter.HasTag(EventTag)) 34 | { 35 | continue; 36 | } 37 | } 38 | } 39 | 40 | if (!Subscription.DomainFilter.IsEmpty()) 41 | { 42 | if (Subscription.OnlyMatchExactDomain) 43 | { 44 | if (!Subscription.DomainFilter.HasTagExact(DomainTag)) 45 | { 46 | continue; 47 | } 48 | } 49 | else 50 | { 51 | if (!Subscription.DomainFilter.HasTag(DomainTag)) 52 | { 53 | continue; 54 | } 55 | } 56 | } 57 | 58 | if (Subscription.PayloadFilter.Num() > 0) 59 | { 60 | if (!Payload.IsValid()) 61 | { 62 | UE_LOG( 63 | LogTemp, Warning, 64 | TEXT("No payload passed for Listener %s but the listener has a payload filter"), 65 | *Listener->GetName()); 66 | continue; 67 | } 68 | 69 | if (!Subscription.PayloadFilter.Contains(Payload.GetScriptStruct())) 70 | { 71 | UE_LOG( 72 | LogTemp, Warning, 73 | TEXT("Payload type %s does not pass the Listener payload filter"), 74 | *Payload.GetScriptStruct()->GetName()); 75 | continue; 76 | } 77 | } 78 | 79 | if (Subscription.SenderFilter.Num() > 0) 80 | { 81 | if (!Sender) 82 | { 83 | UE_LOG( 84 | LogTemp, Warning, 85 | TEXT("No sender passed for Listener %s but the listener has a sender filter"), 86 | *Listener->GetName()); 87 | continue; 88 | } 89 | 90 | if (!Subscription.SenderFilter.Contains(Sender)) 91 | { 92 | UE_LOG( 93 | LogTemp, Warning, 94 | TEXT("Sender %s does not pass the Listener sender filter"), 95 | *Sender->GetName()); 96 | continue; 97 | } 98 | } 99 | 100 | bool WasCalled = Subscription.CallbackDelegate.ExecuteIfBound(EventTag, DomainTag, Payload); 101 | 102 | if (!WasCalled) 103 | { 104 | UE_LOG(LogTemp, Warning, TEXT("Event %s passed filters but failed to call the delegate for Listener %s"), 105 | *EventTag.GetTagName().ToString(), *Listener->GetName()); 106 | } 107 | else if (Subscription.OnlyTriggerOnce) 108 | { 109 | InvalidListenerIndexes.Add(i); 110 | } 111 | } 112 | 113 | for (const int32 i : InvalidListenerIndexes) 114 | { 115 | EventSubscriptions.RemoveAt(i); 116 | } 117 | } 118 | 119 | FGuid USimpleEventSubsystem::ListenForEvent(UObject* Listener, bool OnlyTriggerOnce, FGameplayTagContainer EventFilter, 120 | FGameplayTagContainer DomainFilter, const FSimpleEventDelegate& EventReceivedDelegate, 121 | TArray PayloadFilter, TArray SenderFilter, bool OnlyMatchExactEvent, 122 | bool OnlyMatchExactDomain) 123 | { 124 | FEventSubscription Subscription; 125 | 126 | if (!Listener) 127 | { 128 | UE_LOG(LogTemp, Warning, TEXT("Null Listener passed to ListenForEvent. Can't listen for event.")); 129 | return Subscription.EventSubscriptionID; 130 | } 131 | 132 | if (!EventReceivedDelegate.IsBound()) 133 | { 134 | UE_LOG(LogTemp, Warning, TEXT("No delegate bound to ListenForEvent. Can't listen for event.")); 135 | return Subscription.EventSubscriptionID; 136 | } 137 | 138 | Subscription.EventSubscriptionID = FGuid::NewGuid(); 139 | Subscription.ListenerObject = Listener; 140 | Subscription.CallbackDelegate = EventReceivedDelegate; 141 | Subscription.EventFilter.AppendTags(EventFilter); 142 | Subscription.DomainFilter.AppendTags(DomainFilter); 143 | Subscription.PayloadFilter = PayloadFilter; 144 | Subscription.SenderFilter.Append(SenderFilter); 145 | Subscription.OnlyTriggerOnce = OnlyTriggerOnce; 146 | Subscription.OnlyMatchExactEvent = OnlyMatchExactEvent; 147 | Subscription.OnlyMatchExactDomain = OnlyMatchExactDomain; 148 | 149 | EventSubscriptions.Add(Subscription); 150 | return Subscription.EventSubscriptionID; 151 | } 152 | 153 | void USimpleEventSubsystem::StopListeningForEventSubscriptionByID(FGuid EventSubscriptionID) 154 | { 155 | EventSubscriptions.RemoveAll([this, EventSubscriptionID](const FEventSubscription& Subscription) 156 | { 157 | if (Subscription.EventSubscriptionID == EventSubscriptionID) 158 | { 159 | OnEventSubscriptionRemoved.Broadcast(Subscription.EventSubscriptionID); 160 | return true; 161 | } 162 | return false; 163 | }); 164 | } 165 | 166 | 167 | void USimpleEventSubsystem::StopListeningForEventsByFilter(UObject* Listener, FGameplayTagContainer EventTagFilter, FGameplayTagContainer DomainTagFilter) 168 | { 169 | EventSubscriptions.RemoveAll([this, Listener, EventTagFilter, DomainTagFilter](const FEventSubscription& Subscription) 170 | { 171 | if (Subscription.ListenerObject == Listener && 172 | (!EventTagFilter.Num() || EventTagFilter.HasAny(Subscription.EventFilter)) && 173 | (!DomainTagFilter.Num() || DomainTagFilter.HasAny(Subscription.DomainFilter))) 174 | { 175 | OnEventSubscriptionRemoved.Broadcast(Subscription.EventSubscriptionID); 176 | return true; 177 | } 178 | return false; 179 | }); 180 | } 181 | 182 | void USimpleEventSubsystem::StopListeningForAllEvents(UObject* Listener) 183 | { 184 | EventSubscriptions.RemoveAll([this, Listener](const FEventSubscription& Subscription) 185 | { 186 | if (Subscription.ListenerObject == Listener) 187 | { 188 | OnEventSubscriptionRemoved.Broadcast(Subscription.EventSubscriptionID); 189 | return true; 190 | } 191 | return false; 192 | }); 193 | } 194 | -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/SimpleEventSubsystem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Subsystems/GameInstanceSubsystem.h" 5 | #include "SimpleEventTypes.h" 6 | #include "SimpleEventSubsystem.generated.h" 7 | 8 | UCLASS() 9 | class SIMPLEEVENTPLUGIN_API USimpleEventSubsystem : public UGameInstanceSubsystem 10 | { 11 | GENERATED_BODY() 12 | 13 | public: 14 | /** 15 | * Sends an event to all listeners. The event is not replicated. 16 | * 17 | * @param EventTag The gameplay tag identifying the event. (mandatory) 18 | * @param DomainTag The domain tag categorizing the event. (optional) 19 | * @param Payload The payload of the event as an instanced struct (optional). 20 | * @param Sender The actor that sent the event. (optional) 21 | */ 22 | UFUNCTION(BlueprintCallable, Category = "SimpleEventSubsystem") 23 | void SendEvent(FGameplayTag EventTag, FGameplayTag DomainTag, FInstancedStruct Payload, AActor* Sender = nullptr); 24 | 25 | /** 26 | * Register a listener to receive events. The listener will be notified when an event is sent that matches the provided filters. 27 | * 28 | * @param Listener The object listening for the event (mandatory). 29 | * @param OnlyTriggerOnce If true, the listener will only be notified once and then automatically unsubscribed. 30 | * @param EventFilter Only listen for events with tags matching this filter. If not set, the listener will accept all events. 31 | * @param DomainFilter Only listen for events with domains matching this filter. If not set, the listener will accept events from all domains. 32 | * @param EventReceivedDelegate The FGlobalEventDelegate that gets called when the event is received and passes the filters (mandatory). 33 | * @param PayloadFilter Only respond to the event if there is a payload present and it is of this type. If not set, the listener will accept events with any (or no) payload. 34 | * @param SenderFilter Only respond to the event if the sender is in this list. If not set, the listener will accept events from any sender. 35 | * @param OnlyMatchExactEvent If true, only listen for events that match the filter tags exactly. i.e "A.B" will only match "A.B" and not "A.B.C". 36 | * @param OnlyMatchExactDomain If true, only listen for events that match the domain tags exactly. i.e "A.B" will only match "A.B" and not "A.B.C". 37 | */ 38 | UFUNCTION(BlueprintCallable, Category = "SimpleEventSubsystem", meta=(AdvancedDisplay=5, AutoCreateRefTerm = "PayloadFilter,SenderFilter")) 39 | FGuid ListenForEvent( 40 | UObject* Listener, 41 | bool OnlyTriggerOnce, 42 | FGameplayTagContainer EventFilter, 43 | FGameplayTagContainer DomainFilter, 44 | const FSimpleEventDelegate& EventReceivedDelegate, 45 | TArray PayloadFilter, 46 | TArray SenderFilter, 47 | bool OnlyMatchExactEvent = true, 48 | bool OnlyMatchExactDomain = true); 49 | 50 | /** 51 | * Stop listening for an event on a listener. 52 | * 53 | * @param EventSubscriptionID The ID of the event subscription to stop listening for (mandatory). 54 | */ 55 | UFUNCTION(BlueprintCallable, Category = "SimpleEventSubsystem") 56 | void StopListeningForEventSubscriptionByID(FGuid EventSubscriptionID); 57 | 58 | /** 59 | * Stop listening for events on a listener that match the provided filters. 60 | * 61 | * @param Listener The object listening for the event (mandatory). 62 | * @param EventTagFilter Stop listening for these events (mandatory). 63 | * @param DomainTagFilter Only stop listening if the domain also matches (optional). If not set, the listener will stop listening for events in EventTagFilter across all domains. 64 | */ 65 | UFUNCTION(BlueprintCallable, Category = "SimpleEventSubsystem") 66 | void StopListeningForEventsByFilter(UObject* Listener, FGameplayTagContainer EventTagFilter, FGameplayTagContainer DomainTagFilter); 67 | 68 | /** 69 | * Stop listening all for events on a listener 70 | * 71 | * @param Listener The object listening for the event (mandatory). 72 | */ 73 | UFUNCTION(BlueprintCallable, Category = "SimpleEventSubsystem") 74 | void StopListeningForAllEvents(UObject* Listener); 75 | 76 | UPROPERTY(BlueprintAssignable) 77 | FOnEventSubscriptionRemoved OnEventSubscriptionRemoved; 78 | 79 | private: 80 | TArray EventSubscriptions; 81 | }; -------------------------------------------------------------------------------- /Source/SimpleEventPlugin/SimpleEventTypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "GameplayTagContainer.h" 5 | #include "InstancedStruct.h" 6 | #include "SimpleEventTypes.generated.h" 7 | 8 | DECLARE_DYNAMIC_DELEGATE_ThreeParams( 9 | FSimpleEventDelegate, 10 | FGameplayTag, EventTag, 11 | FGameplayTag, Domain, 12 | FInstancedStruct, Payload); 13 | 14 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnEventSubscriptionRemoved, FGuid, EventSubscriptionID); 15 | 16 | USTRUCT(BlueprintType) 17 | struct FEventSubscription 18 | { 19 | GENERATED_BODY() 20 | 21 | /** 22 | * The ID of the event subscription. This is used to identify the subscription when removing it. 23 | */ 24 | UPROPERTY() 25 | FGuid EventSubscriptionID; 26 | 27 | /** 28 | * The delegate to call when an event is received that passes all the filters 29 | */ 30 | UPROPERTY() 31 | FSimpleEventDelegate CallbackDelegate; 32 | 33 | /** 34 | * The object to call the delegate on 35 | */ 36 | UPROPERTY() 37 | TWeakObjectPtr ListenerObject; 38 | 39 | /** 40 | * Only listen for events with these event tags. If empty, all event tags are accepted. 41 | * Event tags examples: "UI.ButtonClicked" or "Game.PlayerDied" 42 | */ 43 | UPROPERTY() 44 | FGameplayTagContainer EventFilter; 45 | 46 | /** 47 | * Only listen for events with these domain tags. If empty, all domain tags are accepted. 48 | * Event tags examples: "Domains.UI" or "Domains.Game" 49 | */ 50 | UPROPERTY() 51 | FGameplayTagContainer DomainFilter; 52 | 53 | /** 54 | * Only listen for events with these payload types. If empty, all payload types are accepted. 55 | */ 56 | UPROPERTY() 57 | TArray PayloadFilter; 58 | 59 | /** 60 | * Only listen for events from these sender actors. If empty, all sender actors are accepted. 61 | * Note: Sender actors are an optional parameter when sending events which means that some events might not have a sender actor. 62 | */ 63 | UPROPERTY() 64 | TArray> SenderFilter; 65 | 66 | /** 67 | * Only trigger the delegate once. After the first event is received, the listener is removed. 68 | */ 69 | UPROPERTY() 70 | bool OnlyTriggerOnce = false; 71 | 72 | /** 73 | * Only trigger the delegate if the event tags match exactly. i.e "A.B" will only match "A.B" and not "A.B.C". 74 | */ 75 | UPROPERTY() 76 | bool OnlyMatchExactEvent = true; 77 | 78 | /** 79 | * Only trigger the delegate if the domain tags match exactly. i.e "A.B" will only match "A.B" and not "A.B.C". 80 | */ 81 | UPROPERTY() 82 | bool OnlyMatchExactDomain = true; 83 | }; --------------------------------------------------------------------------------