Note
Logging is your game's "black box." It lets you peek inside running code without stopping execution. This article covers four levels of logging: from basic console output to writing errors to a file in a production build.
The simplest and fastest way to see what's happening in your code.
Debug.Log("Message")— regular informational message (white text).Debug.LogWarning("Warning")— yellow warning (not critical, but worth attention).Debug.LogError("Error")— red error (usually breaks logic but doesn't stop the game).
using UnityEngine;
public class PlayerHealth : MonoBehaviour
{
public int health = 100;
public void TakeDamage(int amount)
{
health -= amount;
Debug.Log($"Player took {amount} damage. Health left: {health}");
if (health <= 0)
{
Debug.LogError("PLAYER IS DEAD! Triggering death animation.");
}
if (health < 20)
{
Debug.LogWarning("Player health is critically low!");
}
}
}In the Unity editor — Console window (Window → General → Console).
- Instant, simple, requires no setup.
- Click on a message in the console — Unity highlights the object that sent the log.
- Remain in builds (if not removed) and reduce performance.
- Clutter the console in the final version of the game.
The [Conditional] attribute completely removes method calls from the build if a conditional compilation symbol is not defined.
public static class MyLogger
{
public static void Log(string message)
{
Debug.Log($"[LOG] {message}");
}
}using UnityEngine;
using System.Diagnostics;
public static class MyLogger
{
[Conditional("UNITY_EDITOR")] // Works only in the editor
public static void LogEditor(string message)
{
Debug.Log($"[EDITOR] {message}");
}
[Conditional("DEVELOPMENT_BUILD")] // Works in DEVELOPMENT builds
public static void LogDev(string message)
{
Debug.Log($"[DEV] {message}");
}
[Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
public static void LogEditorOrDev(string message)
{
Debug.Log($"[EDITOR OR DEV] {message}");
}
}- In
Player Settings→Other Settings→Script Compilation→Scripting Define Symbols, addDEVELOPMENT_BUILD(or create your own symbol, e.g.,MY_GAME_LOGS). - If the symbol is NOT defined — all calls to methods marked with
[Conditional("SYMBOL")]are completely cut out by the compiler. - Important: The method and its body physically exist in the code, but the calls disappear — this doesn't make an empty method faster; it removes the call itself.
UNITY_EDITOR— always defined when code is running inside the editor.DEVELOPMENT_BUILD— defined for Development builds (theDevelopment Buildcheckbox during building).UNITY_ANDROID/UNITY_IOS— automatically defined when building for that platform.
- Enables the Profiler.
- Allows remote debugging of the game.
- Retains all
Debug.Login the build (they can be viewed via the log file). - Adds performance overhead — not for release!
Debug.Logis technically called, but the Unity runtime in a build does not output them to a console. However, they still execute and create overhead.- To remove the overhead — use
[Conditional]or#if ... #endifpreprocessor directives.
public static void MyExpensiveLog(string message)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.Log(message);
#endif
}Difference from [Conditional]: the directive removes all code inside the block, while Conditional removes only method calls, leaving the method body in the build.
For most cases, Conditional is cleaner and more convenient.
In a finished build, there is no console. To find out what went wrong for the player, you need to write logs to a file.
- Windows:
%USERPROFILE%\AppData\LocalLow\<CompanyName>\<ProductName>\Player.log - Mac:
~/Library/Logs/<CompanyName>/<ProductName>/Player.log - Linux:
~/.config/unity3d/<CompanyName>/<ProductName>/Player.log
CompanyName and ProductName are taken from Edit → Project Settings → Player.
using UnityEngine;
using System.IO;
public class FileLogger : MonoBehaviour
{
private string logPath;
void Awake()
{
// Application.persistentDataPath — another option (cross-platform folder)
logPath = Path.Combine(Application.persistentDataPath, "my_game_log.txt");
Debug.Log($"Log file will be here: {logPath}");
// Intercept all Debug.Log messages
Application.logMessageReceived += HandleLog;
}
void HandleLog(string logString, string stackTrace, LogType type)
{
string entry = $"[{System.DateTime.Now:HH:mm:ss}] [{type}] {logString}\n";
if (type == LogType.Error || type == LogType.Exception)
{
entry += $"STACK: {stackTrace}\n";
}
File.AppendAllText(logPath, entry);
}
void OnDestroy()
{
Application.logMessageReceived -= HandleLog;
}
// Manual write
public static void WriteToFile(string message)
{
string path = Path.Combine(Application.persistentDataPath, "my_game_log.txt");
File.AppendAllText(path, $"[{System.DateTime.Now:HH:mm:ss}] {message}\n");
}
}- Catches all
Debug.Log,Debug.LogWarning,Debug.LogErrorcalls, even from third-party plugins. - Works in builds, including Release.
- Allows you to duplicate everything to a file without rewriting every log message.
| Stage | What to use |
|---|---|
| Early development | Debug.Log + Console window |
| Feature testing | [Conditional("DEVELOPMENT_BUILD")] + Development Build |
| Release build | Disable all logs via [Conditional] + keep only critical errors in a file via Application.logMessageReceived |
| Live support (LiveOps) | Write selected logs to Application.persistentDataPath and give the player a "Send log" button |