Monday, 29 June 2026

How to Fix Unity Scene Management: Load Scenes, Pass Data, and Stop Losing Progress

 At some point in almost every Unity project, you hit a wall that feels embarrassingly simple on the surface: your game has more than one scene, and now nothing works the way it did when everything was in one place. The player's health resets to full every time you load the next level. The score disappears when you transition to the game-over screen. Your music cuts out and restarts from the beginning whenever a new scene loads.

I ran into every single one of these problems on my first multi-scene project. My "fix" at the time was a GameManager singleton that I kept alive across scenes using DontDestroyOnLoad, which then grew into a sprawling 600-line god object that knew about the player, the UI, the audio system, the save system, and the current level index. It worked, technically. It was also the most painful part of the entire codebase to maintain.

This guide teaches you how Unity's scene system actually works, how to load and transition between scenes properly, and — most importantly — how to pass data between scenes cleanly using ScriptableObjects (introduced in our previous guide) so you never need a 600-line GameManager holding the whole project together.



What Is Scene Management in Unity?

Scene management is the system Unity uses to load, unload, and transition between scenes — which are the individual "rooms" your project is divided into, like a main menu, a level, a boss arena, and a game-over screen. Everything you place in the Hierarchy belongs to the currently active scene, and by default when you load a new scene, everything from the old one is completely destroyed and replaced.

Unity's primary tool for this is the SceneManager class, found in the UnityEngine.SceneManagement namespace. It handles everything from instantly swapping one scene for another to loading scenes additively in the background while gameplay continues.

Why Scene Management Causes So Many Problems

The core issue is that Unity's default scene loading behavior destroys everything in the current scene before loading the next one. This is clean and efficient, but it means any data that was living on a GameObject — player health, current score, inventory contents — is gone the moment the new scene loads, unless you explicitly designed a way to preserve it.

A common mistake beginners make is assuming that public static variables or global variables will automatically carry data across scenes. Static variables do persist between scenes in Unity, but using them for complex game state creates hidden dependencies that are difficult to debug and nearly impossible to test in isolation.

The patterns in this guide give you better options.



When To Use Multiple Scenes

  • Separating the main menu from gameplay so the menu does not load any gameplay assets until needed.
  • Dividing a large game world into separate levels or zones that are loaded individually to manage memory.
  • Keeping a persistent loading scene active while heavier content loads in the background.
  • Separating boss arenas or cutscene areas from the main exploration scene to keep individual scene complexity manageable.

When To Keep Things in One Scene

  • For small games or prototypes where the complexity of managing scene transitions outweighs any benefit.
  • When you are still building and testing core mechanics — splitting into scenes too early adds unnecessary friction during development.
  • For elements like a persistent HUD or audio manager that need to survive across every scene — these are better handled with DontDestroyOnLoad or ScriptableObjects rather than being placed in every scene.

Advantages of Proper Scene Management

Advantage Why It Helps
Memory control Only the assets for the currently active scene need to be in memory, reducing RAM usage significantly on large projects.
Faster iteration Opening a specific scene directly for testing is faster than navigating a huge single scene to find what you need.
Cleaner organization Separating the main menu, each level, and the game-over screen into individual scenes keeps each one focused and manageable.
Additive loading possibilities Scenes can be loaded on top of each other (additively), enabling seamless world streaming without any visible loading screen.

Disadvantages of Scene Management

Disadvantage Why It Matters
Data persistence complexity Any data that needs to survive a scene transition requires deliberate design — it does not happen automatically.
Reference management Scripts that reference objects across scenes need careful handling since objects from one scene are destroyed when loading another.
Build settings management Every scene that needs to be loadable at runtime must be manually added to the Build Settings list, which is easy to forget.
'unity-build-settings-scenes-diagram.png' failed to upload.


Project Setup: Adding Scenes to Build Settings

Before any scene can be loaded at runtime by name or index, it must be registered in Build Settings. This is the step that trips up almost every beginner the first time, because the error message you get when a scene is missing from Build Settings is not immediately obvious.

  1. Go to File > Build Settings.
  2. Drag each scene file from your Project window into the Scenes In Build list, or click Add Open Scenes while each scene is open.
  3. The scene at index 0 is always loaded first when the game starts — this should typically be your main menu or a dedicated initialization scene.

A quick tip: name your scenes clearly and consistently. MainMenu, Level_01, Level_02, GameOver, and Loading are much easier to work with than Scene1, Scene2, UntitledScene. This matters because if you ever load a scene by name (rather than by index), a typo in the name string is an easy, hard-to-spot bug.

Part 1: Basic Scene Loading

Loading a Scene Instantly

using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneLoader : MonoBehaviour
{
    public void LoadSceneByName(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void LoadSceneByIndex(int sceneIndex)
    {
        SceneManager.LoadScene(sceneIndex);
    }

    public void ReloadCurrentScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }

    public void LoadNextScene()
    {
        int nextIndex = SceneManager.GetActiveScene().buildIndex + 1;
        if (nextIndex < SceneManager.sceneCountInBuildSettings)
        {
            SceneManager.LoadScene(nextIndex);
        }
        else
        {
            Debug.Log("No next scene — already at the last scene in Build Settings.");
        }
    }
}

Line By Line Explanation

SceneManager.LoadScene(sceneName); is the simplest scene load call. It destroys the current scene immediately and loads the named one. The string must exactly match the scene's filename (without the .unity extension) and the scene must be in Build Settings.

SceneManager.GetActiveScene().name returns the name of the currently active scene, which is useful for reloading the current level on death or retry without hardcoding its name in the script.

SceneManager.GetActiveScene().buildIndex + 1 gets the current scene's index in the Build Settings list and adds one, giving the next scene in sequence — a clean way to implement a "load next level" button without maintaining a manual level order in your scripts.

SceneManager.sceneCountInBuildSettings is the total number of scenes registered, used here to check that a "next" scene actually exists before trying to load it.

Part 2: Async Loading With a Loading Screen

Instant scene loading freezes the game for a noticeable moment on larger scenes. Async loading lets you show a loading screen and track progress while the new scene loads in the background.

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;

public class AsyncSceneLoader : MonoBehaviour
{
    public Slider progressBar;
    public string sceneToLoad;

    public void StartLoading()
    {
        StartCoroutine(LoadAsync(sceneToLoad));
    }

    private IEnumerator LoadAsync(string sceneName)
    {
        AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
        operation.allowSceneActivation = false;

        while (!operation.isDone)
        {
            float progress = Mathf.Clamp01(operation.progress / 0.9f);

            if (progressBar != null)
            {
                progressBar.value = progress;
            }

            if (operation.progress >= 0.9f)
            {
                yield return new WaitForSeconds(0.5f);
                operation.allowSceneActivation = true;
            }

            yield return null;
        }
    }
}

Key Details

LoadSceneAsync() begins loading the scene in the background without freezing the current frame. It returns an AsyncOperation object you can check for progress.

operation.allowSceneActivation = false prevents the new scene from becoming active even after it finishes loading, giving you control over exactly when the transition happens — useful for ensuring the loading bar reaches 100% visibly before switching.

operation.progress runs from 0 to 0.9 during loading, then stops at 0.9 and waits for allowSceneActivation to become true. This is why the progress calculation divides by 0.9 — it maps the 0-to-0.9 range onto a clean 0-to-1 for your progress bar.

When I first implemented a loading screen without knowing about the 0.9 behaviour, my progress bar would fill to 90% and then visibly freeze there until the next scene appeared. It looked broken even though it was technically working correctly. The allowSceneActivation trick fixed it immediately.



Part 3: Passing Data Between Scenes

This is the section most tutorials skip entirely, which is exactly why so many beginners end up with singleton GameManagers that know about everything.

Method 1: ScriptableObject Data Containers (Recommended)

Since we covered ScriptableObjects in the previous guide, this is the cleanest approach: store shared state in a ScriptableObject asset that both the sending and receiving scene reference directly.

using UnityEngine;

[CreateAssetMenu(fileName = "GameState", menuName = "Game Data/Game State")]
public class GameState : ScriptableObject
{
    public int currentScore;
    public int currentLevel;
    public float playerHealth;
    public bool hasUnlockedDoubleJump;
}

Your gameplay scene updates gameState.currentScore as the player earns points. When it loads the game-over scene, that scene reads gameState.currentScore from the same asset and displays it. No singleton, no static variable, no data loss — the ScriptableObject asset itself is the persistent data container.

The one rule to remember from the ScriptableObjects guide applies here too: changes made to this asset during play mode in the Editor persist after you exit play mode. For a runtime score counter this is usually fine during development, but for release builds you may want to reset values to defaults on game start.

Method 2: DontDestroyOnLoad (For GameObjects That Must Persist)

Sometimes you genuinely need a specific GameObject to survive every scene load — an audio manager that keeps music playing, or a system that truly needs to run continuously. DontDestroyOnLoad is the right tool for this case.

