Separating game logic
When you write a mod, you are doing two fundamentally different things:
- Making gameplay decisions: Should the enemy attack? Did the mission succeed? Does the player have enough reputation?
- Calling the game engine: Spawning 3D meshes, playing audio, setting weather, clearing animations.
If you tangle those two things together in the same method, you can only test your gameplay rules by launching GTA V, loading your save, driving to the location, and hoping you can reproduce the scenario. That turns a five-second logic check into a ten-minute slog.
The split: Brain vs Hands
Think of your mod as a brain and a pair of hands:
- The Brain (Pure C# Logic): Takes plain numbers, booleans, and strings. It contains zero references to
GTA.dllorScriptHookVDotNet. It decides what should happen. - The Hands (SHVDN Glue): Inherits from
Script. It queries the game world, feeds the data to the brain, gets a decision back, and tells the engine what to render.
+------------------------------------+
| Game World (GTA V Engine) |
+------------------------------------+
^
| (read positions / apply tasks)
+------------------------------------+
| Script Class (Hands / Adapter) |
+------------------------------------+
| (passes pure values: distance, isArmed)
v
+------------------------------------+
| Logic Class (Brain / Pure C#) |
+------------------------------------+
Concrete example: The Encounter Evaluator
Here is a pure logic class. Notice that it does not import using GTA;:
public sealed class EncounterEvaluator
{
public EncounterState Evaluate(float distanceToPlayer, bool isPlayerArmed, EncounterState currentState)
{
// Rule 1: Drawing a weapon within 8 meters triggers immediate combat
if (isPlayerArmed && distanceToPlayer <= 8.0f)
{
return EncounterState.Combat;
}
// Rule 2: Walking within 4 meters triggers combat even without an armed weapon
if (distanceToPlayer <= 4.0f)
{
return EncounterState.Combat;
}
// Rule 3: Being within 14 meters triggers alert
if (distanceToPlayer <= 14.0f)
{
return EncounterState.Alert;
}
// Rule 4: Backing away beyond 18 meters restores calm
if (distanceToPlayer > 18.0f)
{
return EncounterState.Calm;
}
return currentState;
}
}
How the script uses the brain
Now your script's OnTick is delightfully simple:
private readonly EncounterEvaluator _evaluator = new EncounterEvaluator();
private void OnTick(object sender, EventArgs e)
{
Ped player = Game.Player.Character;
if (player == null || !player.Exists())
{
return;
}
float distance = player.Position.DistanceTo(_guard.Position);
bool isArmed = player.IsArmed(WeaponCheckFlags.All);
// Let the pure brain decide
EncounterState nextState = _evaluator.Evaluate(distance, isArmed, _currentState);
if (nextState != _currentState)
{
ApplyState(nextState);
}
}
Because EncounterEvaluator has no GTA dependencies, its decisions can be tested in milliseconds without booting the game, while its thresholds can be tuned in real time inside GTA V.
In the next lesson, we explore how to test your gameplay logic and instrument your mod with in-game debug tools.