Unity Development

Unity Debugging: Console.Log Is Your Friend

Coming from Rails development, where I could use puts anywhere and see output instantly, Unity debugging initially frustrated me. Why wasn't my code working? What values were my variables actually holding? How could I trace the flow of execution?

Then I discovered Unity's debugging tools, and realized that Debug.Log is just the beginning. In this post, I'll share the debugging techniques that saved my sanity as a Unity beginner.

The Unity Console: Your Debug Window

Unlike Rails where debug output goes to the terminal, Unity has a dedicated Console window.

Access it: Window → General → Console (or Ctrl/Cmd + Shift + C)

The Console shows three types of messages:
- Info (white) - Debug.Log() messages
- Warnings (yellow) - Debug.LogWarning() messages

- Errors (red) - Debug.LogError() and exceptions

Basic Debug.Log Techniques

1. Simple Value Logging

public class PlayerController : MonoBehaviour 
{
    public float speed = 5f;
    private float horizontalInput;

    void Update() 
    {
        horizontalInput = Input.GetAxisRaw("Horizontal");

        // Basic logging
        Debug.Log("Horizontal Input: " + horizontalInput);
        Debug.Log($"Player position: {transform.position}");

        // Check if code is reached
        if (Input.GetButtonDown("Jump")) {
            Debug.Log("Jump button pressed!");
        }
    }
}

2. Conditional Logging

public class Enemy : MonoBehaviour 
{
    public bool debugMode = true;  // Toggle in inspector
    private float health = 100f;

    public void TakeDamage(float damage) 
    {
        health -= damage;

        // Only log when debugging
        if (debugMode) {
            Debug.Log($"Enemy took {damage} damage. Health: {health}");
        }

        // Important events always logged
        if (health <= 0) {
            Debug.Log("Enemy died!");
        }
    }
}

3. Structured Logging with Context

public class GameManager : MonoBehaviour 
{
    void Start() 
    {
        // Include context in logs
        Debug.Log($"[GameManager] Game started at {System.DateTime.Now}");
    }

    public void AddScore(int points) 
    {
        int oldScore = score;
        score += points;

        // Show before/after values
        Debug.Log($"[Score] {oldScore} → {score} (+{points})");
    }

    void OnApplicationPause(bool pauseStatus) 
    {
        // Log system events
        Debug.Log($"[System] Game {(pauseStatus ? "paused" : "resumed")}");
    }
}

Advanced Debugging Techniques

1. Color-Coded Logging

public class DebugHelper 
{
    public static void LogInfo(string message) 
    {
        Debug.Log($"<color=white>[INFO]</color> {message}");
    }

    public static void LogWarning(string message) 
    {
        Debug.Log($"<color=yellow>[WARNING]</color> {message}");
    }

    public static void LogError(string message) 
    {
        Debug.Log($"<color=red>[ERROR]</color> {message}");
    }

    public static void LogSuccess(string message) 
    {
        Debug.Log($"<color=green>[SUCCESS]</color> {message}");
    }
}

// Usage
DebugHelper.LogInfo("Player spawned successfully");
DebugHelper.LogWarning("Low health detected");
DebugHelper.LogError("Failed to load save file");
DebugHelper.LogSuccess("Level completed!");

2. Frame-Based Debugging

public class PlayerController : MonoBehaviour 
{
    private int debugFrame = 0;
    private int logEveryNFrames = 60; // Log once per second at 60fps

    void Update() 
    {
        debugFrame++;

        // Avoid console spam - log periodically
        if (debugFrame % logEveryNFrames == 0) {
            Debug.Log($"Frame {debugFrame}: Player at {transform.position}");
        }

        // But always log important events
        if (Input.GetButtonDown("Jump")) {
            Debug.Log($"Frame {debugFrame}: Jump initiated");
        }
    }
}

3. Component State Logging

public class PlayerController : MonoBehaviour 
{
    [Header("Debug")]
    public bool logStateChanges = true;

