Code responsibilities
Look at your working code from Project 02. It works, and it passes every test on the checklist. But if you look inside SpawnOne(), notice how many different jobs that single method is performing:
- It checks whether a ped is already active.
- It validates the model and requests it from disk with a timeout.
- It calculates player offsets and coordinates.
- It calls
World.CreatePedto manipulate the engine. - It sends UI feedback to the screen.
When your scripts stay this small, having multiple responsibilities in one place is manageable. But in Stage 4, you will give this ped weapons, AI combat tasks, and faction relationships. If all of that code lives inside a single method, modifying one detail will break everything else.
The four core responsibilities of a game script
Every mod you build can be split into four distinct layers of responsibility:
+------------------------------------------------------+
| 1. Input & Cadence (KeyDown, Tick timers) |
+------------------------------------------------------+
|
+------------------------------------------------------+
| 2. Validation & State Checks (guard clauses, bounds) |
+------------------------------------------------------+
|
+------------------------------------------------------+
| 3. Engine Operations (streaming, spawning, tasks) |
+------------------------------------------------------+
|
+------------------------------------------------------+
| 4. Teardown & Lifecycle (clean deletion, reset) |
+------------------------------------------------------+
Refactoring Project 02 by role
Let's refactor the spawner so that each method has exactly one reason to change:
Role 1: Input handling
Only decides when to spawn:
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.NumPad1)
{
TrySpawnPed();
}
else if (e.KeyCode == Keys.NumPad0)
{
DeleteCurrentPed();
}
}
Role 2: State validation
Only decides if spawning is allowed:
private void TrySpawnPed()
{
if (IsPedAlreadyActive())
{
GTA.UI.Notification.PostTicker("A ped is already active.", false);
return;
}
Ped player = Game.Player.Character;
if (player == null || !player.Exists())
{
return;
}
Vector3 spawnPos = CalculateSpawnPosition(player);
_spawned = SpawnPedAt(PedHash.Trevor, spawnPos, player.Heading);
}
private bool IsPedAlreadyActive()
{
return _spawned != null && _spawned.Exists();
}
Role 3: Pure engine spawning
Only cares about streaming and creating the entity:
private Vector3 CalculateSpawnPosition(Ped reference)
{
return reference.Position + reference.ForwardVector * 2.0f;
}
private Ped SpawnPedAt(PedHash hash, Vector3 position, float heading)
{
Model model = new Model(hash);
if (!model.IsValid || !model.IsInCdImage || !model.Request(2000))
{
model.MarkAsNoLongerNeeded();
return null;
}
try
{
return World.CreatePed(model, position, heading);
}
finally
{
model.MarkAsNoLongerNeeded();
}
}
Role 4: Cleanup
Only cares about returning the world to its pristine state:
private void DeleteCurrentPed()
{
if (_spawned != null && _spawned.Exists())
{
_spawned.Delete();
}
_spawned = null;
}
Why this discipline pays off
Notice what just happened:
- If you want to change the spawn distance from 2 meters to 4 meters, you edit
CalculateSpawnPosition. Nothing else can break. - If you want to spawn Michael instead of Trevor, you pass a different argument into
SpawnPedAt. - If you want to add an automatic cleanup timer, you call
DeleteCurrentPed()fromOnTickwithout rewriting the deletion logic.
You have now completed Stage 3 Foundations. You know how the game loop runs, how 3D space works, how entities are structured, and how to write clean, modular script architecture. In Stage 4 Game Systems, we bring these entities to life with artificial intelligence, vehicle driving, dynamic events, and persistent systems.