Skip to main content

Gameplay testing

When programmers transition from web development or enterprise software into game modding, they often bring along textbook advice: you must write automated unit tests for every feature before running your code.

Then they open Visual Studio, attempt to instantiate a Ped or call Game.Player, and immediately hit a wall: NullReferenceException. Outside of the running GTA V process, the engine does not exist. There are no DirectX rendering context, no memory pools, no native tables, and no physics simulator.

If you inspect the source code of the most popular GTA V mods, from Los Santos RED to InteractV, you will find zero xUnit test projects for gameplay mechanics. Real modders test their work differently.

Why traditional tests fail for gameplay

Traditional unit tests compare expected values against returned values:

Assert.Equal(expected, actual);

That assertion works when verifying a discount calculator or an email parser. It falls apart in a sandbox game:

  • A test cannot tell you if an NPC animation looks smooth or glitches into a fence.
  • A test cannot measure whether an eight-meter threat radius feels tense or annoying to the player.
  • A test cannot prove that a pedestrian will navigate around a light pole instead of getting stuck on geometry.

The physical feel of a game must be experienced inside the game engine.

The real modder toolkit: knobs and overlays

Instead of relying on disconnected test suites, experienced modders build their own testing tools directly inside GTA V:

  • Tuning knobs: Configuration settings loaded from .ini or JSON files that can be adjusted on the fly, allowing you to tweak distances and timers without recompiling.
  • Dev hotkeys: Dedicated keys (such as F10 or Insert) that trigger test scenarios, spawn enemy actors, or force state transitions instantly.
  • Visual debug overlays: On-screen subtitles or 3D world markers that display what the script is thinking in real time.

Here is a practical in-game testing setup that displays the current state machine evaluation on screen:

using System;
using System.Windows.Forms;
using GTA;

public sealed class GameplayTestingScript : Script
{
private readonly EncounterEvaluator _evaluator = new EncounterEvaluator();
private EncounterState _currentState = EncounterState.Calm;
private bool _showDebugOverlay = true;

public GameplayTestingScript()
{
Tick += OnTick;
KeyDown += OnKeyDown;
}

private void OnTick(object sender, EventArgs e)
{
Ped player = Game.Player.Character;
if (player == null || !player.Exists())
{
return;
}

// Measure distance to our test point and check weapon status
float distance = 10.0f;
bool isArmed = player.IsArmed(WeaponCheckFlags.All);

_currentState = _evaluator.Evaluate(distance, isArmed, _currentState);

// Visual overlay: see the brain decisions directly on screen
if (_showDebugOverlay)
{
GTA.UI.Screen.ShowSubtitle($"[DEBUG] State: {_currentState} | Dist: {distance:F1}m | Armed: {isArmed}", 1000);
}
}

private void OnKeyDown(object sender, KeyEventArgs e)
{
// Dev hotkey: toggle overlay on and off during gameplay
if (e.KeyCode == Keys.F10)
{
_showDebugOverlay = !_showDebugOverlay;
GTA.UI.Notification.PostTicker($"Debug overlay: {(_showDebugOverlay ? "ON" : "OFF")}", false);
}
}
}

With this overlay active, you walk toward the guard and immediately observe the state transition from Calm to Alert the moment your character crosses the threshold. If the distance feels wrong in game, you adjust your configuration file, press Insert to reload SHVDN, and verify the new distance five seconds later.

When automated testing makes sense

Does this mean automated testing is useless in game modding? Not at all.

When you separated your architecture in Lesson 38, you isolated the EncounterEvaluator into pure C#. That evaluator has no references to GTA.dll or native calls. It is pure decision logic.

For that specific decoupled layer, an automated test provides huge value: it verifies your boundary formulas in milliseconds without waiting through game loading screens.

using Xunit;

public sealed class EncounterEvaluatorTests
{
private readonly EncounterEvaluator _evaluator = new EncounterEvaluator();

[Fact]
public void ArmedPlayer_WithinThreatDistance_TriggersCombat()
{
// 6 meters away with drawn gun must trigger combat
EncounterState state = _evaluator.Evaluate(6.0f, isPlayerArmed: true, EncounterState.Alert);

Assert.Equal(EncounterState.Combat, state);
}

[Fact]
public void UnarmedPlayer_BeyondThreatThreshold_RestoresCalm()
{
// Backing away beyond 18 meters must restore calm
EncounterState state = _evaluator.Evaluate(20.0f, isPlayerArmed: false, EncounterState.Alert);

Assert.Equal(EncounterState.Calm, state);
}
}

The golden rule

Keep the boundary crystal clear in your workflow:

  • Unit tests prove your math is correct: They confirm that state machines transition as specified and numbers do not overflow.
  • In-game knobs and overlays prove your game is fun: They confirm that the pacing feels right, the world responds naturally, and the player remains engaged.

In Project 05, you will combine both approaches to construct a living gang encounter in Los Santos.