    private bool wasGrounded;
    private float lastHealth;

    void Update() 
    {
        bool isGrounded = CheckGrounded();
        float currentHealth = GetComponent<Health>().currentHealth;

        // Log state changes only
        if (logStateChanges) {
            if (isGrounded != wasGrounded) {
                Debug.Log($"Grounded state changed: {wasGrounded} → {isGrounded}");
            }

            if (currentHealth != lastHealth) {
                Debug.Log($"Health changed: {lastHealth} → {currentHealth}");
            }
        }

        wasGrounded = isGrounded;
        lastHealth = currentHealth;
    }
}

Visual Debugging with Gizmos

1. Debug Lines and Rays

public class PlayerController : MonoBehaviour 
{
    public float raycastDistance = 1f;

    void Update() 
    {
        // Visualize raycasts
        Vector3 rayStart = transform.position;
        Vector3 rayDirection = Vector3.down;

        Debug.DrawRay(rayStart, rayDirection * raycastDistance, Color.red);

        // Check what the raycast hits
        RaycastHit hit;
        if (Physics.Raycast(rayStart, rayDirection, out hit, raycastDistance)) {
            Debug.Log($"Raycast hit: {hit.collider.name}");

            // Draw line to hit point
            Debug.DrawLine(rayStart, hit.point, Color.green);
        }
    }
}

2. Custom Gizmos

public class EnemyAI : MonoBehaviour 
{
    public float detectionRadius = 5f;
    public float attackRadius = 2f;

    // Visualize AI ranges in Scene view
    void OnDrawGizmosSelected() 
    {
        // Detection range
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, detectionRadius);

        // Attack range
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRadius);

        // Current target line
        if (Application.isPlaying && currentTarget != null) {
            Gizmos.color = Color.blue;
            Gizmos.DrawLine(transform.position, currentTarget.position);
        }
    }

    void OnDrawGizmos() 
    {
        // Always visible gizmos
        Gizmos.color = Color.white;
        Gizmos.DrawWireCube(transform.position, Vector3.one * 0.5f);
    }
}

Inspector Debugging

1. Expose Private Variables for Debugging

public class PlayerController : MonoBehaviour 
{
    [Header("Movement")]
    public float speed = 5f;

    [Header("Debug Info (Read Only)")]
    [SerializeField] private float currentSpeed;     // Visible in inspector
    [SerializeField] private bool isGrounded;
    [SerializeField] private Vector3 velocity;

    void Update() 
    {
        // Update debug values
        currentSpeed = rigidbody.velocity.magnitude;
        isGrounded = CheckGrounded();
        velocity = rigidbody.velocity;

        // Watch these values change in real-time in inspector
    }
}

2. Debug Buttons in Inspector

public class GameManager : MonoBehaviour 
{
    [Header("Debug Controls")]
    public bool debugMode = true;

    [ContextMenu("Add 100 Score")]
    void DebugAddScore() 
    {
        AddScore(100);
        Debug.Log("Debug: Added 100 score");
    }

    [ContextMenu("Reset Player Position")]
    void DebugResetPlayer() 
    {
        GameObject player = GameObject.FindWithTag("Player");
        if (player != null) {
            player.transform.position = Vector3.zero;
            Debug.Log("Debug: Reset player position");
        }
    }

    [ContextMenu("Spawn 10 Enemies")]
    void DebugSpawnEnemies() 
    {
        for (int i = 0; i < 10; i++) {
            SpawnEnemy(Random.insideUnitSphere * 10f);
        }
        Debug.Log("Debug: Spawned 10 enemies");
    }
}

Performance Debugging

1. Frame Rate Monitoring

public class PerformanceMonitor : MonoBehaviour 
{
    private float deltaTime = 0f;
    private int frameCount = 0;
    private float fpsSum = 0f;

