Sometimes things happen or do not happen differently than we expect it. That often requires a thorough investigation of the causality and flow of our code. The first move would normally be to throw a Debug.Log somewhere, where we expect our problem to happen. However, what if it is not enough?
Another parameter in Debug.Log
The problem: You got a number of objects with the same name that would be normally hard to distinguish.
The solution: The first thing to beef up the log statement is to add an extra parameter to our call. As per documentation:
public static void Log(object message, Object context);
If you pass a GameObject or Component as the optional context argument, Unity momentarily highlights that object in the Hierarchy window when you click the log message in the Console.
Now what does this mean? Let’s say our debug code looks like this:
public class FindMe : MonoBehaviour { void Start() { Debug.Log("ヘ(・ω| Where am I?", this); } }
Then by simply clicking on the log, the corresponding object will be highlighted. In this case a Component
was passed (this refers to the class we’re at, which eventually inherits after Component). Similarly, any GameObject
could be passed.
From the official description, it could seem that only objects that are in the Hierarchy window could be highlighted this way. Though the method header indicates that any UnityEngine.Object
could be used. This is exactly the case and we can use it to locate anything that has an instance ID. It includes but is not limited to classes like: Material
, ScriptableObject
, and Mesh
.
Bonus fact: We can use EditorGUIUtility.PingObject
to use the highlight functionality without writing logs. Link
Making the logs pop
The problem: You need to log a lot of things and while viewing the data and categorize it quickly at a glance. Using the search box and collapsing the logs do not work as well as the order of messages is important and a value that is searched for is not that obvious.
The solution: Spice up your logs visually. Log statements can take in Rich Text tags. Differentiating by color is way faster for eyes than reading each line. In the following example, if a slightly concerning value is shown, then the color of the value changes. This is easily noticed almost immediately.
Stopping the flow
The problem: The special case denoted by color is not understood enough once it passed. You need to inspect the scene and/or look at the exact execution in code.
The solution: Firstly, the more known approach. Setting up a breakpoint. Unity and Visual Studio work quite well together for this purpose. If on the Visual Studio side for basic debugging, link to the official docs. Getting it working in Unity is quite simple:
- Click to the left of the line of code you want the break to happen at.
- Attach to Unity.
- Press Play.
- The breakpoints is hit. From there Visual Studio allows to inspect Local values, investigate the Call Stack and use many other Visual Studio debugging tools.
When Visual Studio takes over, the Editor becomes unresponsive. This form of breakpoint is not suitable for inspecting the scene. If it is necessary to do so, Debug.Break
is your friend. It acts as if the pause button was pressed, except it is exactly at the desired line in code. Then the state of the scene can be fully explored. Alternatively use a debug using Debug.LogError
while enabling the option to pause on error.
Adjusting the console window
The problem: There are a myriad types of data that may need to be displayed. Not every type fits neatly in the limited space provided by the log entry.
The solution: Look into the options of the Console window and select as necessary. Additionally, timestamps can be enabled/disabled.
What did I just build?
The problem: Build completed with a result of ‘Succeeded’ is not enough information about what has just happened. Especially when looking to slim down the size of the app.
The solution: Console window options, then “Open Editor Log”. and select as necessary. A text file opens. Starting from section of Build Report there is a detailed breakdown of what assets went in and how much space do they take up, conveniently in descending order.
Logging based on preprocessor directives
The problem: There is a system, that is so vital to the whole app that every time it is being interacted with, you have a set of key debug statements that you want to know about. However it is too many to simply comment and uncomment every time.
The solution: Creating a static class that is active with a custom directive will call the corresponding log method with custom prefix. To quickly turn that off have another class with exactly the same name and methods that is active with negation of the directive. Then in Edit > Project Settings > Player > Other Settings > Scripting Define Symbols
add the symbol that is used to filter this particular logger. The following example assumes naming conventions for a networking module selective logging:
using UnityEngine; #if NET_LOGGER && (UNITY_EDITOR || DEVELOPMENT_BUILD) public static class DebugNet { public static void Log(string message) { Debug.Log($"[NET] {message}"); } public static void LogWarning(string message) { Debug.LogWarning($"[NET] {message}"); } public static void LogError(string message) { Debug.LogError($"[NET] {message}"); } } #else public static class DebugNet { public static void Log(string message) { } public static void LogWarning(string message) { } public static void LogError(string message) { } } #endif
To ensure that the logs never end up in the final build, display will also be dependent on the build being a development variant or in the editor. To learn more about directives that Unity provides, have a look in the documentation.
A potential drawback of this solution is that it does leave debug statements in the main code flow (even if you can hide them in a way that minimizes impact on performance). On the other hand, it is like leaving a comment in code of what is expected to have happened at that particular point in time.
Numbers not enough?
The problem: There is a lot of data. This data does not make much sense when viewed as numbers. Maybe it denotes some direction, or area, or it is a number/string, but it’s very important to know at a glance its current value and what object it’s attached to.
The solution: Depending on the needs, there are many approaches to debug by drawing things on the screen.
Debug class
The quickest way to draw a line is via the Debug class. From documentation (line, ray):
public static void DrawLine(Vector3 start, Vector3 end, Color color = Color.white, float duration = 0.0f, bool depthTest = true);
public static void DrawRay(Vector3 start, Vector3 dir, Color color = Color.white, float duration = 0.0f, bool depthTest = true);
Those two methods are especially useful in debugging things like raycast. The simple example would be to see how your object sees the world in front of them. This could highlight many possible issues that arise from improper layer setups or wrongly defined raycast source and direction.
[SerializeField] float hitDist = 1000; void Update() { Ray ray = new Ray(transform.position, transform.forward); RaycastHit hit; if (Physics.Raycast(ray, out hit, hitDist)) { Debug.DrawLine(ray.origin, hit.point, Color.green); //Do your thing on hit } else { Debug.DrawLine(ray.origin, ray.GetPoint(hitDist), Color.red); } }
This snippet shows how to visualize your raycast. Do not that if the point is not hit, DrawLine
is used also. This is so that we don’t draw a line of infinite length, but the actual distance that is being tested via raycast. The film shows behavior with the aforementioned code:
Gizmos and Handles
If you need more custom shapes to be displayed than simple lines then the Gizmos context is your friend. It can help you draw all sorts of basic shapes, as well as custom meshes. In the following example bounds are being visualized in a similar way a box collider might do it.
public class BoundingBox : MonoBehaviour { [SerializeField] Bounds bounds; [SerializeField] bool filled; void OnDrawGizmosSelected() { Gizmos.matrix = transform.localToWorldMatrix; if (filled) { Gizmos.DrawCube(bounds.center, bounds.size); } Gizmos.DrawWireCube(bounds.center, bounds.size); } }
To ensure the coordinates of the bounds drawn are responsive to the transform component, a matrix that will transform each drawn point from local space to world space is set.
There is also a Handles class is generally used to make Editor tools, as it has got mostly methods that return a value if the user modifies something. However, for debugging it has one major advantage that Gizmos doesn’t have. It has a handy way to add labels to your objects (documentation).
public static void Label(Vector3 position, string text, GUIStyle style);
void OnDrawGizmos() { Handles.Label(transform.position, $"{gameObject.name} {transform.position.ToString()}", EditorStyles.miniButton); }
This snippet demonstrates how to draw a temporary label over the object. It is drawn at the position of the object and displays the object name and position. This can be used to display all kinds of data that you need to know the current state of some variable, and to which object they correspond to with a single glance. As the label is white and thin by default, a style was applied to rectify that. For quick setup that will be visible with any background EditorStyles class was used that houses a button type display.
Note that Gizmos methods can be called only in functions OnDrawGizmos
, OnDrawGizmosSelected
and those with proper attribute usage. If you try it in any other place, it will not work. This means that Gizmos are specific to Components. If you need to draw things from an Editor window, then Handles are the only option.
In conclusion…
As a final note sometimes to aid with the debug code some third party tools or scripts are needed. One should always be mindful of adding robust plugins, as it may have many functionalities that are simply not needed for the project, and might even hinder it. That being said, now you have hopefully learned some techniques that you might be considering to use when tackling a new bug or aiming to gain a greater understanding of your system. Keep in mind though that a solution should always be tailored to the problem and using a shiny new technique is usually not the way to go. Always take a step back to consider what is actually needed.