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.