unreal-testing-sops
Standard Operating Procedures (SOPs) for testing game functionality in Unreal Engine.
SKILL.mdUnrealEngine/skills/unreal-testing-sops/SKILL.md
GitHubUnreal Engine Testing SOPs
This skill contains Standard Operating Procedures (SOPs) for testing game functionality in Unreal Engine. Following these SOPs ensures reliable testing with minimal token usage.
SOP Design Principles for Minimal Token Usage
To minimize AI assistant quota and token consumption:
- Avoid Viewport Captures (
capture_viewport/capture_widget): Analyzing images with multimodal prompts is extremely expensive. Do not capture images unless verifying UI visuals. - Prefer Keystroke Sequences (
simulate_input): Navigate menus using keyboard/gamepad focus keys (e.g.,Tab,Arrow Keys,Enter,SpaceBar, or Gamepad face buttons). - Use Direct Bindings or Actions: If a screen binds an action to a key (like
IA_StartRoundbound toEnteror a gamepad button), trigger that key directly rather than navigating to and clicking the button. - Use Console Commands (
execute_console_command): Bypass UI when testing systems (e.g.open LevelNameor cheat commands) to minimize setup time. - Use Your Native File-Reading Tool with Line Ranges: Avoid
catorGet-Contentfor reading source files. Use your harness's native file-reading tool (view_filein Antigravity,Readin Claude Code, or equivalent) with specific line ranges to read only the code of interest. - Log Parsing Efficiency: When checking log files for activity, use
Get-Content -Path <LogPath> -Tail <N>(PowerShell) ortail -n <N>(Unix/macOS) rather than reading entire log files. - Realistic Wait Durations & Native Scheduler: For one-shot timer delays, use your harness's native wait/scheduling mechanism if it has one (e.g. Antigravity's
scheduletool withDurationSeconds); otherwise a single bounded shell wait is acceptable. Never use tight polling loops. Use realistic timeouts (e.g., 5-15s) for map loading, PIE startup, and compilation. - Fail Fast & Escalate: Do not loop/retry blindly on failed commands or unresponsive editor sessions. Report the issue clearly with log details and ask the user for help.
SOP-001: PvP Matchmaking & Round Start
Goal
Verify the PvP matchmaking flow from the Main Menu to the Fleet Management screen, and initiate the combat round.
Prerequisites
- Unreal Editor is open.
- The project builds and matches the current commit.
Execution Workflows
Option A: Direct UI Action Triggering (Most Cost-Efficient / Recommended)
This method utilizes bound input actions to trigger UI handlers directly, bypassing the need to visually locate or click elements.
- Start PIE Session:
- Tool:
start_pie_session
- Tool:
- Navigate Main Menu:
- Simulating gamepad/keyboard navigation to focus and select the PvP button:
- Tool:
simulate_input->key:"Tab"(to move focus to the PvPButton) - Tool:
simulate_input->key:"Enter"(to click the focused button)
- Tool:
- Alternative (if focus is already default):
- Tool:
simulate_input->key:"Enter"or"Gamepad_FaceButton_Bottom"
- Tool:
- Simulating gamepad/keyboard navigation to focus and select the PvP button:
- Wait for Loading:
- Pause execution for a brief moment (e.g. 2-3 seconds) to allow the fleet management screen (
W_FleetManagementUI) to load and register.
- Pause execution for a brief moment (e.g. 2-3 seconds) to allow the fleet management screen (
- Trigger Start Round Action:
- The
UFleetManagementUIscreen binds theIA_StartRoundaction toHandleStartRound. - Instead of mouse-clicking the button, simulate the key mapped to
IA_StartRound(e.g.,Enteror the specified key mapping):- Tool:
simulate_input->key:"Enter"(or the mapped key name)
- Tool:
- The
Option B: Debug Console Commands (Highly Recommended for Agent Automation)
This method utilizes the custom C++ executive debug commands to jump directly to the target screens and trigger actions, requiring minimal token overhead and no image processing.
- Start PIE Session:
- Tool:
start_pie_session
- Tool:
- Trigger PvP Matchmaking Transition (Main Menu):
- Tool:
execute_console_command->command:"TauDebugGoToPvP"
- Tool:
- Wait for Loading:
- Pause execution for 2-3 seconds.
- Trigger Start Round (Fleet Management):
- Tool:
execute_console_command->command:"TauDebugStartRound"
- Tool:
Option C: Programmatic UI Actions (Native MCP C++ Tools)
Use this option when input action bindings or console command cheats are not available for UI transitions.
- Start PIE Session:
- Tool:
start_pie_session
- Tool:
- Enumerate & Inspect Active PIE Widgets:
- Query active UMG widget instances in the running PIE game world:
- Tool:
get_active_runtime_widgets->widget_class:"UserWidget"
- Tool:
- Or extract the full visual Slate element hierarchy:
- Tool:
extract_ui_state
- Tool:
- Query active UMG widget instances in the running PIE game world:
- Trigger UI Actions via Native MCP Tools:
- Method A: Direct Delegate Invocation (Recommended for Off-Screen/Unfocused Widgets):
- Directly trigger the button delegate on the active PIE widget instance:
- Tool:
invoke_pie_widget_delegate->widget_class_or_name:"W_TauMainMenu_C",widget_property_name:"PvPButton",delegate_name:"OnClicked"
- Tool:
- Directly trigger the button delegate on the active PIE widget instance:
- Method B: Slate Event Simulation:
- Trigger synthesized click on the widget path retrieved from
extract_ui_state:- Tool:
trigger_ui_element->widget_path:"W_TauMainMenu_C_0.PvPButton"
- Tool:
- Trigger synthesized click on the widget path retrieved from
- Method A: Direct Delegate Invocation (Recommended for Off-Screen/Unfocused Widgets):
- Wait for Loading:
- Pause execution for 2-3 seconds.
- Navigate/Click next screen programmatically:
- Trigger the start round delegate on the newly loaded
W_FleetManagementUIwidget:- Tool:
invoke_pie_widget_delegate->widget_class_or_name:"W_FleetManagementUI",widget_property_name:"StartRoundButton",delegate_name:"OnClicked"
- Tool:
- Trigger the start round delegate on the newly loaded
Verification & Success Criteria
- The game state transitions to
TauRoundGameMode(Combat Phase). - The logs show
HandleStartRoundexecution and the spawning of squads on the battlefield. - Verify using
read_message_logor viewport frame inspection that the match starts successfully.
SOP-002: Automated Performance Testing & Analysis
Goal
Verify the performance of the game under high-entity counts by running a simulated spectator round, programmatically capturing Unreal Insights trace and CSV profile data, and automatically generating an optimization diagnostic report in JSON.
Prerequisites
- Unreal Editor is open.
- The project builds and matches the current commit.
- In-game graphics quality settings are at their target profile.
Execution Workflow (Console Commands / Automation)
This is the workflow for the AI agent to test performance, analyze results, and report bottlenecks:
- (Optional) Configure Custom Benchmark Settings:
- By default, the benchmark spawns 800 entities and runs for 10 seconds. To configure custom settings, set the following console variables (CVars):
- Tool:
execute_console_command->command:"Tau.Test.BenchmarkEntities <count>" - Tool:
execute_console_command->command:"Tau.Test.BenchmarkDuration <seconds>"
- Tool:
- By default, the benchmark spawns 800 entities and runs for 10 seconds. To configure custom settings, set the following console variables (CVars):
- Ensure PIE safety:
- If a Play-In-Editor (PIE) session is currently active, ensure it is closed to avoid conflicts:
- Tool:
stop_pie_session(if running)
- Tool:
- If a Play-In-Editor (PIE) session is currently active, ensure it is closed to avoid conflicts:
- Run the Automation Test:
- Tool:
run_automation_tests->test_filter:"Tau.Performance.Benchmark"
- Tool:
- Verify Test Success:
- Ensure the test returns success. (Background throttling is automatically disabled by the AgentFramework editor plugin on startup and during the trace).
- Agent Analysis & Optimization Recommendations:
- Run the custom Python analysis script to filter the data, calculate frame time spikes, analyze parallel CPU execution/concurrency, and produce a query index:
- Tool:
run_command->CommandLine:"python .agents/scripts/parse_benchmark.py"
- Tool:
- Present the returned JSON report summary (including the identified bottleneck, frame spikes, longest-running Mass and Niagara tasks, concurrency overlap details, and the targeted Query Index) to the user.
- Run the custom Python analysis script to filter the data, calculate frame time spikes, analyze parallel CPU execution/concurrency, and produce a query index:
Verification & Success Criteria
- The benchmark runs for the specified duration and exits cleanly.
Saved/Performance/BenchmarkResult.jsonis generated and contains the following JSON structure:{ "SimulationTimeSeconds": 15.0, "StartActiveUnits": 1000, "EndActiveUnits": 980, "PrimaryBottleneck": "Game Thread", "ClientFPS": { "Average": 45.2, "Min": 28.5, "Max": 60.1 }, "GameThread": { "AverageMs": 16.5, "MinMs": 10.2, "MaxMs": 35.1 }, "RenderThread": { "AverageMs": 12.1, "MinMs": 8.5, "MaxMs": 20.2 }, "GPU": { "AverageMs": 14.2, "MinMs": 10.5, "MaxMs": 18.1 }, "Recommendations": [ "Game Thread is the primary bottleneck. Review CPU performance.", "Mass Entity System update time is high. Consider optimizing UUnitBehaviorProcessor logic or fragment sizes." ] }
SOP-003: E2E Latent Testing & Run Isolation Guardrails
1. Isolated Run Command Line Flags
When running automation tests from the command line (especially in headless or build environments), always include the -Nomessaging flag.
- Why: By default, Unreal Engine's automation system utilizes UDP Messaging to discover other testing workers. If multiple editor/game instances are running concurrently, they will peer with each other, leading to port conflicts and deadlocks in enqueued latent commands.
- Example CLI Pattern:
& "UnrealEditor-Cmd.exe" "Project.uproject" -game -nullrhi -ExecCmds="Automation RunTests TestName" -stdout -unattended -nopause -unbuffered -Nomessaging
2. Loading Screen & World Transition Race Conditions
When writing latent commands that search for widgets (e.g., waiting for screen activation after loading a map), you must verify that the map load is fully complete and all garbage collection has dropped.
- Why:
open Maptransitions are asynchronous. A simple check forGetAllWidgetsOfClasscan returntrueon the old widget instances before the level swap teardown and GC starts, resulting in immediate null pointers/timeouts once the new map begins loading. - Implementation: Always verify with the loading screen subsystem (
ULoadingScreenManager) that the loading screen has fully closed before accepting widgets:if (UGameInstance* GI = World->GetGameInstance()) { if (ULoadingScreenManager* LSM = GI->GetSubsystem<ULoadingScreenManager>()) { if (LSM->GetLoadingScreenDisplayStatus()) { return false; // Skip widget checks, the loading screen is still up } } }
SOP-004: Singleplayer Progression & Match Flow Testing
Goal
Verify the Singleplayer Challenge run flow from the Main Menu through Capital Ship/Fleet selection, handle suspended save games cleanly, and transition into the Fleet Management screen.
Prerequisites
DA_ProgressionRegistry(/Game/Data/Progression/DA_ProgressionRegistry.uasset) must register all valid Capital Ships (DA_Capital_Dreadnought,DA_Capital_Hyperion,DA_Capital_Leviathan,DA_Capital_Archangel) withbUnlocked = trueto prevent the Singleplayer Challenges button from being disabled.- INI audio maps in
Config/DefaultGame.inimust use valid((Key1,Value1),(Key2,Value2))Unreal syntax.
Execution Workflow
- Start PIE Session:
- Tool:
start_pie_session
- Tool:
- Handle Existing Suspended Runs:
- Singleplayer runs persist to
Saved/SaveGames/CurrentRunState_SP.sav. - If a saved run exists, clicking "Singleplayer Challenges" opens an overwrite confirmation modal (
W_ConfirmationDefault/Start New Run?). - Option A (Clean New Run): Delete
CurrentRunState_SP.savbefore testing to guarantee direct transition without modal intervention. - Option B (Continue Existing Run): Trigger the
SingleplayerContinueButton(slate_scommonbutton_*labeled "Continue") which callsStartShopFlow(true, false). - Option C (Confirm Overwrite): If the modal appears, click the "Yes" button (
ECommonMessagingResult::Confirmed) to invokeExecuteStartMatchFlow.
- Singleplayer runs persist to
- Trigger Singleplayer Match Flow:
- Trigger the "Singleplayer Challenges" button:
- Tool:
trigger_ui_elementon the button path retrieved fromextract_ui_state. - Or Python:
state_comp.start_match_flow(match_class)wherematch_class = BP_SingleplayerMatchInstance_C.
- Tool:
- Trigger the "Singleplayer Challenges" button:
- Verify Transition to Fleet Management:
- Wait 4-6 seconds for
W_FleetManagementUIto instantiate. - Tool:
get_active_runtime_widgets-> verifyW_FleetManagementUI_C_0is present, visible, and has active input focus.
- Wait 4-6 seconds for
SOP-005: End-to-End Combat Simulation Testing (L_Round)
Goal
Transition from the Fleet Management Phase into the automated physical combat simulation (L_Round), verify squad and gun visualization spawning, simulate combat via Mass ECS, and verify the Post-Battle Round End sequence.
Prerequisites
- All gun data assets in
/Game/Data/Guns/MUST have a validVisualizationAssetassigned from/Game/EntityRepresentations/DataAssets/Guns/DA_Rep_*. A missing visualization asset causes Mass entity representation instantiation to fail. - At least one active squad must be placed on the hex grid (
ActiveSquadIDs.Num() > 0) to enable round start.
Execution Workflow
- Verify Fleet Management Pre-Conditions:
- Ensure
StartRoundButtonis enabled (ActiveSquadIDs.Num() > 0).
- Ensure
- Trigger Round Start:
- Normal gameplay uses an input hold gesture (
IA_StartRoundheld forStartRoundHoldRequiredDuration). - Method A (Simulate Input Hold):
- Tool:
simulate_input->key:"SpaceBar",action_type:"down" - Wait 2.0-3.0 seconds (using
schedulewithDurationSeconds). - Tool:
simulate_input->key:"SpaceBar",action_type:"up"
- Tool:
- Method B (Direct Debug Function):
- Execute
DebugStartRound()directly on the activeUFleetManagementUIinstance.
- Execute
- Normal gameplay uses an input hold gesture (
- Verify Level Transition to
L_Round:- Wait 5-8 seconds for
L_Roundlevel load and Mass ECS world subsystems initialization. - Tool:
read_message_log-> Verify:NS_UnitRepresentationandNS_GunRepresentationcompile cleanly.SpawnGunsForSquadreports all mounted gunsValid=1.- 0 critical errors or access violation logs.
- Wait 5-8 seconds for
- Accelerate Combat Simulation:
- In
L_Round, combat unfolds automatically. To avoid long idle test durations, accelerate time dilation:- Tool:
simulate_inputonGameSpeedButtonFastor invokeATauRoundPlayerController::SetGameSpeedBypassingPawn(EGameSpeed::TIMES_THREE)via Python.
- Tool:
- In
- Verify Combat Progression & Resolution:
- Tool:
get_active_runtime_widgets-> Verify battle HUD elements (W_RoundHUD_C_0,W_Minimap_C,GameSpeedSelectionWidget). - Wait for units on one side to be destroyed.
- Verify that
UGameLoopSubsystem::NotifyRoundEnd()executes, audio stingers fire, andRoundEndScreenClass(UTauRoundEndWidget) is pushed to the HUD layer.
- Tool:
SOP-006: Diagnostic & Null-Safety Guardrail Protocol
Goal
Prevent unhandled Access Violation crashes during automated testing and live gameplay through strict defensive coding patterns.
Mandatory Developer Guardrails
MatchInstance Safety in UI Constructors:
UTauGameInstance::GetMatchInstance()can returnnullptrduring standalone UI testing, early initialization, or level loading.- RULE: Never chain
GetMatchInstance()->GetMapGridSize()without a null check. - Pattern:
TPair<float, float> FleetGridDimensions(10.f, 10.f); if (UTauGameInstance* TauGI = GetWorld() ? GetWorld()->GetGameInstance<UTauGameInstance>() : nullptr) { if (UMatchInstance* MatchInst = TauGI->GetMatchInstance()) { FleetGridDimensions = MatchInst->GetMapGridSize(); } }
Mouse Hover & Dynamic Upgrade Slot Validation:
- Slate synthetic mouse move events can hover over empty upgrade slots or uninitialized widgets.
- RULE: Methods receiving
UUnitSquadUpgradeDataAsset*orUUpgradeSlot*(such asOnUpgradeSlotHoveredandAreUnitAttributesMatchingUpgradeRequirements) MUST null-check the incoming upgrade pointer before dereferencing requirements or properties. - Pattern:
if (!Upgrade) { return false; }
Niagara Representation Asset Contract:
UNiagaraRepresentationSubsystem::GetOrCreateRepresentationClass()keys representations bySprite->GetFName().- RULE: Never pass an unverified or null
Spritepointer into representation creation. Maintain defensive null checking at the subsystem boundary.
Unreal INI TMap Delimiter Syntax:
- Unreal Engine's
FMapProperty::ImportText_Internaldoes NOT support INI line additions (+MapKey=(Key=...,Value=...)). - RULE: Multi-entry TMaps in
DefaultGame.inimust be formatted on a single line using parentheses pairs:MapProperty=((Key1,Value1),(Key2,Value2),(Key3,Value3))
- Unreal Engine's