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
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.
PerfGuardGauntletController reproduces the stock behavior. You only need the module and subclass below when you are actually overriding controller behavior.
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 "UPerfGuardGauntletControlleris 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.
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.
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/ 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:
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:
#include "Modules/ModuleManager.h" IMPLEMENT_MODULE(FDefaultModuleImpl, MyGameGauntlet);
And the controller subclass itself:
#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; };
#include "MyPerfController.h" void UMyPerfController::OnControllerStartup() { Super::OnControllerStartup(); // Launch-time setup: create a savegame, sign in, configure game state. }
Register the Module
Add the module to your .uproject with "Type": "DeveloperTool", so it follows the same include and exclude rules as PerfGuardTests:
"Modules": [ { "Name": "MyGame", "Type": "Runtime", "LoadingPhase": "Default" }, { "Name": "MyGameGauntlet", "Type": "DeveloperTool", "LoadingPhase": "Default" } ]
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.
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 MyPerfControlleronrun/run-scenario. - suite.json:
"controller": "MyPerfController".
perfguard run-scenario DemoScene --project ./MyGame.uproject \ --controller MyPerfController
{
"scenarios": [ { "name": "DemoScene" } ],
"controller": "MyPerfController"
}
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) |
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 mode —
UnrealEditor MyGame.uproject -game -gauntlet=MyPerfController .... This is exactly whatperfguard run-scenarioand 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.
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".