using UnityEngine;

public class PersistentAudioManager : MonoBehaviour
{
    private static PersistentAudioManager instance;

    void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }

        instance = this;
        DontDestroyOnLoad(gameObject);
    }
}

The pattern here is a guarded singleton: if an instance already exists (because this scene was loaded before and the object survived), the new duplicate is destroyed. If no instance exists, this one claims the singleton slot and is marked to survive scene loads.

Use this pattern sparingly. When I first learned DontDestroyOnLoad, I applied it to almost every manager in my project and ended up with a growing collection of persistent objects that were nearly impossible to test in isolation. The rule I follow now: DontDestroyOnLoad for systems that genuinely need continuous runtime presence (audio, input), and ScriptableObjects for data that simply needs to be available across scenes.

Method 3: PlayerPrefs (For Simple Persistent Values)

// Saving before scene transition
PlayerPrefs.SetInt("CurrentScore", currentScore);
PlayerPrefs.SetFloat("PlayerHealth", playerHealth);
PlayerPrefs.Save();

// Reading in the next scene
int savedScore = PlayerPrefs.GetInt("CurrentScore", 0);
float savedHealth = PlayerPrefs.GetFloat("PlayerHealth", 100f);

PlayerPrefs is the simplest possible option for small amounts of data. It persists between sessions (not just scenes), making it suitable for settings like audio volume, graphics quality, or a high score — but it is not designed for complex game state. Using it for anything beyond simple key-value pairs becomes messy quickly.



Part 4: Additive Scene Loading

Additive loading lets you load a second scene on top of an existing one, without destroying what is already loaded. This is how many modern games implement persistent elements like a HUD or audio manager as a separate scene that is always present, while the actual level content loads and unloads beneath it.

using UnityEngine;
using UnityEngine.SceneManagement;

public class AdditiveSceneManager : MonoBehaviour
{
    public void LoadAdditive(string sceneName)
    {
        SceneManager.LoadScene(sceneName, LoadSceneMode.Additive);
    }

    public void UnloadScene(string sceneName)
    {
        SceneManager.UnloadSceneAsync(sceneName);
    }
}

LoadSceneMode.Additive is the key parameter — without it, the default LoadSceneMode.Single destroys everything in the current scene first. With Additive, both scenes coexist in memory and the Hierarchy simultaneously.

A practical use of this for the series: load your HUD scene additively at game start, then load and unload individual level scenes beneath it as the player progresses. The HUD never gets destroyed, the player never loses their health bar reference, and you never need DontDestroyOnLoad for the UI — it simply lives in its own always-loaded scene.

Connecting to the Rest of the Series

Your Character Controller player from guide one carries health, current speed, and grounded state. None of that should live on the GameObject itself if you want it to survive a scene change — move the values that matter (starting health, move speed) into a PlayerConfig ScriptableObject from the previous guide, and let the MonoBehaviour read from it fresh each time it spawns.

Your HealthBarUI from the Canvas guide updates a UI Image based on the player's current health. If both the player scene and the HUD scene reference the same GameState ScriptableObject, the health bar reads the same value the player script writes to — zero shared GameObject references, zero scene dependency issues.

Your NavMesh enemy from guide eight targets the player's Transform. When a scene reloads, that Transform reference is destroyed and the enemy's target becomes null. The fix is to use a RuntimeSet ScriptableObject — a list of currently active player objects that enemies register themselves and their targets against at runtime, rather than holding a direct cross-scene reference.

Beginner Mistakes

  • Forgetting to add scenes to Build Settings. A scene that exists in your Project window but is not in the Build Settings list cannot be loaded at runtime — you get a warning in the console and nothing happens.
  • Storing scene-transition data in public static variables. Static variables persist between scenes, but they create hidden global state that makes code hard to test, debug, and reason about.
  • Using DontDestroyOnLoad on every manager script. This leads to an ever-growing collection of persistent objects that duplicate themselves on scene reload if the singleton guard is missing.
  • Holding direct Transform or Component references across scenes. Any reference to a GameObject in a scene that gets unloaded becomes null, causing NullReferenceException errors in every script that held that reference.
  • Not accounting for the 0.9f cap in async loading. AsyncOperation.progress stops at 0.9 until allowSceneActivation is true — progress calculations that do not account for this produce a bar that freezes visibly before completing.
  • Loading scenes by index instead of name in production builds. Build index order can accidentally change when scenes are reordered in Build Settings; loading by name is more resilient to that kind of accidental reordering.

Troubleshooting Guide

Problem: SceneManager.LoadScene() does nothing and logs a warning.

Likely cause: The scene is not added to Build Settings, or the scene name string has a typo.

Fix: Open File > Build Settings and confirm the scene is in the Scenes In Build list. Double-check the exact name string including capitalization — scene names are case-sensitive.

Problem: My player health resets every time I load a new scene.

Likely cause: Health is stored on a MonoBehaviour component that gets destroyed with the old scene.

Fix: Move current health to a GameState ScriptableObject that both scenes reference, or keep the player GameObject alive using DontDestroyOnLoad if the player truly needs to persist between scenes.

Problem: I have duplicate persistent GameObjects after reloading a scene.

Likely cause: The singleton guard in the DontDestroyOnLoad script is missing, so each scene load creates a new instance instead of destroying the duplicate.

Fix: Implement the guarded singleton pattern shown in this guide — check if an instance already exists in Awake() and destroy the new duplicate if so.

Problem: My loading bar freezes at 90% before the scene appears.

Likely cause: allowSceneActivation is set to false and never switched back to true, or the progress calculation does not account for the 0.9f cap.

Fix: Confirm your coroutine sets operation.allowSceneActivation = true after the progress check, and divide by 0.9f when mapping progress to your slider value.

Problem: My NavMesh enemy loses its player target reference after a scene reload.

Likely cause: The enemy holds a direct Transform reference to the player, which is destroyed and recreated as a new object when the scene reloads.

Fix: Use a RuntimeSet ScriptableObject pattern where the player registers itself on Start() and unregisters on OnDestroy(), and enemies look up the current player from the set rather than holding a direct reference.

Problem: My additive scene content overlaps my main scene unexpectedly.

Likely cause: The additively loaded scene contains objects positioned at the same coordinates as the main scene, or both scenes have cameras active simultaneously.

Fix: Design additive scenes carefully so their content coordinates do not overlap, and manage camera activation explicitly — only one camera should be active as the main camera at any given time unless you intend split-screen or stacked rendering.



Best Practices

  • Always add every loadable scene to Build Settings before testing transitions, not as an afterthought before building.
  • Load scenes by name rather than by index in production code, since index order can accidentally change.
  • Use ScriptableObjects from the previous guide as your primary data-passing mechanism between scenes — they are cleaner and more testable than static variables or bloated singletons.
  • Apply DontDestroyOnLoad selectively, only to systems that genuinely need continuous runtime presence, with a guarded singleton pattern to prevent duplicates.
  • Use async loading for any scene that takes more than a fraction of a second to load, to avoid a visible freeze in gameplay.
  • Consider a dedicated initialization scene (index 0) that sets up persistent systems and then immediately loads the main menu, keeping initialization logic separate from both gameplay and UI scenes.
  • Test scene transitions regularly during development, not just at the end — cross-scene reference bugs are much easier to catch while the project is still small.

Key Takeaways

  • SceneManager.LoadScene() destroys the current scene by default — any data on GameObjects is lost unless you explicitly preserve it.
  • Every scene must be registered in Build Settings before it can be loaded at runtime.
  • ScriptableObjects from the previous guide are the cleanest way to share data between scenes for most use cases.
  • DontDestroyOnLoad is appropriate for systems needing continuous runtime presence, but must be paired with a singleton guard to prevent duplicates.
  • Async loading with allowSceneActivation gives you full control over loading screen display and scene activation timing.
  • Additive loading lets multiple scenes coexist simultaneously, enabling persistent HUDs and seamless world streaming.

Action Steps

  1. Create two simple test scenes (MainMenu and Level_01), add both to Build Settings, and implement the SceneLoader script to transition between them.
  2. Add a GameState ScriptableObject and write a score value from Level_01 that is displayed correctly in a GameOver scene.
  3. Implement the async loading coroutine with a simple UI Slider progress bar between two scenes.
  4. Build the guarded singleton PersistentAudioManager and confirm it does not duplicate across multiple scene loads.
  5. Test reloading your existing level scenes from the NavMesh guide and confirm enemy references survive correctly using the ScriptableObject approach.

Next Topics To Learn

Sunday, 28 June 2026

How to Use Unity ScriptableObjects: Fix Messy Data and Stop Hardcoding Values

Here is a problem almost every Unity developer hits at a specific point in their project. You have a player with a movement speed of 6, an enemy with a patrol speed of 3.5, a weapon dealing 25 damage, and a health system starting at 100 — and every single one of those numbers is buried inside a different script, scattered across a dozen MonoBehaviours, duplicated in two or three places, and nearly impossible to tweak without hunting through files one by one.

