Debugging in Unity

Summary

Debugging is the process of finding out why a program does something other than what you intended, and then fixing the cause rather than the symptom. Unity gives you three complementary tools for it: the Console window, which shows errors, warnings and your own log messages; visual debugging with debug lines and gizmos in the Scene view; and a full IDE debugger (Visual Studio, VS Code or Rider) that can pause the game on a chosen line so you can inspect every variable. None of these replaces thinking: the most reliable approach is a short, repeatable method of reproducing the bug, isolating where it happens, forming a hypothesis and testing it.

The course lab series treats debugging as a practical habit: early sessions use Debug.Log, later sessions introduce breakpoints, and tutors are asked to normalise checking the Inspector before assuming a logic error (course lab series, see source-csharp-unity-labs).

Key ideas

  • Read the Console first. Compiler errors stop Play mode altogether; runtime errors such as nullreferenceexception name the script and line where they happened.
  • Log with purpose. A good log message says which object, which method and what value. Pass the object as a context so you can click straight to it.
  • Make the invisible visible. Directions, ranges and raycasts are easier to check as lines and spheres in the Scene view than as numbers.
  • Pause and inspect. A breakpoint stops the game on a line; you can then read every local variable and field, step one line at a time, and see exactly where the program’s path differs from your expectation.
  • Debug mode must be on. The Unity Editor can only accept a debugger when its code optimization mode is set to Debug.
  • Change one thing at a time. A fix you cannot explain is a guess, and guesses tend to create the next bug.

In practice

The Console window

Open it with Window > General > Console. The Unity 6.3 Manual describes its toolbar (Unity Technologies, Console window reference, Unity 6.3 Manual):

ControlWhat it does
ClearRemoves messages generated by your code but keeps compiler errors. Its dropdown offers Clear on Play, Clear on Build and Clear on Recompile.
CollapseShows only the first instance of a repeating message, with a count. Useful when a log in Update floods the list.
Error PausePauses playback when a script calls Debug.LogError (not for Debug.Log).
Editor / Player dropdownConnects the Console to a development build so you can read its logs.
SearchFilters messages by exact text.
Message / Warning / Error togglesShow the count of each type and hide or show that type.

Selecting a message shows its full text and stack trace in the detail area at the bottom. The stack trace lists the chain of method calls that led to the message, newest first, with clickable links to code lines. Read it from the top: the first line that names your script is usually where to start looking. Double-clicking a message normally opens your code editor at the line that produced it; this depends on the External Script Editor set under Edit > Preferences > External Tools (on macOS, Unity > Settings), and Unity’s issue tracker records a failure to jump to the line as a bug (fixed in the Visual Studio Editor package 2.0) rather than intended behaviour (Unity Issue Tracker, When double-clicking on a script, Visual Studio no longer opens on the exact line).

Stack trace detail is configurable per message type (Error, Assert, Warning, Log, Exception) with three options: None, ScriptOnly (managed code only, the default) and Full (managed and engine code). The Manual warns that resolving a stack trace, especially a full one, is resource-intensive, so it should be used for debugging rather than left on in shipped builds (Unity Technologies, Stack trace logging, Unity 6.3 Manual).

Logging with Debug.Log, Debug.LogWarning and Debug.LogError

The three methods differ in severity and Console icon. Use them consistently: Log for tracing what happened, LogWarning for something suspicious that the game can survive, LogError for something that should never happen (and that Error Pause will stop on).

Every overload accepts an optional second argument, context, described as the “Object to which the message applies”. When you click a message logged with a context, Unity momentarily highlights that object in the Hierarchy, which tells you which of twenty identical enemies logged it (Unity Technologies, Scripting API: Debug.Log, Unity 6.3). Unity also writes Debug.Log messages to the Editor and Player log files.

using UnityEngine;
 
public class EnemyDebugProbe : MonoBehaviour
{
    [SerializeField] private float detectRadius = 3f;
    [SerializeField] private Transform target;
 
    private void Start()
    {
        if (target == null)
        {
            // 'this' as context: clicking the message highlights this object
            Debug.LogError($"{name}: target is not assigned in the Inspector.", this);
            enabled = false;
            return;
        }
        Debug.Log($"{name} watching {target.name}", this);
    }
}

The guard turns a vague nullreferenceexception in Update into a named error at start-up that points straight at the misconfigured object. See unity-inspector-references for why unassigned references are the most common beginner fault.

Visual debugging: debug lines and gizmos

Debug.DrawRay(start, dir, color, duration) draws a line from start to start + dir. With the default duration of 0 the line lasts one frame, so call it every frame from Update. The line is drawn in the Scene view, and in the Game view only if Gizmos are enabled there (Unity Technologies, Scripting API: Debug.DrawRay, Unity 6.3). Debug.DrawLine works the same way between two points.

