← Back to Home
Advanced ~6 min read

Custom Gauntlet Controllers in Packaged Builds

Most projects never touch PerfGuard's Gauntlet controller. If yours needs launch-time setup before the scenario map loads — creating a savegame, signing into a backend, configuring game state — you subclass it. The catch is packaging: put the subclass in the wrong module and your Shipping build stops compiling. This tutorial shows the layout that builds in every configuration, and why.

In This Tutorial

  1. When You Need a Custom Controller
  2. Why the Obvious Layout Breaks Your Build
  3. Create a DeveloperTool Module
  4. Register the Module
  5. Point PerfGuard at Your Controller
  6. Build-Configuration Support
  7. Running Captures on a Build Machine
1

When You Need a Custom Controller

Captures run through a Gauntlet controller, UPerfGuardGauntletController, which drives the load-map to warmup to capture to finish state machine. The stock controller handles almost every project, so reach for a subclass only when you need launch-time setup that has to happen before the scenario map loads — for example creating a savegame, signing into an online backend, or putting the game into a particular state.

The base class exposes its lifecycle phases as virtual and its state as protected, so you override one phase without reimplementing the rest. For one-time launch setup, override OnControllerStartup(), which runs exactly once when the engine is ready, before the startup delay and before the map loads.

*
If you do not need launch-time setup, skip this entirely The default PerfGuardGauntletController reproduces the stock behavior. You only need the module and subclass below when you are actually overriding controller behavior.
2

Why the Obvious Layout Breaks Your Build

The natural move is to drop the subclass into your main game module. That compiles in the editor and in Development, then fails the moment you build Shipping. Here is why.

UPerfGuardGauntletController lives in PerfGuard's PerfGuardTests module, which is typed DeveloperTool. Unreal includes DeveloperTool modules in Editor, DebugGame, and Development builds, and excludes them from Test and Shipping — the Gauntlet harness is test infrastructure and should never ship inside a retail game. Your game (runtime) module is compiled for Shipping, so if it references the controller it has to depend on a module that is not there. What happens next depends on how PerfGuard is installed:

  • Installed to the engine (precompiled): there is no Shipping binary for PerfGuardTests, so the link fails with "UPerfGuardGauntletController is not available in Shipping". This is the error most people hit.
  • Compiled from source in your project: it does build, but it force-compiles the entire test harness into your shipping executable, which you do not want.
!
Preprocessor guards do not fix this Wrapping the UCLASS in #if !UE_BUILD_SHIPPING is a dead end: Unreal Header Tool still generates the class's reflection code regardless of the guard, and the base-class symbol is gone. The supported mechanism is to exclude the whole module, not to guard the code.
3

Create a DeveloperTool Module

Put the subclass in its own module and type that module DeveloperTool. Unreal then includes it exactly where the base class exists and drops it from Test and Shipping as a unit:

Source layout
Source/
  MyGame/                  Type: Runtime       (your existing game module)
  MyGameGauntlet/          Type: DeveloperTool  (new)
    MyGameGauntlet.Build.cs
    Private/MyGameGauntletModule.cpp
    Private/MyPerfController.cpp
    Public/MyPerfController.h

The module's .Build.cs depends on PerfGuardRuntime (scenario types), PerfGuardTests (the controller base class), and Gauntlet:

MyGameGauntlet.Build.cs
using UnrealBuildTool;

public class MyGameGauntlet : ModuleRules
{
    public MyGameGauntlet(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[]
        {
            "Core", "CoreUObject", "Engine",
            "PerfGuardRuntime",   // scenario types
            "PerfGuardTests",     // UPerfGuardGauntletController base class
            "Gauntlet"            // UGauntletTestController
        });
    }
}

A one-line module bootstrap:

Private/MyGameGauntletModule.cpp
#include "Modules/ModuleManager.h"

IMPLEMENT_MODULE(FDefaultModuleImpl, MyGameGauntlet);

And the controller subclass itself:

Public/MyPerfController.h
#pragma once
#include "CoreMinimal.h"
#include "PerfGuardGauntletController.h"
#include "MyPerfController.generated.h"

UCLASS()
class UMyPerfController : public UPerfGuardGauntletController
{
    GENERATED_BODY()
protected:
    // Called once when the engine is ready, before the startup delay and map load.
    virtual void OnControllerStartup() override;
};
Private/MyPerfController.cpp
#include "MyPerfController.h"

void UMyPerfController::OnControllerStartup()
{
    Super::OnControllerStartup();
    // Launch-time setup: create a savegame, sign in, configure game state.
}
4

Register the Module

Add the module to your .uproject with "Type": "DeveloperTool", so it follows the same include and exclude rules as PerfGuardTests:

MyGame.uproject
"Modules": [
    { "Name": "MyGame",         "Type": "Runtime",       "LoadingPhase": "Default" },
    { "Name": "MyGameGauntlet", "Type": "DeveloperTool", "LoadingPhase": "Default" }
]
!
Keep the controller out of your runtime module Do not add PerfGuardTests (or MyGameGauntlet) to your game module's .Build.cs, and do not reference the controller class from game code. Nothing your game compiles for Shipping should touch the controller. PerfGuard selects it by name, so there is no compile-time link from your shipping code into it.
5

Point PerfGuard at Your Controller

Select the controller by class name, without the leading U (Gauntlet maps -gauntlet=<Name> to the U-prefixed class). Any of three places works:

  • Project Settings: Plugins > PerfGuard > Gauntlet > Gauntlet Controller Class.
  • CLI: --controller MyPerfController on run / run-scenario.
  • suite.json: "controller": "MyPerfController".
CLI
perfguard run-scenario DemoScene --project ./MyGame.uproject \
    --controller MyPerfController
suite.json
{
  "scenarios": [ { "name": "DemoScene" } ],
  "controller": "MyPerfController"
}
6

Build-Configuration Support

With the subclass in a DeveloperTool module, the project compiles in every configuration the engine supports. The controller itself is present only where developer tools are enabled:

Build configuration Controller present?
Editor (UnrealEditor MyGame.uproject -game) Yes (always)
DebugGame (packaged) Yes
Development (packaged) Yes
Test (packaged) No — and Test is not buildable on a Launcher engine (see next step)
Shipping (packaged) No (excluded cleanly; none of the harness ships)
*
Verified end to end This is a compile-verified layout: Development (editor and packaged game) and Shipping both build, with the controller present in Development and the Shipping binary containing none of the Gauntlet harness.
7

Running Captures on a Build Machine

Because the controller is present in Editor, DebugGame, and Development, you have two paths that work on a stock engine for QA and CI:

  • The editor in game modeUnrealEditor MyGame.uproject -game -gauntlet=MyPerfController .... This is exactly what perfguard run-scenario and the editor's "Run Standalone" button launch. Developer tools are always on in an editor target, and a build machine can drive it headlessly.
  • A packaged Development build — it includes the controller with no target-rules changes, and is much closer to retail performance than the editor.
!
A packaged Test build needs an engine built from source On an Epic Games Launcher (installed) engine you cannot build the Test configuration at all — UnrealBuildTool refuses with "Targets cannot be built in the Test configuration with this engine distribution", for every target, not just PerfGuard. If you specifically need Test-configuration captures, build the engine from source (and set bBuildDeveloperTools = true in your *Test.Target.cs so the DeveloperTool module is included). Otherwise use a Development package or the editor path above.

For the same guidance in the plugin's shipped documentation, see Docs/ScenarioAuthoring.md — "Packaging the controller for all build configurations".