I ran into this exact wall while building the NPC state machine for a survival game project. My enemy patrol speed was hardcoded in one script, its detection radius in another, and its damage value somewhere else entirely. Changing anything meant opening three files, hoping I had found every instance, and testing whether the change broke something I had not touched. It was slow, error-prone, and frankly embarrassing for code I was supposed to be proud of.

ScriptableObjects solve this problem. They are Unity's built-in system for storing data as standalone assets — separate from any scene or GameObject — that any script in your project can read from without being tightly coupled to each other. This guide explains exactly how they work, how to create them, and how to rebuild the most common beginner data patterns using them properly.



What Is a ScriptableObject in Unity?

A ScriptableObject is a Unity class you can inherit from (instead of MonoBehaviour) to create data containers that live as standalone asset files in your project rather than as components attached to a GameObject. Unlike a MonoBehaviour, a ScriptableObject has no Update() loop, no Transform, and no scene presence — it is just data, organized in a file, that can be referenced from anywhere.

You create a ScriptableObject the same way you create a material or an animation clip — it is an asset in your Project window that persists between play sessions, survives scene loads, and can be shared between multiple scripts and GameObjects simultaneously without any of them needing to know about each other.

Why ScriptableObjects Matter

The moment your project has more than a handful of scripts, hardcoded values become a maintenance problem. Change a value in one script, forget it is duplicated in another, and you have a bug that took two minutes to introduce and two hours to find.

ScriptableObjects solve this by moving shared data out of scripts entirely and into a single authoritative asset. Every script that needs to know "how fast does this enemy move?" reads from one EnemyData ScriptableObject. You change the speed once, in one place, and every enemy in every scene that references that asset updates automatically — no script editing, no recompilation, no hunting.

Beyond data management, ScriptableObjects are also a foundation for some genuinely elegant architecture patterns, including decoupled event systems and runtime item databases, which we will cover in the advanced examples section later in this guide.



ScriptableObject vs MonoBehaviour

Feature MonoBehaviour ScriptableObject
Lives on A GameObject in a scene An asset file in the Project window
Has Update() / Start() Yes No — it is data only, not a runtime loop
Persists between scenes Only with DontDestroyOnLoad() Yes, always — it is a project asset
Can be shared between multiple objects Not cleanly — each script has its own values Yes — any number of GameObjects can reference the same asset
Editable in the Inspector Yes, per-component Yes, as a standalone asset
Requires a GameObject to exist Yes No

When To Use ScriptableObjects

  • Any value that multiple scripts or multiple instances need to share — enemy stats, weapon damage, movement speeds.
  • Data that a designer or non-programmer needs to tweak regularly in the Inspector without opening any scripts.
  • Item databases, character classes, and other configuration sets where you want multiple variants (EnemySoldierData, EnemyBossData) sharing the same structure but different values.
  • Decoupled event systems where scripts need to communicate without knowing about each other directly (an advanced pattern covered later in this guide).
  • Runtime sets — tracking which objects currently exist in the scene without a manager singleton knowing about every object explicitly.

When Not To Use ScriptableObjects

  • For data that changes at runtime per-instance. A ScriptableObject's values are shared — if EnemyData stores currentHealth, every enemy in the scene shares the same currentHealth field, which is almost never what you want. Treat ScriptableObjects as read-only configuration, and store per-instance runtime state in the MonoBehaviour scripts on each enemy.
  • For simple, truly project-global constants that will genuinely never change — a static class or constants file can be simpler for values like physics layer indices or tag string constants.
  • For data that needs to be serialized as per-player save data. ScriptableObjects work well as configuration templates, but for actual save data you need a different serialization approach (JSON, binary, or Unity's PlayerPrefs system).

This part usually confuses new users, and honestly it confused me too at first: a ScriptableObject is a configuration template, not a per-instance data store. The distinction matters because Unity shares the same ScriptableObject asset across everything that references it, in both the Editor and in builds. Mutating its values at runtime works in the Editor but can produce unexpected shared-state bugs in builds if you have not planned for it.



Project Setup

Recommended Folder Structure

  • Assets/Scripts/Data — your ScriptableObject class definitions.
  • Assets/Data — the actual ScriptableObject asset instances (EnemySoldierData.asset, PlayerConfig.asset, and so on).
  • Assets/Scripts — your MonoBehaviour scripts that reference those assets.

Separating the ScriptableObject definitions (the C# class files) from the ScriptableObject instances (the .asset files) makes your Project window much easier to navigate as the project grows, and makes it immediately clear at a glance which files are code and which are data.

Creating Your First ScriptableObject: Enemy Configuration

Step 1: Write the ScriptableObject Class

Create a new C# script in Assets/Scripts/Data and name it EnemyData. Replace all contents with the following.

using UnityEngine;

[CreateAssetMenu(fileName = "NewEnemyData", menuName = "Game Data/Enemy Data")]
public class EnemyData : ScriptableObject
{
    [Header("Movement")]
    public float moveSpeed = 3.5f;
    public float patrolRadius = 10f;

    [Header("Detection")]
    public float detectionRadius = 8f;
    public float chaseSpeed = 6f;

    [Header("Combat")]
    public float attackDamage = 15f;
    public float attackRange = 1.5f;
    public float attackCooldown = 1.2f;

    [Header("Health")]
    public float maxHealth = 100f;
}

Step 2: Create an Asset Instance

  1. Right-click in your Assets/Data folder inside the Project window.
  2. Choose Create > Game Data > Enemy Data — the exact menu path you defined in the CreateAssetMenu attribute above.
  3. Name the resulting file EnemySoldierData.
  4. Click it to select it, and you will see all the fields from your class appear in the Inspector, ready to edit.

You can create as many instances as you need. An EnemySoldierData with low stats and a large patrol radius, an EnemyBossData with high stats and short patrol range — both from the exact same class definition, but configured independently in the Inspector.

Step 3: Reference the Asset in a MonoBehaviour

using UnityEngine;
using UnityEngine.AI;

public class EnemyController : MonoBehaviour
{
    public EnemyData data;

    private NavMeshAgent agent;
    private float currentHealth;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.speed = data.moveSpeed;
        currentHealth = data.maxHealth;
    }

    public void TakeDamage(float amount)
    {
        currentHealth -= amount;

        if (currentHealth <= 0f)
        {
            Die();
        }
    }

    void Die()
    {
        Debug.Log(gameObject.name + " has died.");
        Destroy(gameObject);
    }
}

In the Inspector, drag your EnemySoldierData asset into the Data field on the EnemyController component. Now your enemy reads all its configuration from the ScriptableObject, and changing the patrol speed or detection radius only requires editing the asset — no script editing, no recompile.

Line By Line Explanation

[CreateAssetMenu(...)] is an attribute that adds your ScriptableObject to the right-click Create menu in the Project window. Without it, creating instances requires going through Assets > Create, which is harder to find. The menuName field controls exactly where in that menu the item appears.

public class EnemyData : ScriptableObject is the key difference from a regular MonoBehaviour script — inheriting from ScriptableObject instead of MonoBehaviour is the only thing that makes this a data container rather than a component.

[Header("Movement")] organizes the Inspector display into labeled sections. With several fields, this makes a big difference to readability in the Inspector, especially if a designer (or future you) is editing these values regularly.

public EnemyData data; in the MonoBehaviour is just a regular public reference field. Unity's Inspector lets you drag any asset of the right type into this slot, including your ScriptableObject assets.

agent.speed = data.moveSpeed; reads a value from the ScriptableObject and applies it to the NavMeshAgent at startup. This is the core workflow: ScriptableObject stores configuration, MonoBehaviour reads it and uses it for actual runtime behavior.


Real Example: Building a Weapon Item Database

Let's look at a more complete example — an item system where each weapon type is a ScriptableObject asset, and your inventory or loot system just keeps a list of these assets rather than hardcoding weapon stats anywhere.

The WeaponData ScriptableObject

using UnityEngine;

[CreateAssetMenu(fileName = "NewWeapon", menuName = "Game Data/Weapon")]
public class WeaponData : ScriptableObject
{
    public string weaponName;
    public Sprite icon;
    public float damage;
    public float fireRate;
    public float range;
    public int ammoCapacity;

    [TextArea(2, 4)]
    public string description;
}

A Simple Inventory That Uses It

using UnityEngine;
using System.Collections.Generic;

public class PlayerInventory : MonoBehaviour
{
    public List<WeaponData> availableWeapons;
    private WeaponData equippedWeapon;

    void Start()
    {
        if (availableWeapons.Count > 0)
        {
            EquipWeapon(availableWeapons[0]);
        }
    }

    public void EquipWeapon(WeaponData weapon)
    {
        equippedWeapon = weapon;
        Debug.Log("Equipped: " + equippedWeapon.weaponName +
                  " | Damage: " + equippedWeapon.damage);
    }
}

In the Inspector, you drag your weapon ScriptableObject assets directly into the Available Weapons list. Sword, assault rifle, sniper — each one is a separate asset file with its own configured values. Your PlayerInventory script itself has zero hardcoded weapon data; it only knows how to hold and equip weapons, not what any specific weapon's stats are. This separation is the core benefit of data-driven design.

When I refactored my survival game to use this pattern instead of hardcoded weapon values, the first thing I noticed was how much easier playtesting became. Adjusting weapon balance no longer meant opening a script, changing a constant, waiting for recompilation, entering play mode, and testing again. It meant opening the WeaponData asset in the Inspector, changing the damage field directly, entering play mode, and testing. No compile step at all for balance changes.

Advanced Pattern: ScriptableObject Game Events

This is one of the most powerful and underused patterns in Unity development. Instead of scripts calling functions on each other directly (tight coupling), you create a ScriptableObject event that acts as a communication channel — scripts subscribe to it or raise it without needing to know who else is listening.

The GameEvent ScriptableObject

using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(fileName = "NewGameEvent", menuName = "Game Data/Game Event")]
public class GameEvent : ScriptableObject
{
    private List<GameEventListener> listeners = new List<GameEventListener>();

    public void Raise()
    {
        for (int i = listeners.Count - 1; i >= 0; i--)
        {
            listeners[i].OnEventRaised();
        }
    }

    public void RegisterListener(GameEventListener listener)
    {
        if (!listeners.Contains(listener))
        {
            listeners.Add(listener);
        }
    }

    public void UnregisterListener(GameEventListener listener)
    {
        if (listeners.Contains(listener))
        {
            listeners.Remove(listener);
        }
    }
}

The GameEventListener MonoBehaviour

using UnityEngine;
using UnityEngine.Events;

public class GameEventListener : MonoBehaviour
{
    public GameEvent gameEvent;
    public UnityEvent response;

    void OnEnable()
    {
        gameEvent.RegisterListener(this);
    }

    void OnDisable()
    {
        gameEvent.UnregisterListener(this);
    }

    public void OnEventRaised()
    {
        response.Invoke();
    }
}

Using It in Practice

Create a GameEvent asset named OnPlayerDied. Your health script raises it when health reaches zero. Your UI script listens for it to show the death screen. Your respawn system listens for it to start a countdown. Your achievement system listens for it to check a death-related challenge.

None of those systems need to know about each other. None of them import or reference each other's scripts. They all just reference the same OnPlayerDied ScriptableObject asset. This is the kind of clean, decoupled architecture that makes larger projects manageable rather than a tangled mess of inter-script dependencies.



Connecting ScriptableObjects to the Rest of the Series

If you have followed this series from the Character Controller and Rigidbody guides through to the NavMesh and Animator articles, you already have code that would benefit directly from the patterns in this guide.

Your EnemyController (from the NavMesh guide) hardcodes patrol speed and detection radius as public floats directly on the component. Replacing those with a reference to an EnemyData ScriptableObject gives you the ability to swap one enemy type for another just by assigning a different asset in the Inspector — without touching the script at all.

Your HealthBarUI (from the Canvas guide) has maxHealth hardcoded as a float. Replacing it with a reference to a PlayerConfig ScriptableObject means your UI and your player movement script can both read from the same authoritative source for player stats, rather than each maintaining their own separate copy of the same value.

Your AnimatorBridge (from the Animator guide) has moveSpeed as a serialized field. Moving it into a PlayerConfig ScriptableObject means a designer can adjust player speed without hunting for which of several scripts contains the relevant field.

Troubleshooting Guide

Problem: My ScriptableObject does not appear in the Create menu.

Likely cause: The CreateAssetMenu attribute is missing from the class definition, or the class name does not exactly match the file name.

Fix: Confirm the attribute is on the line immediately above the class declaration, and that the script file is named exactly the same as the class (EnemyData.cs for public class EnemyData).

Problem: Changes I made during play mode persisted after I exited play mode.

Likely cause: You modified a ScriptableObject's fields at runtime. Unlike MonoBehaviour component values, ScriptableObject asset modifications during play mode affect the actual asset file.

Fix: Either avoid writing to ScriptableObject fields at runtime for values that should reset between sessions, or make a copy of the relevant data into a local variable at Start() and modify that copy instead.

Problem: All my enemies share the same health value and die at the same time.

Likely cause: Current health is stored directly in the ScriptableObject rather than in each enemy's own MonoBehaviour.

Fix: Move currentHealth to the MonoBehaviour script on each enemy, initialized from data.maxHealth at Start(). The ScriptableObject stores the starting configuration; the MonoBehaviour stores the live runtime state.

Problem: My ScriptableObject reference shows as "None (EnemyData)" in the Inspector even after assigning it.

Likely cause: The field type in the script does not match the ScriptableObject type you created, or the asset was created from the wrong class.

Fix: Confirm the public field type in the MonoBehaviour exactly matches the ScriptableObject class name, and that the asset file in the Project window was created from that specific class.

Problem: I cannot find my ScriptableObject asset after creating it.

Likely cause: It was created in a random folder because you right-clicked in the wrong location in the Project window.

Fix: Use the Project window's search bar and search for the asset name, then move it to your designated Assets/Data folder once found. This is exactly why setting up a dedicated data folder before creating assets matters.




Frequently Asked Questions

What is a ScriptableObject in Unity?

A ScriptableObject is a Unity class you can inherit from to create data containers that live as standalone asset files in your project, rather than as components on a GameObject. Multiple scripts can reference the same ScriptableObject without being coupled to each other.

What is the difference between ScriptableObject and MonoBehaviour?

MonoBehaviour lives on a GameObject in a scene, has a runtime Update loop, and is tied to the scene's lifecycle. ScriptableObject lives as a project asset, has no Update loop, and persists independently of any scene.

How do I create a ScriptableObject in Unity?

Write a class that inherits from ScriptableObject instead of MonoBehaviour, add the CreateAssetMenu attribute, then right-click in the Project window and choose Create followed by the path you defined in that attribute.

Can ScriptableObjects be shared between multiple GameObjects?

Yes — this is one of the core benefits. Any number of GameObjects can hold a reference to the same ScriptableObject asset, all reading from one authoritative source rather than maintaining separate copies of the same values.

Why should I use ScriptableObjects instead of hardcoded values?

Hardcoded values buried in scripts require opening and editing script files every time a value needs to change. ScriptableObject assets can be edited directly in the Inspector, without opening any script, and a single change propagates to everything that references that asset.

Can I store runtime data like currentHealth in a ScriptableObject?

Technically yes, but this is almost always a mistake — all objects sharing that ScriptableObject will share the same currentHealth value. Store configuration in ScriptableObjects and runtime state in MonoBehaviour components.

Do ScriptableObject changes persist between play sessions?

Yes, including changes made during play mode in the Editor, which is different from MonoBehaviour component values that reset when you exit play mode. Be careful about mutating ScriptableObject values at runtime if you expect them to reset.

What is the CreateAssetMenu attribute?

It is an attribute that adds your ScriptableObject to the right-click Create menu in the Project window, making it easy to create instances without navigating through menus manually. Without it, creating asset instances is significantly more inconvenient.

Can I use ScriptableObjects as a save system?

They are not designed for this. ScriptableObjects are suitable for configuration data, but for persistent save data that should survive between game sessions use a proper serialization approach like JSON or a dedicated save system.

How many ScriptableObject instances can I create from one class?

As many as you need — each instance is an independent asset file with its own set of values. This is how item databases work: one WeaponData class definition, dozens of individual weapon asset instances.

Can ScriptableObjects communicate between scripts?

Yes, using the Game Events pattern described in this guide, where a ScriptableObject acts as a shared communication channel that scripts subscribe to or trigger without needing to reference each other directly.

Why are my enemies all dying at the same time?

Current health is almost certainly stored in a shared ScriptableObject rather than in each enemy's own MonoBehaviour. Move runtime state like currentHealth to the per-enemy script, initialized from the ScriptableObject's maxHealth at Start().

Can I use ScriptableObjects with the NavMesh system from earlier in this series?

Yes — storing NavMeshAgent speed, detection radius, and attack values in an EnemyData ScriptableObject, then applying them in the enemy's Start() method, is exactly the kind of use case ScriptableObjects are designed for.

Do ScriptableObjects work in Unity builds, not just the Editor?

Yes. ScriptableObject assets referenced by objects in your scenes are included in builds automatically, and read-only access to their values works identically in builds and in the Editor.

Key Takeaways

  • ScriptableObjects are data containers that live as project assets, shared between scripts without tight coupling.
  • They solve the hardcoded-values problem by moving shared configuration into one authoritative, Inspector-editable file.
  • Use them for configuration (stats, speeds, descriptions) not for per-instance runtime state (current health, current ammo).
  • The CreateAssetMenu attribute is essential for making ScriptableObjects easy to create and use in practice.
  • The Game Events pattern is an advanced but very practical use case for decoupled communication between scripts.
  • Changes to ScriptableObject assets during play mode persist after exiting play mode — always be aware of this.

Action Steps

  1. Create an EnemyData ScriptableObject class and one instance, then connect it to your enemy script from the NavMesh guide.
  2. Create a WeaponData ScriptableObject and build two or three weapon instances with different stats, then wire a simple inventory script to hold and equip them.
  3. Identify three hardcoded values in your current project that belong in a ScriptableObject and refactor them one at a time.
  4. Build the GameEvent system from this guide and replace one direct script-to-script function call in your project with a raised event.
  5. Save your ScriptableObject class definitions and asset instances to source control, and confirm a teammate (or a future you in another project) can create new instances from the Create menu.

Learning Roadmap

  1. Get comfortable creating and referencing basic ScriptableObject data containers using the EnemyData example.
  2. Build a small item database using WeaponData instances and a simple inventory that holds them.
  3. Refactor existing hardcoded values from your Character Controller and NavMesh scripts into ScriptableObject assets.
  4. Implement the GameEvent pattern for at least one inter-system communication point, like player death or scene load.
  5. Explore scene management and how ScriptableObjects provide a clean way to pass data between scenes without singletons.

Next Topics To Learn

Friday, 26 June 2026

Unity NavMesh AI Movement Explained for Beginners

 You have a player that walks, jumps, pushes crates, and animates smoothly using everything covered earlier in this series. Now your level needs something that moves on its own — an enemy that chases the player, a guard that patrols a route, or an NPC that walks to a specific point and waits. Writing that pathfinding logic by hand, avoiding every wall and obstacle, would take an enormous amount of work. That is exactly what Unity's NavMesh system solves for you automatically.

This guide starts from zero: what a NavMesh actually is, how to bake one for your level, how the NavMeshAgent component moves a character along it, and how to build a working chase-and-patrol enemy with animation blending tied into the Animator system from our earlier guide.



What Is a NavMesh in Unity?

A NavMesh (short for Navigation Mesh) is a simplified representation of the walkable surfaces in your level — floors, ramps, platforms — that Unity generates automatically through a process called baking. Once baked, this mesh tells Unity exactly where characters are allowed to walk, letting it calculate paths around walls, obstacles, and gaps without you writing any pathfinding code yourself.

The NavMesh itself is invisible during normal gameplay; it exists purely as data Unity uses internally for pathfinding. You only see it as a highlighted blue overlay inside the Unity Editor's Navigation window, never in the actual built game.

What Is a NavMeshAgent?

A NavMeshAgent is the component you attach to any character that needs to move along the NavMesh. Once you give it a destination, the NavMeshAgent automatically calculates the shortest valid path, moves the character along it, and steers around both static level geometry and other dynamic obstacles — all without you writing the underlying pathfinding math.

This is conceptually similar to giving someone a GPS destination instead of telling them exactly which streets to turn down — you say "go here," and the system figures out the route.



Why NavMesh Matters

A common mistake beginners make when first building enemy AI is writing simple "always move toward the player" code using basic Vector3 math. This works in a completely open, empty room, but the moment a wall or pillar exists between the enemy and the player, that approach breaks down — the enemy walks straight into the wall and gets stuck, with no idea how to go around it.

NavMesh solves exactly this problem. It understands the actual layout of your level, including walls, ramps, and gaps, and calculates a path that intelligently avoids obstacles, which would otherwise require a substantial amount of custom pathfinding code (like implementing A* search yourself) to replicate.

When To Use NavMesh

  • Enemies that need to chase or hunt the player through complex level geometry.
  • NPCs that patrol a fixed route between several points.
  • Characters that need to walk to a specific location and stop, like an NPC walking to a chair and sitting down.
  • Crowd or group movement, where multiple agents need to navigate the same space without all colliding into each other at once.

When Not To Use NavMesh

  • For your player character — NavMesh is designed for AI-controlled movement, not direct player input. Continue using Character Controller or Rigidbody, as covered in our earlier guides, for the player.
  • For simple, fixed-path movement that never needs to react to obstacles, like an elevator moving between two exact points — a basic scripted Transform or Rigidbody movement is simpler and more precise for that case.
  • For very small, simple levels with no obstacles at all between the AI and its target, where direct movement may be sufficient and baking a NavMesh would be unnecessary overhead.
  • For physics-driven characters that need to be affected by forces and momentum in complex ways — NavMeshAgent handles its own movement directly and does not interact with the physics engine the same way Rigidbody does.

This part usually confuses new users: NavMeshAgent and Character Controller solve similar-sounding problems (moving a character around a level) but for very different audiences — NavMeshAgent is built for AI making its own movement decisions, while Character Controller is built for direct, frame-by-frame player input.

Advantages of NavMesh

Advantage Why It Helps
Automatic obstacle avoidance Agents intelligently path around walls and obstacles without any custom pathfinding code.
Built into Unity No additional packages or plugins are required for basic NavMesh functionality.
Handles complex level geometry Works correctly across ramps, multiple floors, and irregularly shaped rooms once properly baked.
Dynamic re-routing If a path becomes blocked by a NavMesh Obstacle, agents automatically recalculate a new route.

Disadvantages of NavMesh

Disadvantage Why It Matters
Requires baking Any significant change to level geometry requires re-baking the NavMesh, which is easy to forget during development.
Less precise than scripted movement Fine-grained control over exact movement feel is more limited compared to a custom Character Controller or Rigidbody script.
Performance cost with many agents A large number of simultaneously pathfinding agents adds CPU cost, which matters more on mobile, per our Mobile Optimization guide.
Static by default The baked mesh does not automatically update for moving doors or destructible walls without additional NavMesh Obstacle setup.



Project Setup

Installing the AI Navigation Package

In recent Unity versions, NavMesh baking tools live in a separate package rather than being built into the core engine by default.

  1. Open Window > Package Manager.
  2. Switch the dropdown to Unity Registry.
  3. Search for AI Navigation and click Install.

Recommended Folder Structure

  • Assets/Scripts — the EnemyChase and EnemyPatrol scripts from this guide.
  • Assets/Prefabs — save your finished enemy character here once working.

Baking Your First NavMesh

Step 1: Mark Your Geometry as Navigation Static

  1. Select your floor, walls, and any other level geometry that should affect walkability.
  2. In the top-right of the Inspector, click the Static dropdown next to the Static checkbox, and ensure Navigation Static is checked.

Step 2: Add a NavMesh Surface

  1. In the Hierarchy, right-click and choose AI > NavMesh Surface (or add the NavMeshSurface component to an existing empty GameObject).
  2. Select the NavMesh Surface object, and in the Inspector, click Bake.

You will see a blue overlay appear on your floor in the Scene view, representing every area Unity has calculated as walkable. If a section of your floor does not turn blue, double-check that it was marked Navigation Static before baking, or that nothing is blocking it from above (like an invisible collider sitting just above the floor).

Understanding Key Bake Settings

Setting What It Does Typical Beginner Setting
Agent Radius How wide the agent is considered to be, used to keep paths away from walls by this distance. 0.5 (default, fine for most humanoid characters)
Agent Height How tall the agent is considered to be, affecting which low ceilings or gaps it can pass under. 2 (default, fine for most humanoid characters)
Max Slope The steepest ramp angle the agent can walk up, similar in concept to Character Controller's Slope Limit. 45 degrees
Step Height The maximum height of a step or ledge the agent can climb without it being treated as an obstacle. 0.4 (similar to Character Controller's Step Offset)

A quick rule of thumb: if your enemy character and player character are roughly the same size, you can use very similar values here to the Character Controller settings from our earlier guide, since both are describing the same physical space the character needs to move through.



Adding a NavMeshAgent

  1. Create or select your enemy character GameObject.
  2. Click Add Component and search for Nav Mesh Agent.
  3. Leave the default settings for now — we will tune Speed and Stopping Distance shortly.

Understanding Each Property

Property What It Does Typical Beginner Setting
Speed How fast the agent moves, in units per second. 3.5 (a typical walking pace)
Angular Speed How quickly the agent rotates to face its movement direction, in degrees per second. 120
Acceleration How quickly the agent speeds up or slows down. 8
Stopping Distance How close the agent gets to its destination before considering itself "arrived." 0.5 - 1.5, depending on the situation
Auto Braking When enabled, the agent slows down smoothly as it approaches its destination instead of stopping abruptly. Checked, for natural-feeling stops

Writing Your First Script: Chasing the Player

Let's build the simplest and most common NavMesh use case: an enemy that constantly moves toward the player.

using UnityEngine;
using UnityEngine.AI;

public class EnemyChase : MonoBehaviour
{
    public Transform target;

    private NavMeshAgent agent;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
    }

    void Update()
    {
        if (target != null)
        {
            agent.SetDestination(target.position);
        }
    }
}

Attach this script to your enemy, and drag your Player object into the Target field in the Inspector. Press Play — the enemy will now automatically walk toward the player, navigating around any walls or obstacles in between.

Line By Line Explanation

public Transform target; is a reference to whatever the enemy should chase — typically the player's Transform, assigned directly in the Inspector for simplicity in this beginner example.

agent = GetComponent<NavMeshAgent>(); caches the NavMeshAgent reference once in Start(), following the same caching pattern used throughout this series.

agent.SetDestination(target.position); is the entire pathfinding instruction. This single line tells Unity "calculate a path to this position and move there," and the NavMeshAgent handles literally everything else — pathing around obstacles, accelerating, rotating to face the movement direction, and avoiding other agents.

Calling SetDestination() every frame in Update() is intentional here, since the player is constantly moving and the enemy needs to continuously recalculate its path toward the player's current position rather than a single fixed point.

A Real Example: Building a Patrol Route

Let's look at a simple example of the second most common NavMesh pattern: an NPC or enemy that moves between a set of fixed waypoints rather than chasing a moving target.

using UnityEngine;
using UnityEngine.AI;

public class EnemyPatrol : MonoBehaviour
{
    public Transform[] waypoints;
    public float waitTimeAtPoint = 2f;

    private NavMeshAgent agent;
    private int currentWaypointIndex = 0;
    private float waitTimer = 0f;
    private bool isWaiting = false;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        GoToNextWaypoint();
    }

    void Update()
    {
        if (isWaiting)
        {
            waitTimer += Time.deltaTime;
            if (waitTimer >= waitTimeAtPoint)
            {
                isWaiting = false;
                waitTimer = 0f;
                GoToNextWaypoint();
            }
            return;
        }

        if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
        {
            isWaiting = true;
        }
    }

    void GoToNextWaypoint()
    {
        if (waypoints.Length == 0)
        {
            return;
        }

        agent.SetDestination(waypoints[currentWaypointIndex].position);
        currentWaypointIndex = (currentWaypointIndex + 1) % waypoints.Length;
    }
}

Create several empty GameObjects in your scene to act as waypoints, drag them into the Waypoints array on this script in order, and attach it to your enemy alongside a NavMeshAgent. The enemy will now walk to each point in sequence, pause briefly, and continue to the next one in a loop.

Key Lines Explained

agent.pathPending is true while Unity is still calculating a path and has not finished yet. Checking this prevents your script from thinking the agent has "arrived" during the brief moment the path is still being computed.

agent.remainingDistance <= agent.stoppingDistance checks whether the agent is close enough to its current destination to be considered arrived, using the Stopping Distance value set on the NavMeshAgent component itself.

currentWaypointIndex = (currentWaypointIndex + 1) % waypoints.Length; advances to the next waypoint in the array, and the modulo operator (%) wraps back around to the first waypoint once the last one is reached, creating a continuous loop.



Connecting NavMesh to the Animator

Since you already built an Animator-driven character in our earlier guide, here is how to drive the same Speed parameter using a NavMeshAgent instead of a Character Controller.

using UnityEngine;
using UnityEngine.AI;

public class NavMeshAnimatorBridge : MonoBehaviour
{
    private NavMeshAgent agent;
    private Animator animator;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>();
    }

    void Update()
    {
        float normalizedSpeed = agent.velocity.magnitude / agent.speed;
        animator.SetFloat("Speed", normalizedSpeed);
    }
}

agent.velocity gives the agent's current actual movement speed, which can be lower than its maximum Speed setting while accelerating, decelerating, or turning. Dividing by agent.speed normalizes this into a roughly 0-to-1 range, which works well as input for the same Blend Tree built in our Animator guide, without needing two different Speed calculation systems for player and enemy characters.

Beginner Mistakes

  • Forgetting to mark level geometry as Navigation Static before baking. Unmarked geometry is invisible to the bake process, leaving floors un-walkable even though they look fine visually.
  • Forgetting to re-bake after changing the level. The NavMesh is a snapshot taken at bake time — moving walls or adding new geometry afterward has no effect until you bake again.
  • Setting transform.position directly on a NavMeshAgent-controlled character. This can desynchronize the agent from its internal pathfinding state. Use Warp() instead if you need to instantly teleport an agent.
  • Calling SetDestination() every single frame for a patrol script. Unlike chasing a moving player, a patrol destination is fixed — recalculating it constantly wastes CPU cycles for no benefit; only call it once when starting a new leg of the patrol.
  • Not accounting for agent.pathPending. Checking remainingDistance before the path has finished calculating can cause false "arrived" detections, especially right after SetDestination() is called.
  • Adding a NavMeshAgent and a Rigidbody to the same object without understanding the interaction. NavMeshAgent handles its own movement and does not need a Rigidbody for basic functionality; combining them without care can cause conflicting movement behavior.
  • Forgetting to add a NavMesh Obstacle to dynamic objects like doors or movable crates. Without this, agents will not know to path around objects that move during gameplay.

Troubleshooting Guide

Problem: My agent does not move at all.

Likely cause: The NavMesh was never baked, the floor was not marked Navigation Static, or the agent's starting position is not actually on the baked NavMesh surface.

Fix: Confirm the blue NavMesh overlay is visible on your floor in the Scene view. Check that the agent spawns on or very near that blue area, not floating above it or embedded inside a wall.

Problem: I get a "Failed to create agent" or similar warning in the console.

Likely cause: The agent's spawn position is too far from any baked NavMesh surface for Unity to place it correctly.

Fix: Move the agent's starting position closer to or directly onto the blue NavMesh area, or increase the NavMesh Surface's bake settings to cover the intended spawn area.

Problem: My enemy walks straight through a wall instead of going around it.

Likely cause: The wall was not marked Navigation Static before baking, so it was never factored into the NavMesh calculation.

Fix: Select the wall, mark it Navigation Static, and re-bake the NavMesh Surface.

Problem: My patrol script thinks the agent has arrived immediately, before it actually moves.

Likely cause: remainingDistance is being checked before pathPending has finished, so it briefly reads an old or zero value right after SetDestination() is called.

Fix: Always check !agent.pathPending alongside the remainingDistance comparison, as shown in the EnemyPatrol script in this guide.

Problem: My enemy's animation does not match its movement speed.

Likely cause: The Animator's Speed parameter is being driven by agent.speed (the maximum configured speed) instead of agent.velocity.magnitude (the actual current speed).

Fix: Use agent.velocity.magnitude divided by agent.speed, as shown in the NavMeshAnimatorBridge script, so the animation reflects real-time acceleration and deceleration.

Problem: Multiple agents pile up and push through each other at a doorway.

Likely cause: Avoidance settings, or the Agent Radius set during baking, may be too small relative to the actual space available for multiple agents to pass through comfortably.

Fix: Adjust the NavMeshAgent's Quality / Avoidance settings, or widen narrow passages in your level geometry if many agents are expected to pass through the same area simultaneously.



Best Practices

  • Always mark relevant level geometry as Navigation Static before baking, and remember to re-bake after any layout changes.
  • Use NavMesh Obstacle components on dynamic objects like doors or movable crates so agents correctly path around them at runtime.
  • Only call SetDestination() when the target actually needs to change — every frame for a moving chase target, but only once per waypoint for fixed patrol points.
  • Always check !agent.pathPending before relying on remainingDistance, to avoid false "arrived" detections.
  • Drive Animator parameters using agent.velocity.magnitude rather than the configured maximum Speed, for animation that reflects real acceleration and deceleration.
  • Keep Agent Radius and Height values consistent with your actual character size, similar to tuning Character Controller's Radius and Height from our earlier guide.
  • Limit the number of simultaneously active pathfinding agents in performance-sensitive scenes, especially on mobile, per our Mobile Optimization guide.


Frequently Asked Questions

What is a NavMesh in Unity?

A NavMesh is a simplified representation of the walkable surfaces in a level, generated through a baking process, which Unity uses to calculate paths for AI-controlled characters around obstacles automatically.

What is a NavMeshAgent?

It is a component attached to an AI-controlled character that moves it along a baked NavMesh toward a given destination, automatically handling pathfinding, obstacle avoidance, and movement.

What is the difference between NavMeshAgent and Character Controller?

NavMeshAgent is designed for AI-controlled characters that calculate their own paths toward a destination automatically, while Character Controller is designed for direct, frame-by-frame player input where you script every movement decision yourself.

How do I bake a NavMesh in Unity?

Mark your level geometry as Navigation Static, add a NavMesh Surface component to a GameObject (after installing the AI Navigation package), and click Bake in its Inspector.

Why does my NavMeshAgent not move?

The most common causes are the NavMesh never having been baked, the floor not being marked Navigation Static, or the agent's spawn position being too far from the actual baked NavMesh surface.

Why does my enemy walk through walls instead of around them?

The wall geometry was likely not marked Navigation Static before the NavMesh was baked, so it was never accounted for in the pathfinding calculation. Mark it and re-bake.

How do I make an enemy chase the player in Unity?