    void Update() 
    {
        deltaTime += (Time.unscaledDeltaTime - deltaTime) * 0.1f;
        frameCount++;

        float currentFPS = 1f / deltaTime;
        fpsSum += currentFPS;

        // Log average FPS every 60 frames
        if (frameCount >= 60) {
            float averageFPS = fpsSum / frameCount;

            if (averageFPS < 30f) {
                Debug.LogWarning($"Low FPS detected: {averageFPS:F1}");
            }

            frameCount = 0;
            fpsSum = 0f;
        }
    }

    void OnGUI() 
    {
        // Show FPS on screen during testing
        if (Application.isPlaying) {
            float fps = 1f / deltaTime;
            GUI.Label(new Rect(10, 10, 100, 20), $"FPS: {fps:F1}");
        }
    }
}

2. Memory Usage Tracking

public class MemoryMonitor : MonoBehaviour 
{
    private float checkInterval = 5f;
    private float nextCheck = 0f;

    void Update() 
    {
        if (Time.time >= nextCheck) {
            CheckMemoryUsage();
            nextCheck = Time.time + checkInterval;
        }
    }

    void CheckMemoryUsage() 
    {
        long memoryUsage = System.GC.GetTotalMemory(false);
        float memoryMB = memoryUsage / (1024f * 1024f);

        Debug.Log($"Memory usage: {memoryMB:F2} MB");

        if (memoryMB > 100f) {
            Debug.LogWarning($"High memory usage: {memoryMB:F2} MB");
        }
    }
}

Debugging Common Unity Issues

1. Null Reference Exceptions

public class PlayerController : MonoBehaviour 
{
    private Rigidbody rb;
    private PlayerInput input;

    void Start() 
    {
        // Defensive programming with logging
        rb = GetComponent<Rigidbody>();
        if (rb == null) {
            Debug.LogError("PlayerController: Rigidbody component not found!");
            return;
        }

        input = GetComponent<PlayerInput>();
        if (input == null) {
            Debug.LogError("PlayerController: PlayerInput component not found!");
            return;
        }

        Debug.Log("PlayerController: All components found successfully");
    }

    void Update() 
    {
        // Check before using
        if (rb == null || input == null) {
            Debug.LogError("PlayerController: Missing required components");
            return;
        }

        // Safe to use components here
    }
}

2. Physics Issues

public class PhysicsDebugger : MonoBehaviour 
{
    void OnCollisionEnter(Collision collision) 
    {
        Debug.Log($"Collision: {gameObject.name} hit {collision.gameObject.name}");
        Debug.Log($"Impact force: {collision.impulse.magnitude}");
        Debug.Log($"Contact points: {collision.contacts.Length}");

        foreach (ContactPoint contact in collision.contacts) {
            Debug.DrawRay(contact.point, contact.normal, Color.red, 2f);
        }
    }

    void OnTriggerEnter(Collider other) 
    {
        Debug.Log($"Trigger: {gameObject.name} entered by {other.name}");
    }

    void OnTriggerExit(Collider other) 
    {
        Debug.Log($"Trigger: {gameObject.name} exited by {other.name}");
    }
}

Debugging Best Practices

1. Use Compiler Directives

public class GameManager : MonoBehaviour 
{
    void Start() 
    {
        #if UNITY_EDITOR
        Debug.Log("Running in Unity Editor");
        #elif UNITY_STANDALONE
        Debug.Log("Running as standalone build");
        #elif UNITY_WEBGL
        Debug.Log("Running as WebGL build");
        #endif

        // Only debug in development builds
        #if DEVELOPMENT_BUILD
        EnableDebugFeatures();
        #endif
    }

    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    void DebugLog(string message) 
    {
        Debug.Log(message);
    }
}

2. Create Debug Categories

public static class DebugCategories 
{
    public const string PLAYER = "PLAYER";
    public const string ENEMY = "ENEMY";
    public const string UI = "UI";
    public const string AUDIO = "AUDIO";
    public const string SAVE = "SAVE";
}

public static class DebugLogger 
{
    public static bool enablePlayerLogs = true;
    public static bool enableEnemyLogs = true;
    public static bool enableUILogs = false;

