Other Topics

Comments

Comments additional notes which do not affect the execution of the program in any way and are very useful for explaining things about your code and your reasoning. There are 3 types of comments in C#:

Name Example
Single Line
// Everything on this line after the double slash is part of the comment.
Block
/* Everything from the "/*"
   on any number of lines
   is part of the comment
   until it reaches the next */
Documentation
/// <summary>
/// Tripple slash comments
/// can also take up as many lines as you want.
/// <para></para>
/// They must be placed above a particular member (class, method, field, etc.)
/// <para></para>
/// They can be displayed by an IDE to give you information about that member.
/// So you do not need to always go to other scripts to find out their details.
/// <para></para>
/// They use XML tags (often only "summary") for various formatting purposes.
/// </summary>

Click here for an example:

The Strings class in Animancer has the following comment:

/// <summary>Various string constants used throughout Animancer.</summary>
public static class Strings
{
   ...
}

This allows Visual Studio to display that text as a tooltip when you hover over any reference to that class:

It also allows Wyam (the tool used to generate this documentation website) to show that comment in the API Documentation for the Strings class.

In Visual Studio, typing /// before any member (class, method, field, etc.) will automatically insert a tripple slash comment for it.

Exceptions

An Exception is an error that occurs at runtime when an operation cannot be completed properly. For example:

  • If you try to set the name of a GameObject but do not actually have a reference to one (your reference is null) then it will throw a NullReferenceException.
  • If you have an array with a length of 4 and you try to access the 5th element, it will throw an IndexOutOfRangeException.
  • If you try to load a file that does not exist or write to a file that the operating system has marked as read-only, it will throw an IOException.

You can respond to exceptions using try/catch/finally blocks like so:

try
{
    // Do things that might throw an exception.
    Method1();
    Method2();
    Method3();
}
catch (ExceptionType exception)
{
    // Log the exception if you want to.
    // Or sometimes you might even just ignore it and continue on.
    Debug.LogException(exception);
}
finally
{
    // Do any cleanup that needs to always happen regardless of whether an exception was thrown.
}
  • If Method2 throws an exception, then it will be passed into the catch block and Method3 will not get called.
  • If you do not catch an exception, Unity will catch it and log it in the Console window.
  • You do not need to have both a catch and finally block if you do not need them both.
  • You can have multiple catch blocks for different exception types and you can use the base System.Exception to catch everything.
  • In Visual Studio, if you type try then press Tab twice it will insert a try/catch block.

Debugging

During development there will inevitably be times when you encounter behaviour you do not want, so you need to be able to track down the cause of that behaviour before you can fix it. There are two main ways of tracking down bugs which each have advantages in different situations:

Debugger Logging
The Unity Manual explains how you can use an IDE like Visual Studio as a debugger, allowing you to set "breakpoints" in your script which will pause the execution of the program to step through your code line by line and allow you to view the value of each variable. The Debug class contains a Log method (and several other variations) which you can use to show a message in the Console window. Logged messages are also saved to a Log File in case you need to debug issues in a runtime build or issues that cause the Unity Editor to crash.
You can set a breakpoint at any time. You need to edit your script to call the Log method and let Unity recompile it, meaning you probably also need to exit Play Mode and restart it again.
You can easily view any variables on the current object without needing to decide what you want to see beforehand. You need to decide what to actually log and all messages are kept in a list. This can be bad if you have too many objects all filling up the Console with messages that make it hard to find what you want, but it can also be good if you need to compare the contents of messages from different objects and at different times without knowing what you are looking for beforehand.
Pauses the game. This makes it very difficult or even impossible to use a debugger for situations like input related issues where you need to be able to play the game properly and then review what actually happened afterwards or network multiplayer where pausing one computer would cause the others to think it disconnected. Does not pause the game unless you intentionally call Debug.Break.

Also note that Debug.Log has an optional second parameter to specify the context of the message so that if you click that message in the Console window it will highlight which object in the Hierarchy the message came from.

Memory Management

Every variable needs to be allocated some space in the computer's RAM and then de-allocated when it is no longer needed so that space can be reused for other variables. C# automates this process so you do not generally need to worry about it except to note that it has a performance cost which can be very significant in some cases. The Unity Manual has a more extensive page about Automatic Memory Management, but this section goes over the main points.

There are two general types of memory: the Stack and the Heap.

Stack

When a Method declares a Value Type local variable, it will need to allocate a specific amount of memory for it. For example, an int takes 4 bytes while the size of a struct is determined by the types of its fields. Since that variable will only exist for the duration of the method, managing it is simple: you allocate memory for it when the method is called and de-allocate that memory when the method ends. If the method calls another method, the other method can allocate any of its local variables using the next available memory region. This type of memory is called the Stack because each new variable is allocated into the next available section of memory as if you had a stack of boxes. You can add a box to the top or you can remove the top box, but nothing can remove a box from the middle of the stack. It does not matter what size a new box is because it can always simply go on top of the previous one. This simplicity makes it very fast to execute.

Heap

When you create an instance of a Reference Type, its lifetime is not so clearly defined. You might do nothing with it so it could be de-allocated when the method ends, but more than likely you will give something else a reference to it so it might last until an enemy is killed or the level is unloaded or even until the whole application is closed. This makes the memory management process far more complicated:

  • New objects cannot simply be allocated in the next available space. If you allocate a small object and a large one then de-allocate the small one, a new small object can be allocated in that same space but a new large object cannot.
  • It is no longer clear when exactly an object can be deallocated so it needs to check whether or not anything else is referencing that object. This process is called Garbage Collection and is the primary performance concern of this topic.

Unlike local variables, a Value Type used as a field in a Reference Type will be part of the memory allocated for that type and will therefore be on the heap.

Garbage Collection

The Garbage Collector is an automated runtime system that periodically checks through everything in the Heap. Any object that has nothing left referencing it can no longer be used by anything so it is considered to be garbage which can be collected (de-allocated so that the memory it occupied can be reused in the future). It also needs to account for the possibility that two objects might reference each other even though nothing else has a reference to either of them, in which case they are still garbage. This process is quite complex and can take a significant amount of time to execute, meaning that it can cause the frame in which it runs to take much longer than other frames, which users might notice as a stutter or lag in the game.

The main way you can avoid this performance issue is by avoiding the creation of garbage. Note that the performance cost is often insignificant and many people advise against trying to optimise things like this unless you actually find a problem occuring because optimisations generally involve adding more complexity to your program and therefore create more potential for bugs to occur.

  • Object Pooling is a common strategy for reducing garbage by adding objects to a collection when they are no longer needed so that they are not garbage and can later be reused when another object is needed instead of allocating a new one. Animancer uses a very simple ObjectPool system for some tasks.
  • strings are a common source of garbage. Using String.Concat or a StringBuilder can help you build strings more efficiently (this section describes their differences)
  • Delegates are another common source of garbage. The Animancer Events page has a section which explains how to avoid creating Garbage when using them.