How I Made My Unreal Engine C++ Code Reusable by Using Modules
I have already had very good experiences encapsulating my code with the Unity Engine by structuring the Unity3D project similarly to a web project and applying a Multi-Tier Layer Architecture to extract the game's business logic from the engine to a large extent. As a result, I had much leaner and cleaner implemented components and single points of responsibility. There were few points where communication between the components and the business logic was necessary. Where it was needed, I used my Event Manager to communicate changes in the current game state to the components responsible for presenting the current state of the game.
My goal was to be able to execute the game code as much as possible without the Unity Engine. In theory, the gameplay logic should also work entirely via console input. The Unity Engine itself should only be responsible for rendering the current game state and processing user inputs. I don't know how feasible this is for first-person games since I rely on the engine's features, such as the physics system or AI pathfinding, but it worked excellently with turn-based games, such as card games, that I have worked on.
Because I had such good experiences with the use of a Multi-Tier Layer Architecture in Unity3D projects, I would like to continue this in my Unreal Engine 5 projects.
An important restriction of a Multi-Tier Layer Architecture is that the underlying layer has no information about the layer above it. The Business Logic Module has no information about how it is used and no access to Actors, Components, the UI, and the HUD but contains the entire logic of the game. How does the Business Logic now communicate with the rest of the code? One solution would be to abandon the architecture and modules, simply pack the entire implementation into the primary module, and make the dependencies with the business logic known to each other and directly call the functions of the respective classes that want to be informed about updates to the game state. However, this would completely negate the purpose of the Multi-Tier Architecture.
One way to call functions that you don't know at implementation time would be to use an event system, often also called a messaging system. This way, the Business Logic does not need to know which classes are interested in updates to the game state. The classes interested in events implement a listener function, register the listener with the Event Manager, and receive a function call every time an event is triggered. Events can, for example, look like this: in the game logic, an enemy dies, or the player loses HP, and the Actor must be informed that an animation needs to be played, or the UI needs to update the display of the player's health or the current score. This way, the UI does not need to have any information about the implementation of the business logic but simply waits for the call of an event to tell it that the displayed information needs to be updated.
This results in very good decoupling of code and classes that are only loosely coupled do not need to know about each other's existence. For example, why should the player actor know that his health display is shown in the UI? There's no reason for that. Therefore, the Player Actor should also not have a reference to the life display in the HUD.
What Makes Modules So Useful?
Before I talk about how to create a module in a project, I thought it would be beneficial to discuss the additional advantages of using modules. In the Official Documentation, there's significant promotion for modules, even though I rarely hear or see anyone else on the internet talking or writing about them. The following benefits are advertised when using modules:
- Code Separation: Modules provide excellent code separation, offering a way to encapsulate functionalities and conceal internal components of the code.
- Faster Compilation: Each module is treated as a separate compilation unit. This means that only the affected modules need to be recompiled when changes are made, significantly accelerating the build process in large projects.
- Dependency Management: Modules are linked in a dependency graph and limit header inclusions to the code that's actually used. This aligns with the IWYU (Include What You Use) standard and ensures that unused modules can be safely excluded from compilation.
- Runtime Optimization: Modules allow specific systems to be loaded or unloaded at runtime, which can optimize the project's performance.
- Conditional Inclusion: Modules can be included or excluded under certain conditions, such as the target platform for which the project is being compiled.
With the right approach to modules, your project will not only be better organized and maintainable but also more efficiently compiled and more flexible in adapting to various requirements.
When I develop a game in Unreal, I usually have several active projects: one where I implement the entire game logic, another that I use as a sandbox for environment and marketplace assets, and the final project into which I cleanly implement the results of my experiments, proofs of concept, and prototypes. Using the same code in all projects significantly reduces the complexity of my tests. Moreover, I invest a lot of time in implementing independently functioning managers that can be reused in several subsequent projects, saving a considerable amount of duplicate work. For instance, implementing bug fixes directly for all my projects means that fixes for older games already on Steam can be effortlessly transferred into newer projects and active developments. This approach avoids solving the same problems multiple times and builds a well-maintained code base that can be used for all subsequent projects, potentially reducing the development time of sequels.
Finally, it's also an excellent method for publishing source code via the blog, as I only need to package the folder with the module. You can then simply copy it into your project directory and use it directly without any adjustments.
I believe I've now sufficiently discussed the advantages of modules. What remains is to explain how to create a module and integrate it into a project. I'd like to do this using the example of my favorite module, which forms the basis of every project I develop: My own Event Management System.
How to Create Modules in Unreal Engine 5
Creating a new module in Unreal Engine 5 is relatively straightforward, but there are some steps you should follow. As I've just explained how to implement an Event Manager in C++ in this article about the Event Manager, I'll use that as an example to show how to package and use it as a module.
Requirements
Modules are a C++ feature, so your project should be a C++ project. In this article, I explain how you can convert a Blueprint project into a C++ project.
Step 1
Navigate to your project folder in the File Explorer and create a new folder next to your project's primary module. We'll name the folder EventSystem for our example.
Step 2
Create two new subfolders in that folder: Private and Public. All .cpp
files and private headers will go into the Private folder later, while all public headers that should be accessible outside the module will go into the Public folder.
Step 3
Create a new .cpp file in the module folder and name it [ModuleName].Build.cs
.
This is where the dependencies will be defined. In my case, my module depends only on the Unreal Engine 5 Core module. These are included in the project through the following code:
using UnrealBuildTool;
public class EventSystem: ModuleRules
{
public EventSystem(ReadOnlyTargetRules Target) : base(Target)
{
PrivateDependencyModuleNames.AddRange(new string[] {"Core"});
}
}
"If you're including a dependency exclusively in .cpp
files, you should add the dependency through PrivateDependencyModuleNames
. If you're including a dependency in a .h
header file, you should add the dependency through PublicDependencyModuleNames
.
Through Forward Declarations, you can avoid including dependencies in header files.
Step 4
Now you need to incorporate the newly created EventSystem
module into your primary module:
In this case, I edit the EventManagerDemo.Build.cs
file and add the EventSystem
module to PublicDependencyModuleNames
in my EventManagerDemo
project."
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class EventManagerDemo : ModuleRules
{
public EventManagerDemo(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EventSystem" });
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
Step 5
Now that the module is integrated, the logic can be implemented. Since I've already explained the implementation in this article about the Event Manager, I'll just quickly show what you need to change here:
#pragma once
#include "CoreMinimal.h"
#include "EventSystem/Public/Identifier.h"
#include "EventSystem/Public/IEvent.h"
// Definiere eine Delegate mit einem EventPtr-Parameter.
DECLARE_DELEGATE_OneParam(EventCallback, const EventPtr&);
/**
* \class EventManager
* \brief Der zentrale Event-Manager als Singleton implementiert.
*
* Diese Klasse verwaltet das Hinzufügen und Entfernen von Event-Listeners,
* das Warteschlangen von Events und das unmittelbare Auslösen von Events.
*/
class EVENTSYSTEM_API EventManager
{
public:
/// returns singleton instance
static EventManager& getInstance() { return oneAndOnly; }
void Init(); ///< Initialisiert den Event-Manager.
void Release(); ///< Gibt alle Ressourcen frei und bereinigt.
/**
* Fügt einen Listener für einen spezifischen Event-Typ hinzu.
*
* \param callback Der Callback, der aufgerufen wird, wenn das Event ausgelöst wird.
* \param eventHash Der Hashwert des Event-Typs, auf den gehört werden soll.
* \param listenerName Ein einzigartiger Identifier für den neuen Listener.
* \throw std::exception bei Hash-Kollisionen oder wenn ein Listener mit demselben Namen bereits existiert.
*/
void addListener(const EventCallback& callback, const uint32 eventHash, const Identifier& listenerName);
/**
* Entfernt den angegebenen Listener.
*
* \param listenerName Der Identifier des zu entfernenden Listeners.
* \throw std::exception, wenn der Listener nicht existiert.
*/
void removeListener(const Identifier& listenerName);
/**
* Reit ein Event zur späteren Auslösung in die Warteschlange ein.
*
* \param pEvent Das zu warteschlangende Event.
* \param pSender Der Sender dieses Events - nullptr ist nicht erlaubt!
* \note Die tatsächliche Auslösung erfolgt durch 'triggerQueuedEvents'.
*/
void queueEvent(const EventPtr& pEvent);
/**
* Löst ein Event sofort aus - ruft alle Callbacks direkt auf.
*
* \param pEvent Das auszulösende Event.
* \param pSender Der Sender dieses Events - nullptr ist nicht erlaubt!
*/
void triggerEvent(const EventPtr& pEvent);
/**
* Löst alle in der Warteschlange stehenden Events aus.
*
* \note Sollte nach 'queueEvent' aufgerufen werden, um die Events zu verarbeiten.
*/
void triggerQueuedEvents();
private:
EventManager();
~EventManager();
/// singleton instance - initialized at programm startup; appropriate in this case!
static EventManager oneAndOnly;
TArray<EventPtr> queuedEvents; ///< Liste der in der Warteschlange stehenden Events.
/**
* Verzeichnis für Event-Callbacks.
* Event-Identifier -> (Callback-Identifier -> Callback)
*/
TMap<HashValue, TMap<Identifier, EventCallback>> eventRegistry;
FCriticalSection m_eventManagerCritialSection;
};
Actually, you don't have to change much. The only adjustment you need to make is to the EVENTSYSTEM_API
declaration, since the Event Manager now resides in EventSystem
. This would need to be updated for every header. The rest remains the same.
Final Thoughts
Creating modules in Unreal Engine 5 might seem unnecessary at first glance, but it's a powerful tool for better code organization and more efficient development. With a bit of practice, it will become an indispensable part of your development process.
Here you can download the updated project demo that uses a module to integrate the Event Manager. If you like, you can also simply copy the module into your project and use it directly: