Mastering Unity Scripting | Summary — Chapter 2: Debugging

Ayşe Oduncu
10 min readDec 15, 2021

--

Debugging is the process of finding, identifying and fixing bugs(errors or mistakes) in your code. In this chapter, we will see a broad spectrum of debugging techniques.

Compilation Errors and the Console

If you write code, save it and compile it (that usually compilation will be done automatically), the Console window will show you whether there are errors (and what are they), comments or warnings or not.

By clicking the error filter button on the top-right corner of the Console window, you can filter errors, comments and warnings.

Errors are generally listed in the order that the compiler detected them, from top to bottom in the Console window. You should debug in that order because sometimes, some of the errors cause other errors and fixing them will solve the other problems as well. You can double click on the error and it will open the MonoDevelop and take you to the place where the error occurred.

There may be some warnings in your code as well (for example a warning because of not using a variable anywhere) and the best practice will be to solve these warnings along with the errors.

You can use Debug.Log(“custom message”) to debug your code. All Unity objects also have a toString() function that allows their internal members to be printed to a human-readable string.

The main limitations of Debug.Log as a debugging technique relate to code cleanliness and program complexity. First, the Debug.Log statements require you to explicitly add code to your source files. When you’re done debugging, you’ll need to either remove the Debug.Log statements manually or leave them there, which is wasteful and results in confusion, especially if you have to add the additional Debug.Log statements in many other places. Second, though Debug.Log is useful to target specific problems or monitor specific variables over time, it’s ultimately awkward to get a higher-level picture of your code and its execution to trace errors whose presence you detect but whose location is entirely unknown. When your game is ready to build and ship, remember to remove or comment away any Debug.Log statements for extra cleanliness.

Debug.Log works best when an error or problem can be traced to a prime suspect object, and you want to observe or monitor its values to see how they change or update, especially during events such as OnStart.

Overriding the toString() Method

In C#, every class inherits the toString method by default, so using inheritance and polymorphism, you can override it to produce a more readable and accurate debug string.

Note: You can use String.Format to build a complete string.

Overriding the toString() method and String.Format example from the book:

A global define can also be used for the sake of simplicity in debugging. It is a special preprocessor flag that you can enable or disable to conditionally compile or exclude blocks of code. By setting true, the code block will be executed and if false, the compiler will ignore it as it does for comments. To set the global define, navigate to Edit | Project Settings | Player from the application menu. Then, enter the defined name in the Scripting Define Symbols field, making sure that you press the Enter key after entering the name to confirm the change. Example from the book:

Visual Debugging

Sometimes, visual debugging is a better choice than Console debugging. To achieve visual debugging, we can use Gizmos.

Note: If you’ve added a native Unity object and don’t see a gizmo in the Scene viewport, then be sure to check the Gizmo panel that is accessible from the Scene toolbar via the Gizmo button. Enable all the gizmos you want to see and adjust the Size slider that increases or decreases the gizmo size.

More information can be found online here.

Example from the book:

Error Logging

We need logfiles (that are human-readable text files that are generated on the local computer by the game at runtime) to record and find errors if they occur. There are many ways to implement it, and one way is by using the Application class to receive exception notifications by way of delegates.

Note: Delegates are variables that can be assigned by functions instead of basic types like int or string.

Example code from the book about error logging:

Editor Debugging

We can enable debug mode from Object inspector by clicking on the context menu icon in the top-right corner of the inspector and selecting debug. You can play the game and also follow the inspector panel and change variables at the same time for debugging. Note that these changes won’t be saved.

Note: To save the changes of a component of a game object, you can right-click on the component and select copy component and paste them when you stop the game.

We can also enable the Stats panel from the Game panel to see the stats of the game.

Using the Profiler

The profiler panel is in Unity Pro and can be used for mainly detecting performance issue bugs. More information can be found here.

Debugging with MonoDevelop — Getting Started

We can debug without changing our code. To start with, we’ll see breakpoints. Breakpoints are simply the places where you want the game to stop and give control of the flow in your hands.

To set the breakpoint, you can position your mouse cursor on the desired line of code and right-click on the left-hand grey margin, and choose New Breakpoint. Otherwise, you can use the MonoDevelop application menu to choose the New Breakpoint option in Run, or you can simply press the F9 key.

When you set the breakpoint, the line will be red highlighted. To make it work, attach MonoDevelop to the running Unity process. To achieve this, make sure that the Unity Editor is running alongside MonoDevelop and then choose the Attach to Process option from Run from the MonoDevelop application menu. The Attach to Process dialogue appears, and the Unity Editor should be listed as Process Name to which MonoDevelop can be attached. The Debugger drop-down list in the bottom-left corner of the window should be specified as Unity Debugger. Select the Unity Editor option and then choose the Attach button. When MonoDevelop is attached to Unity as a process, two new bottom-aligned panes will dock themselves to the MonoDevelop interface, and these include the Watch window and the Immediate window, which we’ll see. These windows offer additional debugging information and views when your game runs in the Unity Editor.

You can run your game now. When the breakpoint is reached, Unity will freeze and open up the MonoDevelop. During this mode, you cannot use the Unity Editor, and you cannot switch between viewports or even edit settings inside the Object Inspector as you can with in-editor debugging. MonoDevelop is waiting exclusively for your input to resume execution. We will consider some useful debugging tools that you can use in the break mode.

Debugging with MonoDevelop — the Watch Window

A Watch window allows you to view the value of a variable that’s active in memory in the current step, and this includes both local and global variables. You can hover your mouse on variables to add them. You can also open up the watch window and choose to add a watch from the context menu to watch expressions or variables.

If you only want to see the local variables, you can use the Locals window that brings you all of the local variables by default.

If you don’t see these windows, you can find them from MonoDevelop under Debug Windows option in View.

You can read and also change the values of the variables in the Watch and Locals windows.

Debugging with MonoDevelop — Continue and Stepping

To go out from debugging in MonoDevelop and continue from the game window, you can press the F5 key or play button from the MonoDevelop toolbar. Otherwise, you can select the Continue Debugging option in Run from the MonoDevelop.

There are three main kinds of steps in debugging: step over, step into, and step out. Step over instructs the debugger to move to the next line of code and then to pause again, awaiting your inspection as though the next line were a new breakpoint. If an external function call is encountered in the next line, the debugger would invoke the function as usual and then step to the next line without stepping into the function. In this way, the function is “stepped over”. The function still happens, but it happens in the continue mode, and the next step or breakpoint is set in the next line after the function. To step over, press F10, choose the Step Over command in Run from the application menu or press the Step Over button in the MonoDevelop toolbar. If an external function call is encountered, the Step Into (F11) command allows debugging to enter this function. This effectively sets the next breakpoint in the first line of the entered function that allows debugging to resume in the next step. This can be useful if you need to observe how many functions are working together. If at any point, you want to exit the entered function by moving it forward in the continue mode, you could use the Step Out (Shift + F11) command, and the execution would resume in the next line in the outer function.

Debugging with MonoDevelop — Call Stack

During execution, functions can invoke other functions, and these functions can go on to invoke yet more functions in an intricate chain of functions within functions. This means that when setting breakpoints inside functions, you can never know how the function was invoked initially when it’s actually called at runtime. The breakpoint tells you that program execution has reached the specified line, but it doesn’t tell you how execution arrived there in the first place. Sometimes, it might be easy to deduce, but at other times it can be much harder, especially when functions are invoked within loops, conditionals, and nested loops and conditionals. We can’t know the route because of the many function calling. So, we can use the Call Stack window. This window, displayed by default in the bottom-right corner of the MonoDevelop interface, lists all the function calls that were made to reach the active function for the current step that leads back to the first or initial function call. It gives us a breadcrumb trail of function names that leads from the active function to the first or initial function. Thus, Call Stack lists function names upside down, the active or most recent function being at the top of the stack that leads downwards to the earliest or first function at the bottom of the stack. You can also access the functions at their locations to assess the variables in their scope.

Debugging with MonoDevelop — the Immediate Window

The Immediate window acts like the Console window. You can get and set the values for active variables and also perform additional operations. It is especially useful to test code, write alternative scenarios and see how they evaluate. You can find more about the Immediate window here.

Debugging with MonoDevelop — Conditional Breakpoints

You can set breakpoint conditions that specify the states that must be true for the breakpoint to take effect. To set a breakpoint condition, right-click on the breakpoint and choose Breakpoint Properties from the context menu. Selecting Breakpoint Properties will display the Breakpoint Properties dialogue where conditions for the breakpoint can be specified. In the Condition section, choose the Break when the condition is true option and then use the Condition expression field to specify the condition that determines the breakpoint. For loop conditions, the expression i>5 will trigger the breakpoint when the loop iterator has exceeded 5. Of course, the variable i should be substituted for your own variable names.

Debugging with MonoDevelop — Tracepoints

Tracepoints work like breakpoints, in that they mark lines within your source file. They don’t change the code itself, but (unlike breakpoints) they don’t pause the program execution when encountered by the debugger. Instead, they perform a specified instruction automatically. Typically, they print a debug statement to the Application Output window in MonoDevelop, though not to Unity’s Console. To set a breakpoint in a line of code, position your cursor on the line and select Add Tracepoint in Run from the application menu (or press Ctrl + Shift + F9). On selecting the Add Tracepoint option, MonoDevelop will show the Add Tracepoint dialogue. The Trace Text field allows you to input the text to be printed to the Application Output window when the tracepoint is encountered at runtime. You can also insert the curly braces opening and closing symbols to define the regions in the string where expressions should be evaluated. This lets you print the values of variables into the debug string, such as “Loop counter is {i}”. After clicking on OK, the tracepoint is added to the selected line. Inside MonoDevelop, the line will be marked with a diamond shape as opposed to a circle; this diamond shape indicates a breakpoint.

When the tracepoint is encountered, the application will not pause or enter the break mode as it would with breakpoints. Instead, the tracepoint will automatically output its printed statements to MonoDevelop’s Application Output window, without causing a pause. By default, this window is docked at the bottom of the MonoDevelop interface.

Tracepoints are an effective and helpful alternative to using the Debug.Log statements inside Unity, and you don’t need to amend your code in any way to use them as you do with Debug.Log. Unfortunately, they don’t print directly to Unity’s Console. Instead, they appear inside the Application Output window in MonoDevelop. However, as long as you recognize this, working with tracepoints can be a powerful and useful method to find and remove bugs.

End of Chapter 2: Debugging

← Chapter 1: Unity C# Refresher

--

--

Ayşe Oduncu

I am currently a computer engineering student at Muğla Sıtkı Koçman University. I love many things about game development, so, I will share stories about them.