Skip to main content

🏗️ 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.cs settings beyond dependencies
  • Describe what a .Target.cs configures and the common build configurations
  • Create a second module and give it startup/shutdown code via IModuleInterface
  • Explain module loading phases and the _API export 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;
    }
}
SettingControls
Public/PrivateDependencyModuleNamesWhich modules you link against (Lesson 2.1)
Public/PrivateIncludePathsExtra header search directories
PublicAdditionalLibrariesExternal libs to link (Lesson 8.2)
PCHUsagePrecompiled-header strategy (build speed)
bUseUnityUnity 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:

ConfigurationUse for
Debug / DebugGameFull debugging, no optimization — stepping through code
DevelopmentEveryday work — optimized but with logging/asserts (the default)
ShippingFinal builds — fully optimized, logging/asserts stripped
TestShipping-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: the MYPROJECT_API macro you've seen on every UCLASS (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.

  1. 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.
  2. Which module's classes need the _API macro so the others can use them?
  3. 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 _API on 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.cs configures 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_MODULE and a .uproject entry with a Type and LoadingPhase.
  • IModuleInterface::StartupModule runs early (no world) for engine registration; the _API macro 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.