    public static void Log(string category, string message) 
    {
        bool shouldLog = category switch {
            DebugCategories.PLAYER => enablePlayerLogs,
            DebugCategories.ENEMY => enableEnemyLogs,
            DebugCategories.UI => enableUILogs,
            _ => true
        };

        if (shouldLog) {
            Debug.Log($"[{category}] {message}");
        }
    }
}

// Usage
DebugLogger.Log(DebugCategories.PLAYER, "Player jumped");
DebugLogger.Log(DebugCategories.ENEMY, "Enemy spawned");

3. Debug Manager Component

public class DebugManager : MonoBehaviour 
{
    [Header("Debug Settings")]
    public bool showFPS = true;
    public bool showMemory = true;
    public bool logPlayerActions = true;
    public bool drawGizmos = true;

    [Header("Debug Keys")]
    public KeyCode toggleDebugKey = KeyCode.F1;
    public KeyCode reloadLevelKey = KeyCode.F5;
    public KeyCode pauseKey = KeyCode.P;

    private bool debugMenuVisible = false;

    void Update() 
    {
        if (Input.GetKeyDown(toggleDebugKey)) {
            debugMenuVisible = !debugMenuVisible;
        }

        if (Input.GetKeyDown(reloadLevelKey)) {
            ReloadCurrentLevel();
        }

        if (Input.GetKeyDown(pauseKey)) {
            TogglePause();
        }
    }

    void OnGUI() 
    {
        if (!debugMenuVisible) return;

        GUILayout.BeginArea(new Rect(10, 10, 300, 400));
        GUILayout.BeginVertical("box");

        GUILayout.Label("Debug Menu", GUI.skin.box);

        if (showFPS) {
            float fps = 1f / Time.unscaledDeltaTime;
            GUILayout.Label($"FPS: {fps:F1}");
        }

        if (showMemory) {
            long memory = System.GC.GetTotalMemory(false);
            float memoryMB = memory / (1024f * 1024f);
            GUILayout.Label($"Memory: {memoryMB:F2} MB");
        }

        if (GUILayout.Button("Reload Level")) {
            ReloadCurrentLevel();
        }

        if (GUILayout.Button("Clear Console")) {
            Debug.ClearDeveloperConsole();
        }

        GUILayout.EndVertical();
        GUILayout.EndArea();
    }
}

Building Without Debug Code

For production builds, disable debug logging:

// In build settings, add scripting define symbol: DISABLE_DEBUG

public static class Debug 
{
    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    [System.Diagnostics.Conditional("DEVELOPMENT_BUILD")]
    public static void Log(object message) 
    {
        UnityEngine.Debug.Log(message);
    }

    [System.Diagnostics.Conditional("UNITY_EDITOR")]
    [System.Diagnostics.Conditional("DEVELOPMENT_BUILD")]
    public static void LogWarning(object message) 
    {
        UnityEngine.Debug.LogWarning(message);
    }
}

Unity debugging doesn't have to be frustrating. With the right techniques, you can understand what your code is doing, track down bugs quickly, and optimize performance effectively.

Key takeaways:
- Use Debug.Log strategically, not everywhere
- Visual debugging with Gizmos shows spatial relationships
- Inspector debugging lets you watch values in real-time
- Color-coded logging improves readability
- Build debug tools into your game for easier testing

Start with basic Debug.Log statements, then gradually add visual debugging and performance monitoring as your projects grow in complexity!


Next up: "Common Unity Pitfalls for Web Developers" - We'll explore the gotchas that trip up developers coming from web frameworks.

Christopher Lim

Christopher Lim

Rails developer and Unity explorer. Family man, lifelong learner, and builder turning ideas into polished applications. Passionate about quality software development and continuous improvement.

Back to All Posts
Reading time: 9 min read

Enjoyed this unity development post?

Follow me for more insights on unity development, Rails development, and software engineering excellence.