🏗️ Lesson 8.1: Modules in Depth — Build.cs & Target.cs
Back in Lesson 2.1 you met modules as the unit of compilation. Now we go deeper: the Public/Private layout that enforces encapsulation, everything a .Build.cs controls, build targets, loading phases, and the startup hooks that let a module run code when it loads.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Organize a module into Public and Private folders and explain the effect
- Read and edit the main
.Build.cssettings beyond dependencies - Describe what a
.Target.csconfigures and the common build configurations - Create a second module and give it startup/shutdown code via
IModuleInterface - Explain module loading phases and the
_APIexport macro
Estimated Time: 60 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
Public vs Private
A module can split its code into Public/ and Private/ folders. This isn't cosmetic — it controls what other modules can include.
Source/MyModule/
├── MyModule.Build.cs
├── Public/ # headers other modules CAN include
│ └── MyPublicClass.h
└── Private/ # headers only THIS module can include
├── MyPublicClass.cpp
└── MyInternalHelper.h # implementation detail, hidden from others
📖 Public = your module's API
Headers in Public/ form your module's interface — other modules that depend on yours can include them. Headers in Private/ are implementation details, invisible outside. This is encapsulation at the module level: expose a clean surface, hide the internals. The default project layout is simpler (files directly under the module folder), but the Public/Private split matters as a project grows and especially for plugins (Lesson 8.3).
The Full Build.cs
Lesson 2.1 covered dependency lists. A .Build.cs controls much more — it's C# that runs at build time:
public class MyModule : ModuleRules
{
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// Public: dependencies exposed through THIS module's public headers.
PublicDependencyModuleNames.AddRange(new string[]
{ "Core", "CoreUObject", "Engine" });
// Private: dependencies used only in .cpp files.
PrivateDependencyModuleNames.AddRange(new string[]
{ "Slate", "SlateCore" });
// Extra include paths (e.g. for third-party headers — Lesson 8.2).
PublicIncludePaths.AddRange(new string[] { /* ... */ });
// Toggle IWYU-friendly builds, unity builds, optimization, etc.
bUseUnity = true; // combine cpp files for faster builds
// OptimizeCode = CodeOptimization.InShippingBuildsOnly;
}
}
| Setting | Controls |
|---|---|
Public/PrivateDependencyModuleNames | Which modules you link against (Lesson 2.1) |
Public/PrivateIncludePaths | Extra header search directories |
PublicAdditionalLibraries | External libs to link (Lesson 8.2) |
PCHUsage | Precompiled-header strategy (build speed) |
bUseUnity | Unity builds — combine files to compile faster |
⚠️ It's C#, and it runs at build time
Remember from Lesson 2.1: .Build.cs is C#, not C++, evaluated by UnrealBuildTool. You can branch on Target.Platform, Target.Configuration, etc., to add platform-specific dependencies. Edit it and regenerate project files for the change to take effect.
Target.cs & Configurations
A .Target.cs configures a whole build output (Lesson 2.1: game vs editor). It also chooses which modules to include and default settings for the target.
// MyProject.Target.cs — the game target
public class MyProjectTarget : TargetRules
{
public MyProjectTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.Latest;
IncludeOrderVersion = EngineIncludeOrderVersion.Latest;
// The modules this target builds:
ExtraModuleNames.AddRange(new string[] { "MyProject", "MyGameplayModule" });
}
}
Orthogonal to the target type is the build configuration — the optimization/debug level you compile:
| Configuration | Use for |
|---|---|
| Debug / DebugGame | Full debugging, no optimization — stepping through code |
| Development | Everyday work — optimized but with logging/asserts (the default) |
| Shipping | Final builds — fully optimized, logging/asserts stripped |
| Test | Shipping-like but keeps some tooling |
✅ Why configurations matter for your code
Recall Lesson 2.6: check is compiled out in Shipping, UE_LOG verbosity changes, and #if WITH_EDITOR code (Lesson 6.2) is stripped. The configuration you build decides which of your debug scaffolding survives. Develop in Development, verify in Shipping before release so you catch anything that depended on editor-only or debug-only code.
A Second Module
As a project grows, splitting code into multiple modules speeds up builds (only changed modules recompile) and enforces boundaries. To add one, create the folder + files and register it in the .uproject.
// MyGameplayModule.cpp — every module needs an implementation + IMPLEMENT_MODULE.
#include "Modules/ModuleManager.h"
// A minimal module. Use IMPLEMENT_GAME_MODULE for a gameplay module,
// or a custom IModuleInterface (below) if you need startup code.
IMPLEMENT_MODULE(FDefaultModuleImpl, MyGameplayModule);
// In MyProject.uproject — declare the new module:
"Modules": [
{ "Name": "MyProject", "Type": "Runtime", "LoadingPhase": "Default" },
{ "Name": "MyGameplayModule", "Type": "Runtime", "LoadingPhase": "Default" }
]
📖 Module types
The Type in the descriptor controls where a module is available: Runtime (game + editor), Editor (editor only — tools), Developer, UncookedOnly, etc. An editor-only module is where editor tooling lives so it never ships in the packaged game — the same separation as the game vs editor targets.
Loading Phases & Startup
Modules load at a loading phase during engine startup — Default is typical, but some modules must load earlier (before the engine is fully up) or later. And a module can run code when it loads by implementing IModuleInterface.
// MyGameplayModule.cpp — a module with startup/shutdown hooks.
#include "Modules/ModuleManager.h"
class FMyGameplayModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
// Runs when the module loads — register things, load assets, etc.
UE_LOG(LogTemp, Log, TEXT("MyGameplayModule started"));
}
virtual void ShutdownModule() override
{
// Runs when the module unloads — clean up what StartupModule did.
UE_LOG(LogTemp, Log, TEXT("MyGameplayModule shut down"));
}
};
IMPLEMENT_MODULE(FMyGameplayModule, MyGameplayModule);
⚠️ StartupModule is early — very early
StartupModule runs during engine init, before any world exists — so the same rule as an actor constructor (Lesson 3.2): no world, no actors, no gameplay here. It's for engine-level registration (custom asset types, gameplay tags, editor extensions). For per-session game logic, a subsystem (Lesson 7.4) is almost always the better home than module startup.
💡 The _API macro connects back: theMYPROJECT_APImacro you've seen on everyUCLASS(since Lesson 1.5) is the module's export macro — it marks a class as visible to other modules that depend on yours. UHT defines one per module (<MODULENAME>_API). A class without it is usable only within its own module. Now you know why it was always there.
Hands-on Exercise & Quiz
🏋️ Exercise: Plan a module split
Objective: Apply module concepts to architecture.
- Your project has grown: core gameplay, an editor tool for level designers, and a shared math/utility library. Propose a module for each and its
Type. - Which module's classes need the
_APImacro so the others can use them? - Where would code that registers a custom asset type at startup go — a subsystem or module
StartupModule? Why?
✅ Answers
- Core gameplay → Runtime; editor tool → Editor (never ships); math/utility → Runtime (shared).
- The shared utility module (and any runtime module others depend on) needs
_APIon classes exposed across modules; purely-internal classes don't. - Registering a custom asset type is engine-level and must happen at startup before content loads → module
StartupModule. Per-session game state → subsystem.
🎯 Quick Quiz
Question 1: Headers other modules may include belong in which folder?
Question 2: Which build configuration strips check and most logging for release?
Question 3: What is the MYPROJECT_API macro on a UCLASS?
Summary
🎉 Key Takeaways
- A module's
Public/headers are its API;Private/headers are hidden internals — encapsulation at module scale. .Build.cs(C#) controls dependencies, include paths, external libs, PCH, and unity builds;.Target.csconfigures the whole build (type + included modules).- Build configurations (Debug/Development/Shipping) decide what debug scaffolding survives — verify in Shipping before release.
- Extra modules speed builds and enforce boundaries; each needs
IMPLEMENT_MODULEand a.uprojectentry with aTypeandLoadingPhase. IModuleInterface::StartupModuleruns early (no world) for engine registration; the_APImacro is the module export marker you've seen all along.
📚 Additional Resources
🚀 What's Next?
Modules organize your code. Next: bringing in other people's code — integrating third-party C++ libraries through the build system, the thing Blueprint simply cannot do.
🎉 Lesson complete!
You command the build. Now let's link in external code.