For things that should be visible while you edit, such as a detection radius, implement OnDrawGizmos (always drawn, and clickable to select the object) or OnDrawGizmosSelected (drawn only while the object is selected) and use the Gizmos class (Unity Technologies, Scripting API: MonoBehaviour.OnDrawGizmos and MonoBehaviour.OnDrawGizmosSelected, Unity 6.3).

    private void Update()
    {
        Vector3 toTarget = target.position - transform.position;
        bool inRange = toTarget.magnitude <= detectRadius;
 
        // Green when in range, red when not
        Debug.DrawRay(transform.position, toTarget, inRange ? Color.green : Color.red);
    }
 
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, detectRadius);
    }

If the ray turns green while the yellow sphere clearly does not reach the target, the bug is in the distance logic; if both agree but the enemy still does nothing, the bug is further down the chain.

Debug.Break() pauses the Editor from code, which is handy when an event happens too fast to pause by hand (Unity Technologies, Scripting API: Debug.Break, Unity 6.3).

Attaching a debugger

The Unity Manual sets out the same four steps for every supported IDE (Unity Technologies, Set up C# debugging, Unity 6.3 Manual):

  1. Configure the IDE for Unity. Set it as the External Script Editor in Preferences > External Tools and install its Unity support (below).
  2. Switch code optimization to Debug mode. Click the bug icon at the bottom-right of the Editor status bar and choose Debug mode. Debug mode allows a debugger to attach but runs your code more slowly in Play mode; Release mode runs faster but cannot be debugged. Preferences > General > Code Optimization On Startup sets which mode the Editor starts in.
  3. Attach the IDE to the Unity Editor process.
  4. Return to Unity and enter Play mode. When execution reaches a breakpoint, the Editor freezes until you continue or stop debugging in the IDE. This is expected, not a crash.

How to attach in each IDE:

IDEUnity supportAttach
Visual Studio (Windows)Visual Studio Tools for Unity (the Game development with Unity workload)Click Attach to Unity (F5), then press Play in Unity. The dropdown can switch the button to Attach to Unity and Play (Microsoft, Using Visual Studio Tools for Unity).
VS CodeThe Unity extension by Microsoft, which installs C# Dev Kit; in Unity, the Visual Studio Editor package 2.0.20 or later (not the legacy “Visual Studio Code Editor” package)Press F5; the default configuration attaches to the Editor that has the project open (Microsoft, Unity development with VS Code).
JetBrains RiderBuilt-in Unity supportUse the Attach to Unity Editor & Play run configuration (JetBrains, Debug Unity applications).

To debug a build instead of the Editor, tick Development Build and Script Debugging in the build settings, then attach to the Player process. Managed code debugging is supported on all platforms except Web (Unity Technologies, Set up C# debugging).

Breakpoints, stepping and inspecting

  • Breakpoint: click in the margin (gutter) beside a line number. Execution pauses before that line runs.
  • Locals / Variables: shows every local variable and this (the script’s fields) at the paused line. Hover over a variable in the code for the same information.
  • Watch: add an expression such as score, transform.position.y or target == null to see it update as you step.
  • Call Stack: shows how execution reached this line, matching the Console’s stack trace.
  • Stepping: Step Over runs the current line and stops on the next; Step Into follows a method call into its body; Step Out finishes the current method; Continue runs to the next breakpoint. In Visual Studio and VS Code the default keys are F10, F11, Shift+F11 and F5 respectively; Rider uses different defaults, so use its toolbar buttons until you learn them.
  • Conditional breakpoint: right-click a breakpoint and add a condition (for example health <= 0 or name == "Enemy (3)") or a hit count, so the game only pauses in the case you care about. This is far quicker than stepping through 300 frames of Update. Rider also offers pausepoints, which pause the Unity Editor at the end of the current frame instead of suspending code, so you can inspect the scene (JetBrains, Debug Unity applications).

If a breakpoint is never hit, that is evidence too: either the method is not running at all (wrong method name or signature, script disabled, component not attached) or the debugger is not attached (Release mode, wrong Unity instance chosen, Play mode not entered after attaching).

A systematic method

  1. Reproduce. Find the shortest sequence of actions that makes the bug happen every time. Write it down. If you cannot reproduce it, log more until you can.
  2. Isolate. Narrow down where the behaviour first goes wrong: which object, which script, which frame. Disable components, remove objects or move a breakpoint earlier until you find the first point where a value differs from what you expected.
  3. Hypothesise. State one specific, testable cause: “OnTriggerEnter2D never runs because the coin has no trigger collider”, not “physics is broken”.
  4. Test. Predict what a log, breakpoint or Inspector check will show if the hypothesis is right, then look. If it is right, make the smallest fix and re-run the reproduction steps; if it is wrong, you have still removed one possibility.

Check the inspector (missing references, wrong tags, disabled components, wrong collider type) before rewriting code; most beginner bugs in triggers and references are setup faults (see unity-collider2d-and-triggers).

Other engines

Godot shows errors in its Debugger panel and supports breakpoints in its built-in script editor; Unreal Engine uses the Output Log with UE_LOG and Blueprint breakpoints, and an attached Visual Studio or Rider for C++. The workflow (read the log, visualise, pause and inspect, then reason) carries across engines.

Evidence

  • The Unity 6.3 Manual states that code optimization has two modes: Debug mode “allows attaching to an external debugger, but code runs slower in Play mode”, whereas Release mode makes code run faster but prevents attaching a debugger. The mode is switched with the bug icon in the status bar (Unity Technologies, Set up C# debugging and Status bar, Unity 6.3 Manual).
  • The same page lists Visual Studio, Visual Studio Code and JetBrains Rider as the IDEs that can attach, and notes that the Editor is unresponsive at a breakpoint until you continue or stop debugging.
  • The Scripting API says the context argument of Debug.Log causes Unity to highlight that object in the Hierarchy when the message is clicked (Unity Technologies, Scripting API: Debug.Log).
  • The Manual describes stack trace logging options of None, ScriptOnly (default) and Full, and advises using stack traces only for debugging because resolving them is resource-intensive (Unity Technologies, Stack trace logging).
  • The course lab series introduces Debug.Log early and breakpoints later, and frames debugging as tracing values through fields, parameters, lifecycle callbacks and Inspector assignments before rewriting code (course lab series, see source-csharp-unity-labs).

Implications

  • Arguably, the first debugging skill to teach is reading the Console’s top error and its first project line, because it resolves a large share of beginner problems without any other tool.
  • Logging and the debugger complement each other: logs show a history across many frames, while a breakpoint shows everything at one moment. A sensible habit is to log to find the right frame, then breakpoint to inspect it.
  • Release mode is the right default when you are not debugging, because Play mode runs faster; switching to Debug mode is a deliberate step, and forgetting it is the most common reason a breakpoint “does nothing”.
  • Guard clauses with Debug.LogError(..., this) in Start or Awake convert silent misconfiguration into a clear message, which is cheaper than debugging it later.

Open questions

  • How much logging should remain in a finished project? Unity writes logs to the Player log, but this page does not measure the cost of leaving Debug.Log calls in release builds; see unity-profiler for measuring performance.
  • When should students move from Debug.Log to the debugger? The course sequence introduces logging first; whether earlier exposure to breakpoints helps beginners is untested here.

Practice

Create a 2D scene with a Player (Sprite, Rigidbody2D with Gravity Scale 0, BoxCollider2D, tag Player, starting at y = 0) and three Coins (Sprite, CircleCollider2D with Is Trigger on, tag Coin). To move the Player, reuse a movement script from unity-input or simply drag it onto each coin in the Scene view during Play mode. Add this script to the Player. It compiles cleanly but has three bugs, and the score never goes up.

using UnityEngine;
 
public class BuggyCoinCollector : MonoBehaviour
{
    [SerializeField] private int coinValue = 10;
    private int score;
 
    public int Score => score;
 
    private void Update()
    {
        // Meant to reset the score if the player falls off the level
        if (transform.position.y < 10f)
            score = 0;
    }
 
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("coin"))
        {
            score += coinValue;
            Destroy(other.gameObject);
        }
    }
}

Find and fix all three using the tools on this page, not by guessing. Write down, for each bug, which tool revealed it.

Success test:

  • you set a breakpoint inside the trigger method, entered Play mode in Debug mode, and can explain why it was never hit
  • a Debug.Log(..., this) shows the tag of whatever the player touches
  • a Watch on score (or a conditional breakpoint on score > 0) shows the score being wiped, and you can say which line does it
  • after your fixes, collecting all three coins logs a score of 30

Extension:

  • add a Debug.DrawRay from the player to the nearest coin and an OnDrawGizmosSelected circle showing a pickup radius
  • turn on Error Pause, add a Debug.LogError when the player falls below y = -10, and check that the game pauses there

Self-test

  1. Your breakpoint is never hit, although you are sure the line should run. Name two settings or steps to check before suspecting your code.
  2. What does passing this as the second argument to Debug.Log do?
  3. Where does Debug.DrawRay draw, and why must it usually be called every frame?
  4. What is the difference between Step Over and Step Into?
  5. When is a conditional breakpoint better than an ordinary one?
  6. In the Practice script, what are the three bugs?

Answers

  1. Code optimization must be in Debug mode (bug icon in the status bar), and the IDE must be attached to the correct Unity Editor instance before you enter Play mode. Also confirm the method actually runs (correct name and signature, component enabled).
  2. It sets the context object: clicking the message in the Console highlights that object in the Hierarchy, so you know which instance logged it.
  3. In the Scene view, and in the Game view only when Gizmos are enabled there. With the default duration of 0 the line lasts one frame, so it must be redrawn each frame.
  4. Step Over runs the current line, including any method it calls, and stops on the next line; Step Into enters the called method so you can follow it line by line.
  5. When the line runs many times and you only care about one case, such as a particular object or a value crossing a threshold; it stops only when the condition is true.
  6. OnTriggerEnter(Collider) is the 3D callback and never runs for 2D colliders (it should be OnTriggerEnter2D(Collider2D)); the tag check uses "coin" but the tag is Coin (tags are case-sensitive); and the fall check uses < 10f instead of < -10f, so the score is reset every frame.

External references