Gameplay Programming Services

Our video game development company runs independent projects, jointly creates games with the client and provides additional operational services. Expertise of our team allows us to cover all gaming platforms and develop an amazing product that matches the customer’s vision and players preferences.
Showing 5 of 5 servicesAll 242 services
Medium
from 2 business days to 2 weeks
Complex
from 1 week to 1 month
Medium
from 2 business days to 1 week
Medium
from 2 business days to 2 weeks
Medium
from 2 business days to 2 weeks
FAQ
Our competencies
What are the stages of Game Development?
Latest works
  • image_games_mortal_motors_495_0.webp
    Game development for Mortal Motors
    670
  • image_games_a_turnbased_strategy_game_set_in_a_fantasy_setting_with_fire_and_sword_603_0.webp
    A turn-based strategy game set in a fantasy setting, With Fire and Sword
    860
  • image_games_second_team_604_0.webp
    Game development for the company Second term
    490
  • image_games_phoenix_ii_606_0.webp
    3D animation - teaser for the game Phoenix 2.
    533

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: Continuous for 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 Settings configured early—adding them later without refactoring is painful
  • All physics calculations in FixedUpdate, not Update. 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 Radius and Agent Height must match character collider exactly
  • Stopping Distance needs tuning per enemy type's attack range
  • NavMesh Obstacle with Carve: true for 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