Gameplay Programming
A developer hands over a project saying "the architecture's a bit strange, but it works." You open it and see: a 2000-line character controller where physics, animation, UI, and sound are mixed in one MonoBehaviour. In Update() — state checks through a dozen boolean flags. Saving via PlayerPrefs with keys like "player_hp_current_value_int". This isn't hypothetical—it's typical code state in projects that grew organically without architectural planning from the start.
Gameplay programming is the heart of the game. It's where the feel of control, enemy intelligence, honest physics, and reliable progression live. Do it poorly—no art saves you.
Character Controller
First contact with a player: controls. Input lag, slippery movement, getting stuck on obstacles—all instantly perceived and hurt first impressions before gameplay even matters.
Basic choice: Character Controller or Rigidbody?
CharacterController — built-in Unity component specialized for characters. Bypasses the physics engine for movement, but correctly handles steps, slopes, and obstacles. Recommended for action games, platformers, first-person shooters—where precise, predictable response matters.
Rigidbody — a physics object. Necessary when the character must interact with physics: push boxes, react to explosions, get knocked back. Requires careful FixedUpdate work and cautious gravity/friction tweaking so controls don't feel "floaty."
For most 3D projects we use CharacterController with custom gravity handling—gives control without physics engine artifacts. For 2D, Rigidbody2D with rotation constraints and careful Collision Detection Mode: Continuous.
Physics and Collisions
Rigidbody and colliders are regular problem sources if not configured correctly from the start.
A few rules that save time:
-
Collision Detection: Continuousfor fast objects (bullets, projectiles)—otherwise they "tunnel" through thin geometry - Replace complex mesh colliders with composite primitives (Box + Capsule + Sphere)—70–80% cheaper for physics
- Physics Layers and collision matrix in
Physics Settingsconfigured early—adding them later without refactoring is painful - All physics calculations in
FixedUpdate, notUpdate. Otherwise behavior depends on FPS
Deeper: Enemy AI Architecture
This is where the difference between "works" and "works well" is most felt. Bad AI is immediately obvious: enemies stuck in corners, attacking through walls, predictably patrolling the same route.
State Machines (HSM)
Most common approach: hierarchical state machine. Each state—Idle, Patrol, Chase, Attack, Dead—is a class or method with enter, update, and exit.
public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }
private void UpdateStateMachine() {
switch (_currentState) {
case EnemyState.Patrol:
UpdatePatrol();
if (CanSeePlayer()) TransitionTo(EnemyState.Chase);
break;
case EnemyState.Chase:
_navMeshAgent.SetDestination(_player.position);
if (InAttackRange()) TransitionTo(EnemyState.Attack);
if (!CanSeePlayer() && _lostSightTimer > 5f) TransitionTo(EnemyState.Patrol);
break;
// ...
}
}
State Machine works well for enemies with few states (5–8). As complexity grows—explosive growth in state transitions, code becomes hard to read and test.
Behaviour Trees
Behaviour Tree (BT) — the next level. Behavior tree describes agent logic through task hierarchy: Sequence, Selector, Decorator, Leaf.
Advantage over State Machine: each node is atomic and reusable. CheckLineOfSight node written once, used in ten trees. Adding new behavior means adding a tree branch, not refactoring existing logic.
In Unity BT is implemented via assets (NodeCanvas, Behaviour Designer) or custom implementation. For large projects with multiple enemy types, it pays for itself by the second type.
Example tree structure for patrol enemy:
Root
└── Selector
├── Sequence (Combat)
│ ├── IsPlayerVisible
│ ├── IsPlayerInRange
│ └── AttackPlayer
├── Sequence (Alert)
│ ├── HeardSound
│ └── InvestigatePosition
└── Sequence (Patrol)
├── HasPatrolRoute
└── FollowPatrolRoute
GOAP — When BT Isn't Enough
Goal-Oriented Action Planning (GOAP) — approach for truly complex AI where agent must plan action sequences to reach goals considering current world state.
Classic example: an enemy needs to "kill the player." If it has no weapon, it searches for one. If no ammo, it searches for ammo. If player took cover, it seeks alternate routes. GOAP lets you specify these actions and their preconditions/postconditions, the planner builds the chain automatically.
GOAP is significantly more complex than BT, not always justified. For platformers and casual games, overkill. For tactics games, survival sims, stealth-action—might be the right choice.
NavMeshAgent and Navigation
NavMeshAgent — standard navigation tool in Unity. Works correctly with proper NavMesh and agent setup:
-
Agent RadiusandAgent Heightmust match character collider exactly -
Stopping Distanceneeds tuning per enemy type's attack range -
NavMesh ObstaclewithCarve: truefor dynamic obstacles (falling boxes, closing doors)—otherwise agents try to walk through them - For large open worlds—NavMesh Links to connect separate segments and Off-Mesh Links for jumps and descents
Deeper: Save System
Second area where architectural decisions early critically impact everything after. Saves bolted on late "in a week" almost always break when data structure changes.
PlayerPrefs — When It Fits and When It Doesn't
PlayerPrefs is key-value storage for simple types (string, int, float). Fits strictly for settings (volume, controls, language). Using it for game world state is a mistake: no type safety, no versioning, no convenient debug.
JSON Serialization
Working approach for most projects: JSON serialization via JsonUtility (built-in, fast, but limited) or Newtonsoft.Json (full-featured, supports dicts, inheritance, nullable types).
Save system structure:
[Serializable]
public class SaveData {
public int version = 1; // versioning
public PlayerSaveData player;
public WorldSaveData world;
public SettingsSaveData settings;
}
public class SaveSystem : MonoBehaviour {
private const string SAVE_FILE = "/save.json";
public void Save(SaveData data) {
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(Application.persistentDataPath + SAVE_FILE, json);
}
public SaveData Load() {
string path = Application.persistentDataPath + SAVE_FILE;
if (!File.Exists(path)) return new SaveData();
string json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<SaveData>(json);
}
}
ScriptableObject as Data Container
ScriptableObject — underrated tool for storing game data. Item configurations, enemy stats, level parameters—all easier to keep in ScriptableObject than JSON or code constants.
For saves, ScriptableObject is used in Runtime Set and Variable pattern: values stored in ScriptableObject, saving records only the delta from defaults.
Save Versioning
A version field at SaveData root isn't bureaucracy, it's necessity. When months after release you add new mechanics with new fields, you need to migrate old saves correctly. Migration method:
private SaveData MigrateSaveData(SaveData data) {
if (data.version < 2) {
data.player.newField = defaultValue;
data.version = 2;
}
if (data.version < 3) {
// next migration
data.version = 3;
}
return data;
}
Without versioning you're stuck between broken saves for players or refusing to change data structure.
ScriptableObject Architecture
For mid to large projects we use the approach popularized by Ryan Hipple at GDC: ScriptableObject as event bus and shared state container.
// Event variable
[CreateAssetMenu]
public class GameEvent : ScriptableObject {
private List<GameEventListener> _listeners = new();
public void Raise() {
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i].OnEventRaised();
}
}
This lets game systems interact without direct references to each other. PlayerHealth doesn't know about UI, UI doesn't know about GameManager—all know only ScriptableObject events. Project becomes significantly easier to test and extend.
What This Service Includes
- Design and implement character controller (CharacterController or Rigidbody depending on genre)
- Configure physics and collision system
- Implement AI: State Machine, Behaviour Tree, GOAP—based on enemy complexity
- Configure navigation: NavMesh, NavMeshAgent, dynamic obstacles
- Design and implement save system with versioning
- Audit existing code: identify architectural issues, refactor bottlenecks
- Code review and documentation for game systems