Add a NavMeshAgent to the enemy, then call agent.SetDestination(player.transform.position) every frame in Update(), which continuously recalculates the path toward the player's current location.

How do I make an NPC patrol between points?

Store an array of waypoint Transforms, call SetDestination() toward the current waypoint, check agent.remainingDistance combined with !agent.pathPending to detect arrival, then advance to the next waypoint in the array.

What does Stopping Distance do on a NavMeshAgent?

It defines how close the agent needs to get to its destination before it is considered to have arrived, useful for preventing an enemy from walking directly on top of the player before stopping.

Why does my patrol script think the agent arrived immediately?

This usually happens when remainingDistance is checked before the path calculation has finished. Always check !agent.pathPending alongside remainingDistance to avoid this false positive.

Can I use NavMesh with the Animator system?

Yes. Drive the Animator's Speed parameter using agent.velocity.magnitude divided by agent.speed, which produces a normalized value that works well with the same Blend Tree setup used for player characters.

What is a NavMesh Obstacle?

It is a component you add to dynamic objects, like doors or movable crates, so that NavMeshAgents correctly detect and path around them at runtime, since the originally baked NavMesh does not automatically update for moving objects.

Do I need to re-bake the NavMesh if I move level geometry?

Yes. The NavMesh is a static snapshot calculated at bake time. Any meaningful change to walls, floors, or other Navigation Static geometry requires re-baking for the pathfinding data to stay accurate.

Can multiple NavMeshAgents avoid colliding with each other?

Yes, NavMeshAgent includes built-in avoidance behavior that helps agents steer around each other, though very narrow passages with many agents can still cause some congestion depending on Agent Radius and avoidance quality settings.

Is NavMesh suitable for mobile games?

Yes, but the number of simultaneously active pathfinding agents should be managed carefully, since each one adds CPU cost. Refer to our Mobile Optimization guide for general performance management strategies.

Action Steps

  1. Install the AI Navigation package and bake your first NavMesh on a simple test level with at least one wall obstacle.
  2. Add a NavMeshAgent to a test character and build the EnemyChase script, targeting your existing Character Controller player.
  3. Build the EnemyPatrol script with several waypoints and confirm the enemy loops correctly between them.
  4. Connect the NavMeshAnimatorBridge script to drive your existing Animator Controller's Speed parameter from the same guide.
  5. Test what happens when you move a wall without re-baking, to see firsthand why re-baking after layout changes matters.

Next Topics To Learn

Claude AI for Game Developers: A Practical Guide for Unity Devs

 If you have followed this blog for a while, you know we mostly cover Unity tutorials — Character Controllers, the Input System, NavMesh AI, and so on. This article is a bit different. Instead of teaching Unity directly, it covers a tool many of us are already using alongside Unity every day: Claude, the AI assistant from Anthropic.

This is not a hype piece. We will look at what Claude is actually good at for a solo or small-team game developer, where it falls short, what it costs, how it compares to alternatives, and some real, practical prompts you can use today for C# scripting, debugging, design documents, and content creation.



What Is Claude?

Claude is a family of AI language models built by Anthropic, accessible through a web and mobile chat interface at claude.ai, through an API for developers, and through dedicated tools like Claude Code for working directly in a codebase. For a game developer, the most relevant entry points are the regular chat interface for quick scripting help and design discussion, and Claude Code for more involved work directly inside a project's files.

Anthropic offers several model sizes within the Claude family, generally trading off speed and cost against depth of reasoning. The smaller, faster models work well for quick lookups and simple code snippets, while the larger models handle more complex architectural questions, long design documents, and multi-file debugging better.

Why It Matters for Game Developers

Game development sits at an unusual intersection of skills — programming, writing, design, and sometimes art direction — often handled by one or two people on a small team. A common mistake indie developers make is treating an AI assistant as either "the thing that writes all my code" or dismissing it entirely as unreliable, when the more useful reality sits in between: it is a fast, knowledgeable collaborator for specific, well-scoped tasks.

When I first started using Claude alongside Unity work, the most immediate value was not writing entire systems from scratch, but rather explaining unfamiliar error messages, reviewing a script for edge cases I had not considered, and drafting the first pass of dialogue or item descriptions that I could then edit into my own voice.



Core Features Relevant to Game Development

Feature What It Does Relevance to Game Dev
Chat interface (claude.ai) Conversational access to Claude through web, desktop, and mobile apps. Quick C# questions, debugging error messages, brainstorming design ideas on the go.
Claude Code An agentic coding tool that works directly with your project files from the terminal, desktop app, or mobile app. Multi-file refactors, working across an actual Unity project's script folder rather than copy-pasting code back and forth.
Projects A way to organize related chats and reference documents together, so context persists across sessions. Keeping your game's design document, art style guide, or coding conventions available across every conversation about that project.
Artifacts A side panel for generating and iterating on standalone content like documents, code files, or diagrams. Drafting a design document or generating a structured data file (like a JSON item database) you can view and refine separately from the chat.
Web search Claude can search the web for current information when needed. Looking up current Unity API changes, recent package updates, or platform-specific requirements.
File creation and code execution Claude can create downloadable files and run code in a sandboxed environment. Generating a spreadsheet of game balance numbers, or a Word document version of a design doc you can share with a team.

For exact, up-to-date specifics on any of these features, including current usage limits and what is included in each plan, it is worth checking Anthropic's own documentation directly, since these details can change.



Practical Use Case 1: Debugging Unity C# Errors

This is, in my experience, where Claude saves the most time on a day-to-day basis. Unity's error messages are often technically accurate but not always immediately clear to a beginner, especially the dreaded NullReferenceException.

Example Prompt

"I'm getting this error in Unity: NullReferenceException: Object reference not set to an instance of an object, at PlayerMovement.cs line 24. Here's the script: [paste your script]. What's causing this and how do I fix it?"

Pasting both the exact error message and the relevant script gives Claude enough context to identify the likely cause — often something like a missing GetComponent() call in Start(), or a public field that was never assigned in the Inspector — rather than guessing generically.

A Real Example

Let's look at a simple example. Imagine your Character Controller script (from our earlier guide in this series) throws a NullReferenceException on the line calling controller.isGrounded. Pasting the script and error into Claude will typically lead to questions or suggestions checking whether GetComponent<CharacterController>() was actually called before Update() runs, since this is one of the most common causes of exactly this error — a problem we covered directly in the troubleshooting section of our Character Controller guide.

Practical Use Case 2: Reviewing and Improving Existing Scripts

Beyond fixing outright errors, Claude is useful for a second pass on working code — catching edge cases, suggesting more idiomatic C#, or flagging potential performance issues like the ones covered in our Mobile Optimization guide.

Example Prompt

"Here's my enemy patrol script using NavMeshAgent. Can you review it for bugs, and let me know if anything here could cause performance problems on mobile?"

This kind of open-ended review prompt tends to work better than asking yes/no questions, since it gives Claude room to point out things you did not specifically think to ask about — like a missing null check, or a GetComponent() call sitting inside Update() instead of being cached, both patterns we have flagged as beginner mistakes throughout this series.



Practical Use Case 3: Drafting Game Design Documents

Writing a full design document from a blank page is one of the more tedious parts of starting a project. Claude is well suited to producing a structured first draft you then edit into your own voice, rather than a finished, ready-to-ship document.

Example Prompt

"I'm designing a low-poly isometric survival game for mobile and PC, similar in tone to Whiteout Survival. Help me draft a one-page game design document covering core loop, main systems, and target audience. Ask me clarifying questions first if you need more details."

Notice this prompt explicitly invites clarifying questions rather than assuming Claude should guess at missing details — this generally produces a more useful, tailored result than a single one-shot request with no back-and-forth.

Practical Use Case 4: Writing Dialogue and Item Descriptions

Content writing for games — NPC dialogue, item flavor text, quest descriptions — benefits from AI assistance in a similar way to design documents: a strong first draft that you then revise for tone and consistency.

Example Prompt

"Write five short flavor text descriptions (one sentence each) for crafting materials in a survival game: scrap metal, cloth, purified water, herbs, and a rare power cell. Keep the tone practical and slightly weathered, not whimsical."

Specifying tone explicitly, as in this prompt, makes a meaningful difference in output quality — vague requests like "write some item descriptions" tend to produce generic results that need much heavier editing afterward.



When To Use Claude

  • Debugging specific, well-defined error messages with the relevant code attached.
  • Getting a second opinion or review pass on a script you have already written.
  • Drafting a first version of design documents, dialogue, or descriptive text you intend to revise.
  • Explaining unfamiliar Unity concepts, APIs, or C# language features in plain language.
  • Generating structured data, like a starting point for a JSON item database or a balance spreadsheet.

