├── .gitignore ├── GameplayMessageRouter.uplugin ├── LICENSE ├── README.md ├── Resources └── Icon128.png └── Source ├── GameplayMessageNodes ├── GameplayMessageNodes.Build.cs ├── Private │ ├── GameplayMessageNodesModule.cpp │ └── K2Node_AsyncAction_ListenForGameplayMessages.cpp └── Public │ └── K2Node_AsyncAction_ListenForGameplayMessages.h └── GameplayMessageRuntime ├── GameplayMessageRuntime.Build.cs ├── Private └── GameFramework │ ├── AsyncAction_ListenForGameplayMessage.cpp │ ├── GameplayMessageProcessor.cpp │ ├── GameplayMessageRuntime.cpp │ └── GameplayMessageSubsystem.cpp └── Public └── GameFramework ├── AsyncAction_ListenForGameplayMessage.h ├── GameplayMessageProcessor.h ├── GameplayMessageSubsystem.h └── GameplayMessageTypes2.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 | 73 | # Cache files for the editor to use 74 | DerivedDataCache/* 75 | -------------------------------------------------------------------------------- /GameplayMessageRouter.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "Gameplay Message Subsystem", 6 | "Description": "A subsystem that allows registering for and sending messages between unconnected gameplay objects.", 7 | "Category": "Gameplay", 8 | "CreatedBy": "Epic Games, Inc.", 9 | "ModifiedBy": "Broken Rock Studios", 10 | "CreatedByURL": "http://epicgames.com", 11 | "DocsURL": "", 12 | "MarketplaceURL": "", 13 | "SupportURL": "https://github.com/brokenrockstudios/GameplayMessageRouter", 14 | "EnabledByDefault": true, 15 | "CanContainContent": false, 16 | "IsBetaVersion": false, 17 | "Installed": false, 18 | "Modules": [ 19 | { 20 | "Name": "GameplayMessageRuntime", 21 | "Type": "Runtime", 22 | "LoadingPhase": "Default" 23 | }, 24 | { 25 | "Name": "GameplayMessageNodes", 26 | "Type": "UncookedOnly", 27 | "LoadingPhase": "Default" 28 | } 29 | ], 30 | "Plugins": [ 31 | { 32 | "Name": "GameplayTagsEditor", 33 | "Enabled": true 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Important Notice Regarding Unreal Engine Related Code 2 | 3 | This project contains code that is derived from or intended for use with Unreal Engine, which is provided by Epic Games, Inc. under the Unreal Engine End User License Agreement (EULA). 4 | By using or distributing this code, you agree to comply with the terms of the Unreal Engine EULA, available at: 5 | - Unreal Engine EULA: https://www.unrealengine.com/eula 6 | - Unreal Engine Content EULA: https://www.unrealengine.com/eula/content 7 | 8 | This software is **not affiliated with, endorsed by, or sponsored by Epic Games, Inc.** 9 | 10 | --- 11 | 12 | MIT License 13 | 14 | Copyright (c) 2025 Broken Rock Studios 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GameplayMessageRouter 2 | A community plugin version of GameplayMessage Processor from the Epic's Lyra Sample project 3 | 4 | The GameplayMessageRouter acts as a centralized hub for routing, processing, and dispatching gameplay 5 | messages. It decouples message senders from listeners, making your game's event communication more 6 | modular, scalable, and easier to maintain 7 | 8 | ## 📖 Overview 9 | 10 | The GameplayMessageRouter is designed to streamline communication between disparate gameplay components by: 11 | * Centralizing message routing 12 | * All gameplay messages, events, state changes, or notifications are managed by a single router 13 | * Decoupling components 14 | * Senders and receivers don't need direct references to each other, reducing dependencies and simplifying maintenance 15 | * Supporting both C++ and Blueprints. 16 | * Can easily integrate into any combination of language 17 | 18 | By using this system, you can easily add/remove, or update interactions between separate game systems without 19 | modifying each individual module. 20 | 21 | ## 🛠️ Usage 22 | 23 | 24 | ### Blueprints 25 | TODO pictures 26 | 27 | 28 | ### C++ 29 | 30 | Broadcast a message 31 | ```cpp 32 | FYourStructMessage Message; 33 | // Message.SomeValue = SomeOtherValue; 34 | ...populate message 35 | 36 | UGameplayMessageSubsystem& MessageSystem = UGameplayMessageSubsystem::Get(this); 37 | MessageSystem.BroadcastMessage(SomeGameplayTag, Message); 38 | ``` 39 | 40 | Register a listener 41 | ```cpp 42 | UGameplayMessageSubsystem& MessageSubsystem = UGameplayMessageSubsystem::Get(this); 43 | FGameplayMessageListenerHandle ListenerHandle =MessageSubsystem.RegisterListener(SomeGameplayTag, this, &ThisClass::OnSomeMessage); 44 | ... 45 | void YourClass::OnSomeMessage(FGameplayTag Channel, const FYourStructMessage& Payload) {} 46 | ``` 47 | 48 | Unregister a listener 49 | ```cpp 50 | UGameplayMessageSubsystem& MessageSubsystem = UGameplayMessageSubsystem::Get(this); 51 | MessageSubsystem.UnregisterListener(ListenerHandle); 52 | ``` 53 | 54 | 55 | ## 📦 Installation 56 | 57 | 1. Download or clone and place in your `Plugins` folder. 58 | 2. For C++ projects, add the following line to your `YourProject.Build.cs` file: 59 | ```csharp 60 | PublicDependencyModuleNames.AddRange(new string[] { "GameplayMessageRuntime" }); 61 | ``` 62 | Rebuild the project 63 | 64 | ### From Lyra 65 | 66 | Remove the old plugin and GameplayMessageProcessor.h/.cpp 67 | 68 | # 📃 License 69 | This project is a fork of Epic Games' codebase and is therefor subject to the Unreal Engine EULA and the Unreal Engine Content EULA. 70 | Any portions of the code that are not covered by Epic's EULA are licensed under the MIT License, as specified in the LICENSE file. 71 | 72 | 73 | # 🔄Notable changes from original 74 | (Not an exhaustive list) 75 | 76 | * Added GameplayMessageProcessor 77 | 78 | # 🔗 Relevant Links 79 | 80 | * Lyra Sample Project Documentation: https://dev.epicgames.com/documentation/en-us/unreal-engine/lyra-sample-game-in-unreal-engine 81 | * Download Lyra Sample Project: https://www.fab.com/listings/93faede1-4434-47c0-85f1-bf27c0820ad0 82 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brokenrockstudios/GameplayMessageRouter/b2d257239054818df224f5f4a947a7d4ba6d4062/Resources/Icon128.png -------------------------------------------------------------------------------- /Source/GameplayMessageNodes/GameplayMessageNodes.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | using UnrealBuildTool; 5 | 6 | public class GameplayMessageNodes : ModuleRules 7 | { 8 | public GameplayMessageNodes(ReadOnlyTargetRules Target) : base(Target) 9 | { 10 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 11 | 12 | PrivateDependencyModuleNames.AddRange( 13 | new string[] 14 | { 15 | "Core", 16 | "CoreUObject", 17 | "Engine", 18 | "KismetCompiler", 19 | "PropertyEditor", 20 | "GameplayMessageRuntime", 21 | "UnrealEd" 22 | } 23 | ); 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] 27 | { 28 | "BlueprintGraph", 29 | } 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Source/GameplayMessageNodes/Private/GameplayMessageNodesModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | #include "Modules/ModuleManager.h" 5 | 6 | IMPLEMENT_MODULE(FDefaultModuleImpl, GameplayMessageNodes); 7 | -------------------------------------------------------------------------------- /Source/GameplayMessageNodes/Private/K2Node_AsyncAction_ListenForGameplayMessages.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // Modified by Broken Rock Studios 3 | // See the LICENSE file for details. 4 | 5 | #include "K2Node_AsyncAction_ListenForGameplayMessages.h" 6 | 7 | #include "BlueprintActionDatabaseRegistrar.h" 8 | #include "BlueprintFunctionNodeSpawner.h" 9 | #include "K2Node_AssignmentStatement.h" 10 | #include "K2Node_AsyncAction.h" 11 | #include "K2Node_CallFunction.h" 12 | #include "K2Node_TemporaryVariable.h" 13 | #include "KismetCompiler.h" 14 | #include "EdGraph/EdGraph.h" 15 | #include "GameFramework/AsyncAction_ListenForGameplayMessage.h" 16 | 17 | #include UE_INLINE_GENERATED_CPP_BY_NAME(K2Node_AsyncAction_ListenForGameplayMessages) 18 | 19 | class UEdGraph; 20 | 21 | #define LOCTEXT_NAMESPACE "K2Node" 22 | 23 | namespace UK2Node_AsyncAction_ListenForGameplayMessagesHelper 24 | { 25 | static FName ActualChannelPinName = "ActualChannel"; 26 | static FName PayloadPinName = "Payload"; 27 | static FName PayloadTypePinName = "PayloadType"; 28 | static FName DelegateProxyPinName = "ProxyObject"; 29 | }; 30 | 31 | void UK2Node_AsyncAction_ListenForGameplayMessages::PostReconstructNode() 32 | { 33 | Super::PostReconstructNode(); 34 | 35 | RefreshOutputPayloadType(); 36 | } 37 | 38 | void UK2Node_AsyncAction_ListenForGameplayMessages::PinDefaultValueChanged(UEdGraphPin* ChangedPin) 39 | { 40 | if (ChangedPin == GetPayloadTypePin()) 41 | { 42 | if (ChangedPin->LinkedTo.Num() == 0) 43 | { 44 | RefreshOutputPayloadType(); 45 | } 46 | } 47 | } 48 | 49 | void UK2Node_AsyncAction_ListenForGameplayMessages::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const 50 | { 51 | Super::GetPinHoverText(Pin, HoverTextOut); 52 | if (Pin.PinName == UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadPinName) 53 | { 54 | HoverTextOut = HoverTextOut + LOCTEXT("PayloadOutTooltip", "\n\nThe message structure that we received").ToString(); 55 | } 56 | } 57 | 58 | void UK2Node_AsyncAction_ListenForGameplayMessages::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const 59 | { 60 | struct GetMenuActions_Utils 61 | { 62 | static void SetNodeFunc(UEdGraphNode* NewNode, bool /*bIsTemplateNode*/, TWeakObjectPtr FunctionPtr) 63 | { 64 | UK2Node_AsyncAction_ListenForGameplayMessages* AsyncTaskNode = CastChecked(NewNode); 65 | if (FunctionPtr.IsValid()) 66 | { 67 | const UFunction* Func = FunctionPtr.Get(); 68 | const FObjectProperty* ReturnProp = CastFieldChecked(Func->GetReturnProperty()); 69 | 70 | AsyncTaskNode->ProxyFactoryFunctionName = Func->GetFName(); 71 | AsyncTaskNode->ProxyFactoryClass = Func->GetOuterUClass(); 72 | AsyncTaskNode->ProxyClass = ReturnProp->PropertyClass; 73 | } 74 | } 75 | }; 76 | 77 | UClass* NodeClass = GetClass(); 78 | ActionRegistrar.RegisterClassFactoryActions(FBlueprintActionDatabaseRegistrar::FMakeFuncSpawnerDelegate::CreateLambda([NodeClass](const UFunction* FactoryFunc)->UBlueprintNodeSpawner* 79 | { 80 | UBlueprintNodeSpawner* NodeSpawner = UBlueprintFunctionNodeSpawner::Create(FactoryFunc); 81 | check(NodeSpawner != nullptr); 82 | NodeSpawner->NodeClass = NodeClass; 83 | 84 | TWeakObjectPtr FunctionPtr = MakeWeakObjectPtr(const_cast(FactoryFunc)); 85 | NodeSpawner->CustomizeNodeDelegate = UBlueprintNodeSpawner::FCustomizeNodeDelegate::CreateStatic(GetMenuActions_Utils::SetNodeFunc, FunctionPtr); 86 | 87 | return NodeSpawner; 88 | }) ); 89 | } 90 | 91 | void UK2Node_AsyncAction_ListenForGameplayMessages::AllocateDefaultPins() 92 | { 93 | Super::AllocateDefaultPins(); 94 | 95 | // The output of the UAsyncAction_ListenForGameplayMessage delegates is a proxy object which is used in the follow up call of GetPayload when triggered 96 | // This is only needed in the internals of this node so hide the pin from the editor. 97 | UEdGraphPin* DelegateProxyPin = FindPin(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::DelegateProxyPinName); 98 | if (ensure(DelegateProxyPin)) 99 | { 100 | DelegateProxyPin->bHidden = true; 101 | } 102 | 103 | CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadPinName); 104 | } 105 | 106 | bool UK2Node_AsyncAction_ListenForGameplayMessages::HandleDelegates(const TArray& VariableOutputs, UEdGraphPin* ProxyObjectPin, UEdGraphPin*& InOutLastThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext) 107 | { 108 | bool bIsErrorFree = true; 109 | 110 | if (VariableOutputs.Num() != 3) 111 | { 112 | ensureMsgf(false, TEXT("UK2Node_AsyncAction_ListenForGameplayMessages::HandleDelegates - Variable output array not valid. Output delegates must only have the single proxy object output and than must have pin for payload.")); 113 | return false; 114 | } 115 | 116 | for (TFieldIterator PropertyIt(ProxyClass); PropertyIt && bIsErrorFree; ++PropertyIt) 117 | { 118 | UEdGraphPin* LastActivatedThenPin = nullptr; 119 | bIsErrorFree &= FBaseAsyncTaskHelper::HandleDelegateImplementation(*PropertyIt, VariableOutputs, ProxyObjectPin, InOutLastThenPin, LastActivatedThenPin, this, SourceGraph, CompilerContext); 120 | 121 | bIsErrorFree &= HandlePayloadImplementation(*PropertyIt, VariableOutputs[0], VariableOutputs[2], VariableOutputs[1], LastActivatedThenPin, SourceGraph, CompilerContext); 122 | } 123 | 124 | return bIsErrorFree; 125 | } 126 | 127 | bool UK2Node_AsyncAction_ListenForGameplayMessages::HandlePayloadImplementation(FMulticastDelegateProperty* CurrentProperty, const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ProxyObjectVar, const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& PayloadVar, const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ActualChannelVar, UEdGraphPin*& InOutLastActivatedThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext) 128 | { 129 | bool bIsErrorFree = true; 130 | const UEdGraphPin* PayloadPin = GetPayloadPin(); 131 | const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); 132 | 133 | check(CurrentProperty && SourceGraph && Schema); 134 | 135 | const FEdGraphPinType& PinType = PayloadPin->PinType; 136 | 137 | if (PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard) 138 | { 139 | if (PayloadPin->LinkedTo.Num() == 0) 140 | { 141 | // If no payload type is specified, and we're not trying to connect the output to anything ignore the rest of this step 142 | return true; 143 | } 144 | else 145 | { 146 | return false; 147 | } 148 | } 149 | 150 | UK2Node_TemporaryVariable* TempVarOutput = CompilerContext.SpawnInternalVariable( 151 | this, PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject.Get(), PinType.ContainerType, PinType.PinValueType); 152 | 153 | UK2Node_CallFunction* const CallGetPayloadNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 154 | CallGetPayloadNode->FunctionReference.SetExternalMember(TEXT("GetPayload"), CurrentProperty->GetOwnerClass()); 155 | CallGetPayloadNode->AllocateDefaultPins(); 156 | 157 | // Hook up the self connection 158 | UEdGraphPin* GetPayloadCallSelfPin = Schema->FindSelfPin(*CallGetPayloadNode, EGPD_Input); 159 | if (GetPayloadCallSelfPin) 160 | { 161 | bIsErrorFree &= Schema->TryCreateConnection(GetPayloadCallSelfPin, ProxyObjectVar.TempVar->GetVariablePin()); 162 | 163 | // Hook the activate node up in the exec chain 164 | UEdGraphPin* GetPayloadExecPin = CallGetPayloadNode->FindPinChecked(UEdGraphSchema_K2::PN_Execute); 165 | UEdGraphPin* GetPayloadThenPin = CallGetPayloadNode->FindPinChecked(UEdGraphSchema_K2::PN_Then); 166 | 167 | UEdGraphPin* LastThenPin = nullptr; 168 | UEdGraphPin* GetPayloadPin = CallGetPayloadNode->FindPinChecked(TEXT("OutPayload")); 169 | bIsErrorFree &= Schema->TryCreateConnection(TempVarOutput->GetVariablePin(), GetPayloadPin); 170 | 171 | 172 | UK2Node_AssignmentStatement* AssignNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 173 | AssignNode->AllocateDefaultPins(); 174 | bIsErrorFree &= Schema->TryCreateConnection(GetPayloadThenPin, AssignNode->GetExecPin()); 175 | bIsErrorFree &= Schema->TryCreateConnection(PayloadVar.TempVar->GetVariablePin(), AssignNode->GetVariablePin()); 176 | AssignNode->NotifyPinConnectionListChanged(AssignNode->GetVariablePin()); 177 | bIsErrorFree &= Schema->TryCreateConnection(AssignNode->GetValuePin(), TempVarOutput->GetVariablePin()); 178 | AssignNode->NotifyPinConnectionListChanged(AssignNode->GetValuePin()); 179 | 180 | 181 | bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*InOutLastActivatedThenPin, *AssignNode->GetThenPin()).CanSafeConnect(); 182 | bIsErrorFree &= Schema->TryCreateConnection(InOutLastActivatedThenPin, GetPayloadExecPin); 183 | 184 | // Hook up the actual channel connection 185 | UEdGraphPin* OutActualChannelPin = GetOutputChannelPin(); 186 | bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*OutActualChannelPin, *ActualChannelVar.TempVar->GetVariablePin()).CanSafeConnect(); 187 | } 188 | 189 | return bIsErrorFree; 190 | } 191 | 192 | void UK2Node_AsyncAction_ListenForGameplayMessages::RefreshOutputPayloadType() 193 | { 194 | UEdGraphPin* PayloadPin = GetPayloadPin(); 195 | const UEdGraphPin* PayloadTypePin = GetPayloadTypePin(); 196 | 197 | if (PayloadTypePin->DefaultObject != PayloadPin->PinType.PinSubCategoryObject) 198 | { 199 | if (PayloadPin->SubPins.Num() > 0) 200 | { 201 | GetSchema()->RecombinePin(PayloadPin); 202 | } 203 | 204 | PayloadPin->PinType.PinSubCategoryObject = PayloadTypePin->DefaultObject; 205 | PayloadPin->PinType.PinCategory = (PayloadTypePin->DefaultObject == nullptr) ? UEdGraphSchema_K2::PC_Wildcard : UEdGraphSchema_K2::PC_Struct; 206 | } 207 | } 208 | 209 | UEdGraphPin* UK2Node_AsyncAction_ListenForGameplayMessages::GetPayloadPin() const 210 | { 211 | UEdGraphPin* Pin = FindPinChecked(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadPinName); 212 | check(Pin->Direction == EGPD_Output); 213 | return Pin; 214 | } 215 | 216 | UEdGraphPin* UK2Node_AsyncAction_ListenForGameplayMessages::GetPayloadTypePin() const 217 | { 218 | UEdGraphPin* Pin = FindPinChecked(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::PayloadTypePinName); 219 | check(Pin->Direction == EGPD_Input); 220 | return Pin; 221 | } 222 | 223 | UEdGraphPin* UK2Node_AsyncAction_ListenForGameplayMessages::GetOutputChannelPin() const 224 | { 225 | UEdGraphPin* Pin = FindPinChecked(UK2Node_AsyncAction_ListenForGameplayMessagesHelper::ActualChannelPinName); 226 | check(Pin->Direction == EGPD_Output); 227 | return Pin; 228 | } 229 | 230 | #undef LOCTEXT_NAMESPACE 231 | 232 | -------------------------------------------------------------------------------- /Source/GameplayMessageNodes/Public/K2Node_AsyncAction_ListenForGameplayMessages.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | #pragma once 5 | 6 | #include "K2Node_AsyncAction.h" 7 | 8 | #include "K2Node_AsyncAction_ListenForGameplayMessages.generated.h" 9 | 10 | class FBlueprintActionDatabaseRegistrar; 11 | class FKismetCompilerContext; 12 | class FMulticastDelegateProperty; 13 | class FString; 14 | class UEdGraph; 15 | class UEdGraphPin; 16 | class UObject; 17 | 18 | /** 19 | * Blueprint node which is spawned to handle the async logic for UAsyncAction_RegisterGameplayMessageReceiver 20 | */ 21 | 22 | UCLASS() 23 | class UK2Node_AsyncAction_ListenForGameplayMessages : public UK2Node_AsyncAction 24 | { 25 | GENERATED_BODY() 26 | 27 | //~UEdGraphNode interface 28 | virtual void PostReconstructNode() override; 29 | virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin) override; 30 | virtual void GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const override; 31 | //~End of UEdGraphNode interface 32 | 33 | //~UK2Node interface 34 | virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; 35 | virtual void AllocateDefaultPins() override; 36 | //~End of UK2Node interface 37 | 38 | protected: 39 | virtual bool HandleDelegates( 40 | const TArray& VariableOutputs, UEdGraphPin* ProxyObjectPin, 41 | UEdGraphPin*& InOutLastThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext) override; 42 | 43 | private: 44 | 45 | // Add the GetPayload flow to the end of the delegate handler's logic chain 46 | bool HandlePayloadImplementation( 47 | FMulticastDelegateProperty* CurrentProperty, 48 | const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ProxyObjectVar, 49 | const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& PayloadVar, 50 | const FBaseAsyncTaskHelper::FOutputPinAndLocalVariable& ActualChannelVar, 51 | UEdGraphPin*& InOutLastActivatedThenPin, UEdGraph* SourceGraph, FKismetCompilerContext& CompilerContext); 52 | 53 | // Make sure the output Payload wildcard matches the input PayloadType 54 | void RefreshOutputPayloadType(); 55 | 56 | UEdGraphPin* GetPayloadPin() const; 57 | UEdGraphPin* GetPayloadTypePin() const; 58 | UEdGraphPin* GetOutputChannelPin() const; 59 | }; 60 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/GameplayMessageRuntime.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | using UnrealBuildTool; 5 | 6 | public class GameplayMessageRuntime : ModuleRules 7 | { 8 | public GameplayMessageRuntime(ReadOnlyTargetRules Target) : base(Target) 9 | { 10 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 11 | 12 | PublicDependencyModuleNames.AddRange( 13 | new string[] 14 | { 15 | "Core", 16 | "Engine", 17 | "GameplayTags" 18 | }); 19 | 20 | PrivateDependencyModuleNames.AddRange( 21 | new string[] 22 | { 23 | "CoreUObject", 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Private/GameFramework/AsyncAction_ListenForGameplayMessage.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | #include "GameFramework/AsyncAction_ListenForGameplayMessage.h" 5 | 6 | #include "Engine/Engine.h" 7 | #include "Engine/World.h" 8 | #include "GameFramework/GameplayMessageSubsystem.h" 9 | #include "UObject/ScriptMacros.h" 10 | #include "UObject/Stack.h" 11 | 12 | #include UE_INLINE_GENERATED_CPP_BY_NAME(AsyncAction_ListenForGameplayMessage) 13 | 14 | UAsyncAction_ListenForGameplayMessage* UAsyncAction_ListenForGameplayMessage::ListenForGameplayMessages(UObject* WorldContextObject, FGameplayTag Channel, UScriptStruct* PayloadType, EGameplayMessageMatch MatchType) 15 | { 16 | UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); 17 | if (!World) 18 | { 19 | return nullptr; 20 | } 21 | 22 | UAsyncAction_ListenForGameplayMessage* Action = NewObject(); 23 | Action->WorldPtr = World; 24 | Action->ChannelToRegister = Channel; 25 | Action->MessageStructType = PayloadType; 26 | Action->MessageMatchType = MatchType; 27 | Action->RegisterWithGameInstance(World); 28 | 29 | return Action; 30 | } 31 | 32 | void UAsyncAction_ListenForGameplayMessage::Activate() 33 | { 34 | if (UWorld* World = WorldPtr.Get()) 35 | { 36 | if (UGameplayMessageSubsystem::HasInstance(World)) 37 | { 38 | UGameplayMessageSubsystem& Router = UGameplayMessageSubsystem::Get(World); 39 | 40 | TWeakObjectPtr WeakThis(this); 41 | ListenerHandle = Router.RegisterListenerInternal(ChannelToRegister, 42 | [WeakThis](FGameplayTag Channel, const UScriptStruct* StructType, const void* Payload) 43 | { 44 | if (UAsyncAction_ListenForGameplayMessage* StrongThis = WeakThis.Get()) 45 | { 46 | StrongThis->HandleMessageReceived(Channel, StructType, Payload); 47 | } 48 | }, 49 | MessageStructType.Get(), 50 | MessageMatchType); 51 | 52 | return; 53 | } 54 | } 55 | 56 | SetReadyToDestroy(); 57 | } 58 | 59 | void UAsyncAction_ListenForGameplayMessage::SetReadyToDestroy() 60 | { 61 | ListenerHandle.Unregister(); 62 | 63 | Super::SetReadyToDestroy(); 64 | } 65 | 66 | bool UAsyncAction_ListenForGameplayMessage::GetPayload(int32& OutPayload) 67 | { 68 | checkNoEntry(); 69 | return false; 70 | } 71 | 72 | DEFINE_FUNCTION(UAsyncAction_ListenForGameplayMessage::execGetPayload) 73 | { 74 | Stack.MostRecentPropertyAddress = nullptr; 75 | Stack.StepCompiledIn(nullptr); 76 | void* MessagePtr = Stack.MostRecentPropertyAddress; 77 | FStructProperty* StructProp = CastField(Stack.MostRecentProperty); 78 | P_FINISH; 79 | 80 | bool bSuccess = false; 81 | 82 | // Make sure the type we are trying to get through the blueprint node matches the type of the message payload received. 83 | if ((StructProp != nullptr) && (StructProp->Struct != nullptr) && (MessagePtr != nullptr) && (StructProp->Struct == P_THIS->MessageStructType.Get()) && (P_THIS->ReceivedMessagePayloadPtr != nullptr)) 84 | { 85 | StructProp->Struct->CopyScriptStruct(MessagePtr, P_THIS->ReceivedMessagePayloadPtr); 86 | bSuccess = true; 87 | } 88 | 89 | *(bool*)RESULT_PARAM = bSuccess; 90 | } 91 | 92 | void UAsyncAction_ListenForGameplayMessage::HandleMessageReceived(FGameplayTag Channel, const UScriptStruct* StructType, const void* Payload) 93 | { 94 | if (!MessageStructType.Get() || (MessageStructType.Get() == StructType)) 95 | { 96 | ReceivedMessagePayloadPtr = Payload; 97 | 98 | OnMessageReceived.Broadcast(this, Channel); 99 | 100 | ReceivedMessagePayloadPtr = nullptr; 101 | } 102 | 103 | if (!OnMessageReceived.IsBound()) 104 | { 105 | // If the BP object that created the async node is destroyed, OnMessageReceived will be unbound after calling the broadcast. 106 | // In this case we can safely mark this receiver as ready for destruction. 107 | // Need to support a more proactive mechanism for cleanup FORT-340994 108 | SetReadyToDestroy(); 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageProcessor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // Modified by Broken Rock Studios 3 | // See the LICENSE file for details. 4 | 5 | #include "GameFramework/GameplayMessageProcessor.h" 6 | 7 | #include "Engine/World.h" 8 | #include "GameFramework/GameStateBase.h" 9 | 10 | #include UE_INLINE_GENERATED_CPP_BY_NAME(GameplayMessageProcessor) 11 | 12 | void UGameplayMessageProcessor::BeginPlay() 13 | { 14 | Super::BeginPlay(); 15 | 16 | StartListening(); 17 | } 18 | 19 | void UGameplayMessageProcessor::EndPlay(const EEndPlayReason::Type EndPlayReason) 20 | { 21 | Super::EndPlay(EndPlayReason); 22 | 23 | StopListening(); 24 | 25 | // Remove any listener handles 26 | UGameplayMessageSubsystem& MessageSubsystem = UGameplayMessageSubsystem::Get(this); 27 | for (FGameplayMessageListenerHandle& Handle : ListenerHandles) 28 | { 29 | MessageSubsystem.UnregisterListener(Handle); 30 | } 31 | ListenerHandles.Empty(); 32 | } 33 | 34 | void UGameplayMessageProcessor::StartListening() 35 | { 36 | 37 | } 38 | 39 | void UGameplayMessageProcessor::StopListening() 40 | { 41 | } 42 | 43 | void UGameplayMessageProcessor::AddListenerHandle(FGameplayMessageListenerHandle&& Handle) 44 | { 45 | ListenerHandles.Add(MoveTemp(Handle)); 46 | } 47 | 48 | double UGameplayMessageProcessor::GetServerTime() const 49 | { 50 | if (const AGameStateBase* GameState = GetWorld()->GetGameState()) 51 | { 52 | return GameState->GetServerWorldTimeSeconds(); 53 | } 54 | else 55 | { 56 | return 0.0; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageRuntime.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | #include "Modules/ModuleManager.h" 5 | 6 | IMPLEMENT_MODULE(FDefaultModuleImpl, GameplayMessageRuntime) 7 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Private/GameFramework/GameplayMessageSubsystem.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | #include "GameFramework/GameplayMessageSubsystem.h" 5 | #include "Engine/Engine.h" 6 | #include "Engine/GameInstance.h" 7 | #include "Engine/World.h" 8 | #include "UObject/ScriptMacros.h" 9 | #include "UObject/Stack.h" 10 | 11 | #include UE_INLINE_GENERATED_CPP_BY_NAME(GameplayMessageSubsystem) 12 | 13 | DEFINE_LOG_CATEGORY(LogGameplayMessageSubsystem); 14 | 15 | namespace UE 16 | { 17 | namespace GameplayMessageSubsystem 18 | { 19 | static int32 ShouldLogMessages = 0; 20 | static FAutoConsoleVariableRef CVarShouldLogMessages(TEXT("GameplayMessageSubsystem.LogMessages"), 21 | ShouldLogMessages, 22 | TEXT("Should messages broadcast through the gameplay message subsystem be logged?")); 23 | } 24 | } 25 | 26 | ////////////////////////////////////////////////////////////////////// 27 | // FGameplayMessageListenerHandle 28 | 29 | void FGameplayMessageListenerHandle::Unregister() 30 | { 31 | if (UGameplayMessageSubsystem* StrongSubsystem = Subsystem.Get()) 32 | { 33 | StrongSubsystem->UnregisterListener(*this); 34 | Subsystem.Reset(); 35 | Channel = FGameplayTag(); 36 | ID = 0; 37 | } 38 | } 39 | 40 | ////////////////////////////////////////////////////////////////////// 41 | // UGameplayMessageSubsystem 42 | 43 | UGameplayMessageSubsystem& UGameplayMessageSubsystem::Get(const UObject* WorldContextObject) 44 | { 45 | UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::Assert); 46 | check(World); 47 | UGameplayMessageSubsystem* Router = UGameInstance::GetSubsystem(World->GetGameInstance()); 48 | check(Router); 49 | return *Router; 50 | } 51 | 52 | bool UGameplayMessageSubsystem::HasInstance(const UObject* WorldContextObject) 53 | { 54 | UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::Assert); 55 | UGameplayMessageSubsystem* Router = World != nullptr ? UGameInstance::GetSubsystem(World->GetGameInstance()) : nullptr; 56 | return Router != nullptr; 57 | } 58 | 59 | void UGameplayMessageSubsystem::Deinitialize() 60 | { 61 | ListenerMap.Reset(); 62 | 63 | Super::Deinitialize(); 64 | } 65 | 66 | void UGameplayMessageSubsystem::BroadcastMessageInternal(FGameplayTag Channel, const UScriptStruct* StructType, const void* MessageBytes) 67 | { 68 | // Log the message if enabled 69 | if (UE::GameplayMessageSubsystem::ShouldLogMessages != 0) 70 | { 71 | FString* pContextString = nullptr; 72 | #if WITH_EDITOR 73 | if (GIsEditor) 74 | { 75 | extern ENGINE_API FString GPlayInEditorContextString; 76 | pContextString = &GPlayInEditorContextString; 77 | } 78 | #endif 79 | 80 | FString HumanReadableMessage; 81 | StructType->ExportText(/*out*/ HumanReadableMessage, MessageBytes, /*Defaults=*/ nullptr, /*OwnerObject=*/ nullptr, PPF_None, /*ExportRootScope=*/ nullptr); 82 | UE_LOG(LogGameplayMessageSubsystem, Log, TEXT("BroadcastMessage(%s, %s, %s)"), pContextString ? **pContextString : *GetPathNameSafe(this), *Channel.ToString(), *HumanReadableMessage); 83 | } 84 | 85 | // Broadcast the message 86 | bool bOnInitialTag = true; 87 | for (FGameplayTag Tag = Channel; Tag.IsValid(); Tag = Tag.RequestDirectParent()) 88 | { 89 | if (const FChannelListenerList* pList = ListenerMap.Find(Tag)) 90 | { 91 | // Copy in case there are removals while handling callbacks 92 | TArray ListenerArray(pList->Listeners); 93 | 94 | for (const FGameplayMessageListenerData& Listener : ListenerArray) 95 | { 96 | if (bOnInitialTag || (Listener.MatchType == EGameplayMessageMatch::PartialMatch)) 97 | { 98 | if (Listener.bHadValidType && !Listener.ListenerStructType.IsValid()) 99 | { 100 | UE_LOG(LogGameplayMessageSubsystem, Warning, TEXT("Listener struct type has gone invalid on Channel %s. Removing listener from list"), *Channel.ToString()); 101 | UnregisterListenerInternal(Channel, Listener.HandleID); 102 | continue; 103 | } 104 | 105 | // The receiving type must be either a parent of the sending type or completely ambiguous (for internal use) 106 | if (!Listener.bHadValidType || StructType->IsChildOf(Listener.ListenerStructType.Get())) 107 | { 108 | Listener.ReceivedCallback(Channel, StructType, MessageBytes); 109 | } 110 | else 111 | { 112 | UE_LOG(LogGameplayMessageSubsystem, Error, TEXT("Struct type mismatch on channel %s (broadcast type %s, listener at %s was expecting type %s)"), 113 | *Channel.ToString(), 114 | *StructType->GetPathName(), 115 | *Tag.ToString(), 116 | *Listener.ListenerStructType->GetPathName()); 117 | } 118 | } 119 | } 120 | } 121 | bOnInitialTag = false; 122 | } 123 | } 124 | 125 | void UGameplayMessageSubsystem::K2_BroadcastMessage(FGameplayTag Channel, const int32& Message) 126 | { 127 | // This will never be called, the exec version below will be hit instead 128 | checkNoEntry(); 129 | } 130 | 131 | DEFINE_FUNCTION(UGameplayMessageSubsystem::execK2_BroadcastMessage) 132 | { 133 | P_GET_STRUCT(FGameplayTag, Channel); 134 | 135 | Stack.MostRecentPropertyAddress = nullptr; 136 | Stack.StepCompiledIn(nullptr); 137 | void* MessagePtr = Stack.MostRecentPropertyAddress; 138 | FStructProperty* StructProp = CastField(Stack.MostRecentProperty); 139 | 140 | P_FINISH; 141 | 142 | if (ensure((StructProp != nullptr) && (StructProp->Struct != nullptr) && (MessagePtr != nullptr))) 143 | { 144 | P_THIS->BroadcastMessageInternal(Channel, StructProp->Struct, MessagePtr); 145 | } 146 | } 147 | 148 | FGameplayMessageListenerHandle UGameplayMessageSubsystem::RegisterListenerInternal(FGameplayTag Channel, TFunction&& Callback, const UScriptStruct* StructType, EGameplayMessageMatch MatchType) 149 | { 150 | FChannelListenerList& List = ListenerMap.FindOrAdd(Channel); 151 | 152 | FGameplayMessageListenerData& Entry = List.Listeners.AddDefaulted_GetRef(); 153 | Entry.ReceivedCallback = MoveTemp(Callback); 154 | Entry.ListenerStructType = StructType; 155 | Entry.bHadValidType = StructType != nullptr; 156 | Entry.HandleID = ++List.HandleID; 157 | Entry.MatchType = MatchType; 158 | 159 | return FGameplayMessageListenerHandle(this, Channel, Entry.HandleID); 160 | } 161 | 162 | void UGameplayMessageSubsystem::UnregisterListener(FGameplayMessageListenerHandle Handle) 163 | { 164 | if (Handle.IsValid()) 165 | { 166 | check(Handle.Subsystem == this); 167 | 168 | UnregisterListenerInternal(Handle.Channel, Handle.ID); 169 | } 170 | else 171 | { 172 | UE_LOG(LogGameplayMessageSubsystem, Warning, TEXT("Trying to unregister an invalid Handle.")); 173 | } 174 | } 175 | 176 | void UGameplayMessageSubsystem::UnregisterListenerInternal(FGameplayTag Channel, int32 HandleID) 177 | { 178 | if (FChannelListenerList* pList = ListenerMap.Find(Channel)) 179 | { 180 | int32 MatchIndex = pList->Listeners.IndexOfByPredicate([ID = HandleID](const FGameplayMessageListenerData& Other) { return Other.HandleID == ID; }); 181 | if (MatchIndex != INDEX_NONE) 182 | { 183 | pList->Listeners.RemoveAtSwap(MatchIndex); 184 | } 185 | 186 | if (pList->Listeners.Num() == 0) 187 | { 188 | ListenerMap.Remove(Channel); 189 | } 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Public/GameFramework/AsyncAction_ListenForGameplayMessage.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // Modified by Broken Rock Studios 3 | // See the LICENSE file for details. 4 | 5 | #pragma once 6 | 7 | #include "GameplayMessageSubsystem.h" 8 | #include "GameplayMessageTypes2.h" 9 | #include "Engine/CancellableAsyncAction.h" 10 | 11 | #include "AsyncAction_ListenForGameplayMessage.generated.h" 12 | 13 | class UScriptStruct; 14 | class UWorld; 15 | struct FFrame; 16 | 17 | /** 18 | * Proxy object pin will be hidden in K2Node_GameplayMessageAsyncAction. Is used to get a reference to the object triggering the delegate for the follow up call of 'GetPayload'. 19 | * 20 | * @param ActualChannel The actual message channel that we received Payload from (will always start with Channel, but may be more specific if partial matches were enabled) 21 | */ 22 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAsyncGameplayMessageDelegate, UAsyncAction_ListenForGameplayMessage*, ProxyObject, FGameplayTag, ActualChannel); 23 | 24 | UCLASS(BlueprintType, meta=(HasDedicatedAsyncNode)) 25 | class GAMEPLAYMESSAGERUNTIME_API UAsyncAction_ListenForGameplayMessage : public UCancellableAsyncAction 26 | { 27 | GENERATED_BODY() 28 | 29 | public: 30 | /** 31 | * Asynchronously waits for a gameplay message to be broadcast on the specified channel. 32 | * 33 | * @param Channel The message channel to listen for 34 | * @param PayloadType The kind of message structure to use (this must match the same type that the sender is broadcasting) 35 | * @param MatchType The rule used for matching the channel with broadcasted messages 36 | */ 37 | UFUNCTION(BlueprintCallable, Category = Messaging, meta = (WorldContext = "WorldContextObject", BlueprintInternalUseOnly = "true")) 38 | static UAsyncAction_ListenForGameplayMessage* ListenForGameplayMessages(UObject* WorldContextObject, FGameplayTag Channel, UScriptStruct* PayloadType, EGameplayMessageMatch MatchType = EGameplayMessageMatch::ExactMatch); 39 | 40 | /** 41 | * Attempt to copy the payload received from the broadcasted gameplay message into the specified wildcard. 42 | * The wildcard's type must match the type from the received message. 43 | * 44 | * @param OutPayload The wildcard reference the payload should be copied into 45 | * @return If the copy was a success 46 | */ 47 | UFUNCTION(BlueprintCallable, CustomThunk, Category = "Messaging", meta = (CustomStructureParam = "OutPayload")) 48 | bool GetPayload(UPARAM(ref) int32& OutPayload); 49 | 50 | DECLARE_FUNCTION(execGetPayload); 51 | 52 | // ~Begin UBlueprintAsyncActionBase interface 53 | virtual void Activate() override; 54 | virtual void SetReadyToDestroy() override; 55 | // ~End of UBlueprintAsyncActionBase interface 56 | 57 | public: 58 | /** Called when a message is broadcast on the specified channel. Use GetPayload() to request the message payload. */ 59 | UPROPERTY(BlueprintAssignable) 60 | FAsyncGameplayMessageDelegate OnMessageReceived; 61 | 62 | private: 63 | 64 | /** 65 | * Handles the reception of a gameplay message. 66 | * 67 | * @param Channel The message channel that received the payload. 68 | * @param StructType The type of the message structure. 69 | * @param Payload The actual message payload. 70 | */ 71 | void HandleMessageReceived(FGameplayTag Channel, const UScriptStruct* StructType, const void* Payload); 72 | 73 | private: 74 | const void* ReceivedMessagePayloadPtr = nullptr; 75 | 76 | TWeakObjectPtr WorldPtr; 77 | FGameplayTag ChannelToRegister; 78 | TWeakObjectPtr MessageStructType = nullptr; 79 | EGameplayMessageMatch MessageMatchType = EGameplayMessageMatch::ExactMatch; 80 | 81 | FGameplayMessageListenerHandle ListenerHandle; 82 | }; 83 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageProcessor.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // Modified by Broken Rock Studios 3 | // See the LICENSE file for details. 4 | 5 | #pragma once 6 | 7 | #include "Components/ActorComponent.h" 8 | #include "GameFramework/GameplayMessageSubsystem.h" 9 | 10 | #include "GameplayMessageProcessor.generated.h" 11 | 12 | namespace EEndPlayReason { enum Type : int; } 13 | 14 | class UObject; 15 | 16 | /** 17 | * UGameplayMessageProcessor 18 | * 19 | * Base class for any message processor which observes other gameplay messages 20 | * and potentially re-emits updates (e.g., when a chain or combo is detected) 21 | * 22 | * Note that these processors are spawned on the server once (not per player) 23 | * and should do their own internal filtering if only relevant for some players. 24 | */ 25 | UCLASS(BlueprintType, Blueprintable, meta=(BlueprintSpawnableComponent)) 26 | class GAMEPLAYMESSAGERUNTIME_API UGameplayMessageProcessor : public UActorComponent 27 | { 28 | GENERATED_BODY() 29 | 30 | public: 31 | //~UActorComponent interface 32 | virtual void BeginPlay() override; 33 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 34 | //~End of UActorComponent interface 35 | 36 | /** 37 | * Start and stop listening for messages 38 | */ 39 | virtual void StartListening(); 40 | virtual void StopListening(); 41 | 42 | protected: 43 | void AddListenerHandle(FGameplayMessageListenerHandle&& Handle); 44 | double GetServerTime() const; 45 | 46 | private: 47 | TArray ListenerHandles; 48 | }; 49 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageSubsystem.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // Modified by Broken Rock Studios 3 | // See the LICENSE file for details. 4 | 5 | #pragma once 6 | 7 | #include "GameplayTagContainer.h" 8 | #include "GameFramework/GameplayMessageTypes2.h" 9 | #include "Subsystems/GameInstanceSubsystem.h" 10 | #include "UObject/WeakObjectPtr.h" 11 | 12 | #include "GameplayMessageSubsystem.generated.h" 13 | 14 | class UGameplayMessageSubsystem; 15 | struct FFrame; 16 | 17 | GAMEPLAYMESSAGERUNTIME_API DECLARE_LOG_CATEGORY_EXTERN(LogGameplayMessageSubsystem, Log, All); 18 | 19 | class UAsyncAction_ListenForGameplayMessage; 20 | 21 | /** 22 | * An opaque handle that can be used to remove a previously registered message listener 23 | * @see UGameplayMessageSubsystem::RegisterListener and UGameplayMessageSubsystem::UnregisterListener 24 | */ 25 | USTRUCT(BlueprintType) 26 | struct GAMEPLAYMESSAGERUNTIME_API FGameplayMessageListenerHandle 27 | { 28 | public: 29 | GENERATED_BODY() 30 | 31 | FGameplayMessageListenerHandle() {} 32 | 33 | void Unregister(); 34 | 35 | bool IsValid() const { return ID != 0; } 36 | 37 | private: 38 | UPROPERTY(Transient) 39 | TWeakObjectPtr Subsystem; 40 | 41 | UPROPERTY(Transient) 42 | FGameplayTag Channel; 43 | 44 | UPROPERTY(Transient) 45 | int32 ID = 0; 46 | 47 | FDelegateHandle StateClearedHandle; 48 | 49 | friend UGameplayMessageSubsystem; 50 | 51 | FGameplayMessageListenerHandle(UGameplayMessageSubsystem* InSubsystem, FGameplayTag InChannel, int32 InID) : Subsystem(InSubsystem), Channel(InChannel), ID(InID) {} 52 | }; 53 | 54 | /** 55 | * Entry information for a single registered listener 56 | */ 57 | USTRUCT() 58 | struct FGameplayMessageListenerData 59 | { 60 | GENERATED_BODY() 61 | 62 | // Callback for when a message has been received 63 | TFunction ReceivedCallback; 64 | 65 | int32 HandleID; 66 | EGameplayMessageMatch MatchType; 67 | 68 | // Adding some logging and extra variables around some potential problems with this 69 | TWeakObjectPtr ListenerStructType = nullptr; 70 | bool bHadValidType = false; 71 | }; 72 | 73 | /** 74 | * This system allows event raisers and listeners to register for messages without 75 | * having to know about each other directly, though they must agree on the format 76 | * of the message (as a USTRUCT() type). 77 | * 78 | * 79 | * You can get to the message router from the game instance: 80 | * UGameInstance::GetSubsystem(GameInstance) 81 | * or directly from anything that has a route to a world: 82 | * UGameplayMessageSubsystem::Get(WorldContextObject) 83 | * 84 | * Note that call order when there are multiple listeners for the same channel is 85 | * not guaranteed and can change over time! 86 | */ 87 | UCLASS() 88 | class GAMEPLAYMESSAGERUNTIME_API UGameplayMessageSubsystem : public UGameInstanceSubsystem 89 | { 90 | GENERATED_BODY() 91 | 92 | friend UAsyncAction_ListenForGameplayMessage; 93 | 94 | public: 95 | 96 | /** 97 | * @return the message router for the game instance associated with the world of the specified object 98 | */ 99 | static UGameplayMessageSubsystem& Get(const UObject* WorldContextObject); 100 | 101 | /** 102 | * @return true if a valid GameplayMessageRouter subsystem if active in the provided world 103 | */ 104 | static bool HasInstance(const UObject* WorldContextObject); 105 | 106 | //~USubsystem interface 107 | virtual void Deinitialize() override; 108 | //~End of USubsystem interface 109 | 110 | /** 111 | * Broadcast a message on the specified channel 112 | * 113 | * @param Channel The message channel to broadcast on 114 | * @param Message The message to send (must be the same type of UScriptStruct expected by the listeners for this channel, otherwise an error will be logged) 115 | */ 116 | template 117 | void BroadcastMessage(FGameplayTag Channel, const FMessageStructType& Message) 118 | { 119 | const UScriptStruct* StructType = TBaseStructure::Get(); 120 | BroadcastMessageInternal(Channel, StructType, &Message); 121 | } 122 | 123 | /** 124 | * Register to receive messages on a specified channel 125 | * 126 | * @param Channel The message channel to listen to 127 | * @param Callback Function to call with the message when someone broadcasts it (must be the same type of UScriptStruct provided by broadcasters for this channel, otherwise an error will be logged) 128 | * 129 | * @return a handle that can be used to unregister this listener (either by calling Unregister() on the handle or calling UnregisterListener on the router) 130 | */ 131 | template 132 | FGameplayMessageListenerHandle RegisterListener(FGameplayTag Channel, TFunction&& Callback, EGameplayMessageMatch MatchType = EGameplayMessageMatch::ExactMatch) 133 | { 134 | auto ThunkCallback = [InnerCallback = MoveTemp(Callback)](FGameplayTag ActualTag, const UScriptStruct* SenderStructType, const void* SenderPayload) 135 | { 136 | InnerCallback(ActualTag, *reinterpret_cast(SenderPayload)); 137 | }; 138 | 139 | const UScriptStruct* StructType = TBaseStructure::Get(); 140 | return RegisterListenerInternal(Channel, ThunkCallback, StructType, MatchType); 141 | } 142 | 143 | /** 144 | * Register to receive messages on a specified channel and handle it with a specified member function 145 | * Executes a weak object validity check to ensure the object registering the function still exists before triggering the callback 146 | * 147 | * @param Channel The message channel to listen to 148 | * @param Object The object instance to call the function on 149 | * @param Function Member function to call with the message when someone broadcasts it (must be the same type of UScriptStruct provided by broadcasters for this channel, otherwise an error will be logged) 150 | * 151 | * @return a handle that can be used to unregister this listener (either by calling Unregister() on the handle or calling UnregisterListener on the router) 152 | */ 153 | template 154 | FGameplayMessageListenerHandle RegisterListener(FGameplayTag Channel, TOwner* Object, void(TOwner::* Function)(FGameplayTag, const FMessageStructType&)) 155 | { 156 | TWeakObjectPtr WeakObject(Object); 157 | return RegisterListener(Channel, 158 | [WeakObject, Function](FGameplayTag Channel, const FMessageStructType& Payload) 159 | { 160 | if (TOwner* StrongObject = WeakObject.Get()) 161 | { 162 | (StrongObject->*Function)(Channel, Payload); 163 | } 164 | }); 165 | } 166 | 167 | /** 168 | * Register to receive messages on a specified channel with extra parameters to support advanced behavior 169 | * The stateful part of this logic should probably be separated out to a separate system 170 | * 171 | * @param Channel The message channel to listen to 172 | * @param Params Structure containing details for advanced behavior 173 | * 174 | * @return a handle that can be used to unregister this listener (either by calling Unregister() on the handle or calling UnregisterListener on the router) 175 | */ 176 | template 177 | FGameplayMessageListenerHandle RegisterListener(FGameplayTag Channel, FGameplayMessageListenerParams& Params) 178 | { 179 | FGameplayMessageListenerHandle Handle; 180 | 181 | // Register to receive any future messages broadcast on this channel 182 | if (Params.OnMessageReceivedCallback) 183 | { 184 | auto ThunkCallback = [InnerCallback = Params.OnMessageReceivedCallback](FGameplayTag ActualTag, const UScriptStruct* SenderStructType, const void* SenderPayload) 185 | { 186 | InnerCallback(ActualTag, *reinterpret_cast(SenderPayload)); 187 | }; 188 | 189 | const UScriptStruct* StructType = TBaseStructure::Get(); 190 | Handle = RegisterListenerInternal(Channel, ThunkCallback, StructType, Params.MatchType); 191 | } 192 | 193 | return Handle; 194 | } 195 | 196 | /** 197 | * Remove a message listener previously registered by RegisterListener 198 | * 199 | * @param Handle The handle returned by RegisterListener 200 | */ 201 | void UnregisterListener(FGameplayMessageListenerHandle Handle); 202 | 203 | protected: 204 | /** 205 | * Broadcast a message on the specified channel 206 | * 207 | * @param Channel The message channel to broadcast on 208 | * @param Message The message to send (must be the same type of UScriptStruct expected by the listeners for this channel, otherwise an error will be logged) 209 | */ 210 | UFUNCTION(BlueprintCallable, CustomThunk, Category=Messaging, meta=(CustomStructureParam="Message", AllowAbstract="false", DisplayName="Broadcast Message")) 211 | void K2_BroadcastMessage(FGameplayTag Channel, const int32& Message); 212 | 213 | DECLARE_FUNCTION(execK2_BroadcastMessage); 214 | 215 | private: 216 | // Internal helper for broadcasting a message 217 | void BroadcastMessageInternal(FGameplayTag Channel, const UScriptStruct* StructType, const void* MessageBytes); 218 | 219 | // Internal helper for registering a message listener 220 | FGameplayMessageListenerHandle RegisterListenerInternal( 221 | FGameplayTag Channel, 222 | TFunction&& Callback, 223 | const UScriptStruct* StructType, 224 | EGameplayMessageMatch MatchType); 225 | 226 | void UnregisterListenerInternal(FGameplayTag Channel, int32 HandleID); 227 | 228 | private: 229 | // List of all entries for a given channel 230 | struct FChannelListenerList 231 | { 232 | TArray Listeners; 233 | int32 HandleID = 0; 234 | }; 235 | 236 | private: 237 | TMap ListenerMap; 238 | }; 239 | -------------------------------------------------------------------------------- /Source/GameplayMessageRuntime/Public/GameFramework/GameplayMessageTypes2.h: -------------------------------------------------------------------------------- 1 | // Copyright Epic Games, Inc. All Rights Reserved. 2 | // See the LICENSE file for details. 3 | 4 | #pragma once 5 | 6 | #include "GameplayTagContainer.h" 7 | #include "Kismet/BlueprintFunctionLibrary.h" 8 | 9 | #include "GameplayMessageTypes2.generated.h" 10 | 11 | class UGameplayMessageRouter; 12 | 13 | // Match rule for message listeners 14 | UENUM(BlueprintType) 15 | enum class EGameplayMessageMatch : uint8 16 | { 17 | // An exact match will only receive messages with exactly the same channel 18 | // (e.g., registering for "A.B" will match a broadcast of A.B but not A.B.C) 19 | ExactMatch, 20 | 21 | // A partial match will receive any messages rooted in the same channel 22 | // (e.g., registering for "A.B" will match a broadcast of A.B as well as A.B.C) 23 | PartialMatch 24 | }; 25 | 26 | /** 27 | * Struct used to specify advanced behavior when registering a listener for gameplay messages 28 | */ 29 | template 30 | struct FGameplayMessageListenerParams 31 | { 32 | /** Whether Callback should be called for broadcasts of more derived channels or if it will only be called for exact matches. */ 33 | EGameplayMessageMatch MatchType = EGameplayMessageMatch::ExactMatch; 34 | 35 | /** If bound this callback will trigger when a message is broadcast on the specified channel. */ 36 | TFunction OnMessageReceivedCallback; 37 | 38 | /** Helper to bind weak member function to OnMessageReceivedCallback */ 39 | template 40 | void SetMessageReceivedCallback(TOwner* Object, void(TOwner::* Function)(FGameplayTag, const FMessageStructType&)) 41 | { 42 | TWeakObjectPtr WeakObject(Object); 43 | OnMessageReceivedCallback = [WeakObject, Function](FGameplayTag Channel, const FMessageStructType& Payload) 44 | { 45 | if (TOwner* StrongObject = WeakObject.Get()) 46 | { 47 | (StrongObject->*Function)(Channel, Payload); 48 | } 49 | }; 50 | } 51 | }; 52 | --------------------------------------------------------------------------------