When Not To Use Claude

  • As a substitute for actually learning the fundamentals covered in guides like our Character Controller or Rigidbody articles — relying entirely on AI-generated code without understanding it makes debugging much harder later.
  • For final, ship-ready creative writing without a human editing pass — AI-drafted dialogue and descriptions generally need revision for consistency with your game's specific voice and lore.
  • For highly specialized or very recent Unity API changes without giving Claude the chance to search the web first, since training data has a cutoff and Unity's APIs evolve continuously.
  • As your only code reviewer on a team project — useful as a first pass, but not a replacement for a human reviewing for project-specific architecture and conventions.

This part usually confuses new users: an AI assistant being wrong sometimes is not unique to Claude or any specific tool — it is an inherent characteristic of how these systems work. The practical takeaway is to treat any AI-generated code or design advice the way you would treat advice from a knowledgeable but fallible colleague: useful, often correct, but always worth verifying against documentation or actual testing in your project.

Pros

Pro Why It Matters
Strong at explaining unfamiliar errors and concepts Particularly useful for beginners working through the exact kind of error messages covered throughout this Unity series.
Handles both code and creative writing well Useful across the full range of solo developer tasks, from C# scripts to NPC dialogue, without switching tools.
Claude Code integrates directly with project files Reduces the friction of copy-pasting code back and forth between a chat window and your IDE for larger tasks.
Free tier available Lets solo developers and students try the tool before committing to a paid plan.

Cons

Con Why It Matters
Can be confidently wrong Like any AI assistant, incorrect suggestions are presented with the same confidence as correct ones, so verification matters.
Free tier has usage limits Heavy daily use for a full-time project will likely require a paid plan to avoid hitting rate limits.
Not a substitute for Unity-specific documentation For exact, current API behavior, Unity's own official documentation remains the authoritative source.
Requires good prompting to get the best results Vague requests tend to produce generic, less useful output compared to specific, well-scoped prompts.



Claude Pricing (as of June 2026)

Pricing for AI products changes frequently, so always check claude.com/pricing directly for the current figures before making a decision. As of this writing, here is the individual plan breakdown most relevant to solo developers and small teams.

Plan Price Best For
Free $0 Chatting on web, iOS, Android, and desktop, with code generation, content writing, web search, and memory across conversations, subject to usage limits.
Pro $17 per month with an annual subscription, or $20 per month billed monthly Everyday productivity, including Claude Code, Claude Cowork, Claude Design, unlimited projects, and access to more Claude models — generally the right tier for a solo developer using Claude daily.
Max From $100 per month Significantly higher usage limits than Pro (5x or 20x, depending on the tier), higher output limits, and priority access during high traffic times — suited to heavy daily use or small teams sharing one heavy workflow.
Team $20 per seat per month billed annually ($25 if billed monthly) for Standard seats Teams of 5 to 150 people needing shared billing and administration — relevant if your indie studio grows beyond a solo operation.

For developers who want to use Claude programmatically — for example, building your own internal tool that calls Claude's API to help generate content for your game — pricing is based on tokens processed rather than a flat monthly fee. As of this writing, Claude Sonnet 4.6 costs $3 per million input tokens and $15 per million output tokens, while the faster, more cost-efficient Haiku 4.5 costs $1 per million input tokens and $5 per million output tokens. These API rates are separate from the consumer chat plans above and are intended for developers building Claude into their own applications or internal tools, not for typical day-to-day chat usage.

Claude vs Alternatives: A Fair Comparison

Tool Strengths for Game Dev Considerations
Claude Strong reasoning on complex, multi-step coding problems; Claude Code for direct project file integration; capable creative writing for dialogue and design docs. Free tier usage limits; like any AI tool, requires verification of generated code and facts.
ChatGPT Broad general-purpose capability, large existing user base, plugin and GPT ecosystem. Direct comparison depends heavily on the specific task and current model versions — both companies release updates frequently.
GitHub Copilot Deep inline autocomplete integration directly inside code editors like Visual Studio and Rider. Less suited to open-ended design or writing tasks; primarily an in-editor autocomplete tool rather than a conversational assistant.
Unity's own AI features Built directly into the Unity Editor for some workflows, with awareness of your specific project context. Coverage and capability vary by Unity version; check Unity's own documentation for current features.

This is not a "which tool is objectively best" ranking — that depends heavily on your specific workflow, budget, and which tool's outputs you personally find clearest to work with. Many developers use more than one of these tools for different tasks, such as Claude or ChatGPT for design discussion and Copilot for in-editor autocomplete.



Best Practices for Using Claude in Your Game Dev Workflow

  • Always include the exact error message and the relevant code together when asking for debugging help.
  • Specify your Unity version and render pipeline (Built-in, URP, or HDRP) when asking version-sensitive questions.
  • Treat generated design documents and dialogue as a first draft, not a final deliverable.
  • Ask Claude to explain its reasoning, not just provide an answer, especially for unfamiliar concepts — this builds your own understanding alongside getting unblocked.
  • Use Projects (if available on your plan) to keep your game's design document or coding conventions available across multiple conversations.
  • Verify important technical claims against Unity's official documentation, especially for newer or less common APIs.
  • Check Anthropic's pricing page directly before committing to a paid plan, since pricing and plan features are subject to change.

Frequently Asked Questions

What is Claude AI?

Claude is a family of AI language models built by Anthropic, accessible through a chat interface, an API for developers, and tools like Claude Code for working directly within a project's files.

Can Claude write Unity C# scripts for me?

Yes, Claude can generate C# scripts, though for production game code it works best as a collaborative tool — generating a first draft or specific function that you then review, test, and integrate into your project, rather than a fully autonomous code generator.

Is Claude free to use?

Yes, Anthropic offers a Free plan with access to the chat interface across web, mobile, and desktop, subject to usage limits. Paid plans (Pro, Max, Team) offer higher usage limits and additional features.

How much does Claude Pro cost?

As of this writing, Claude Pro costs $17 per month with an annual subscription, or $20 per month if billed monthly. Always check claude.com/pricing for the current rate, since pricing can change.

Is Claude better than ChatGPT for game development?

Both tools are capable for coding and creative writing tasks, and which one works better often comes down to personal preference and the specific task. Many developers use multiple AI tools for different parts of their workflow rather than committing to just one.

Can Claude help with Unity debugging?

Yes, this is one of the most practical use cases for solo developers — pasting an exact error message along with the relevant script generally produces a clear explanation and likely fix.

Should I use AI-generated code without reviewing it?

No. AI-generated code, including from Claude, should always be reviewed, tested, and understood before being relied upon in a real project, since any AI assistant can produce confidently incorrect suggestions.

Can Claude write game dialogue and item descriptions?

Yes, Claude can draft dialogue, item descriptions, and other game content. Results are significantly better when you specify tone, provide examples of your existing writing style, and treat the output as a first draft to edit.

What is Claude Code?

Claude Code is an agentic coding tool that lets developers work with Claude directly inside a codebase from the terminal, desktop app, or mobile app, rather than copy-pasting code back and forth through a chat window.

Does Claude know about the latest Unity updates?

Claude has a training data cutoff and may not be aware of very recent Unity releases or API changes by default, though it can search the web for current information when needed. Always verify version-specific details against Unity's official documentation.

Is there a difference between Claude's chat app and the API?

Yes. The chat plans (Free, Pro, Max, Team) are flat-rate consumer subscriptions for using Claude through the chat interface. The API is billed per token and is intended for developers building Claude into their own applications or internal tools.

Can a solo indie developer realistically afford Claude?

The Free plan is sufficient for light, occasional use, and the Pro plan at roughly $17 to $20 per month is generally affordable for a solo developer using it as a regular part of their daily workflow.

Does Claude replace the need to learn Unity and C# fundamentals?

No. Claude works best as a tool that accelerates learning and saves time on specific tasks, not as a replacement for understanding the fundamentals covered in guides like our Character Controller, Rigidbody, and Animator series.

Key Takeaways

  • Claude is most useful for solo and small-team developers as a debugging assistant, code reviewer, and first-draft writer for design documents and game content.
  • Specific, well-scoped prompts with relevant context (code, error messages, tone examples) produce significantly better results than vague requests.
  • Claude offers a Free plan, with Pro starting around $17 to $20 per month for everyday productivity use — always verify current pricing directly on Anthropic's site.
  • Generated code and creative content should always be reviewed and tested, not accepted blindly.
  • Claude works well alongside, not instead of, learning Unity fundamentals and consulting official documentation.

Action Steps

  1. Try the Free plan at claude.ai with a real debugging problem from your current project.
  2. Practice writing specific, context-rich prompts using the examples in this guide as a template.
  3. Use Claude to draft a one-page design document for a project idea you have been putting off starting.
  4. Compare the Free, Pro, and Max plan details directly on claude.com/pricing against your actual usage patterns before upgrading.
  5. If you do production coding work regularly, explore Claude Code for direct integration with your project files.

How to Use ElevenLabs for Game Development: AI Voice Acting, NPC Dialogue, and Sound Design

Most indie games ship without voice acting. Not because developers do not want it, but because hiring voice actors is expensive, scheduling ...