Advanced C# Concepts for use in Unity
By: John Kidd Jr on 11/20/2022 05:57 PM
Coroutines in Unity
What is a coroutine?
A coroutine a way to initiate Cooperative Multitasking in your code. Essentially allowing you to have multiple functions executing portions of their code and passing execution off between each other.
Sound complicated? It's not quite as bad as it sounds. It really just means that instead of completely running all the code in a code block it can stop mid-way and let something else get a chance at running code for a bit before it picks up where it left off.
What does this look like in code?
IEnumerable<string> DoSomething()
{
Console.WriteLine("Function called for the first time...");
//return exection back to the loop
yield return "Do Stuff";
Console.WriteLine("Function called for the second time...");
//return execution back again
yield return "Do More Stuff";
}
//store the collection into a variable we can iterate through
IEnumerable<string> stringsToWriteToConsole = DoSomething();
foreach (string stringToWrite in stringsToWriteToConsole)
{
//write out the next string in the collection
Console.WriteLine(stringToWrite);
}
In this very basic example, our function returns a collection of strings to print to the console, which is does. However, because we are using the yield
keyword to return each of our strings instead of building and returning a collection as a whole, then execution passes back and forth as we call the next item in the collection. As a result, the output is:
Function called for the first time...
Do stuff
Function called for the second time...
Do more stuff
How does this work in Unity?
Unity has some nice built-in functions to run Coroutines with that make them very easy to use. There is a function that is part of MonoBehaviour
that can be called from any class that inherits from it called StartCoroutine()
. This function lets us pass in the name of a function we want to run, and will run it starting on the next execution frame. If we want a coroutine to run until we say otherwise, we just have to include an infinite loop. Let's see an example:
using System.Collections;
using UnityEngine;
public class FillUpLog : MonoBehaviour
{
void Start()
{
//this starts a coroutine using the function provided
StartCoroutine(FillUpDebugLog());
}
private IEnumerator FillUpDebugLog()
{
//just a warning to anyone watching the logs...
Debug.Log("We are going to start filling it up now...");
while (true)
{
//send a message to the log
Debug.Log("Is if full yet?");
//return execution back until next frame
yield return null;
}
}
}
And if we run our game and look at the log... it did exactly what we expected. It filled the log with a ton of messages:
We can stop a coroutine at any time by executing StopAllCoroutines()
on the GameObject it was attached to or StopCoroutine()
if you can pass in the same parameter(s) you started it with.
What can this be used for in Unity?
Coroutines are very powerful. You can use them if you need to run a piece of code for a short time on certain conditions (like only processing an AI when the enemy has actually spawned). It's also powerful enough that you could completely replace the Update()
method completely!
Delegates
What is a Delegate?
A delegate is a variable that holds a reference to a function with a particular parameter list and return type. What that means is that you can hold a function in a variable, and treat it like you would any other variable.
You declare a function prototype for the delegate using the delegate
keyword, then you create a variable of your new delegate, and then assign the function you want that delegate to execute to it. Remember, the function you assign must match the prototype!
public class Program
{
//the total number of bullets added
public static int totalBullets;
//function to be added to the delegate
public static int AddBullets(int numberOfBullets)
{
//add to the total
totalBullets += numberOfBullets;
//return the new total
return totalBullets;
}
//declare our delegate prototype
public delegate int AddProjectiles(int x);
//declare the variable to hold a function
public static AddProjectiles projectileAdder;
static void Main(string[] args)
{
//assign the function to the delegate
projectileAdder = AddBullets;
//we can call the delegated function like so:
projectileAdder(5);
//write out the total to the console
Console.WriteLine(totalBullets);
}
}
The output of this short program is 5
just like we'd expect!
When would I use them?
Delegates are very useful when you need a function to be executed elsewhere or as part of a sequence. Think if you had to call a function in another class, but needed to run a specific function halfway through that call. You could pass your delegate as a parameter to that function and let that function call it at the right time!
Another good use for them is if you want to call the same function in a loop constantly, and want to change that function from time to time. By using a delegate, you can just assign a new function to the delegate and now the function being executed is different!
Multicasting Delegates
With how useful delegates can be, wouldn't they be more useful if you could execute multiple functions with the same delegate? Well you can!
Adding multiple functions into a delegate is as easy as using the +
operator like you would with another other addition. Keep in mind that every function you store must match the original delegate prototype exactly!
Here's a quick example that builds on the previous delegate example:
public class Program
{
//the total number of projectiles counted
public static int totalProjectiles;
//the total bullets counted
public static int totalBullets;
//the total arrows counted
public static int totalArrows;
//function to be added to the delegate
public static int AddBullets(int numberOfBullets)
{
//add to the total number of projectiles
totalProjectiles += numberOfBullets;
//add to the total bullets
totalBullets += numberOfBullets;
//return the total bullets
return totalBullets;
}
public static int AddArrows(int numberOfArrows)
{
//add to the total number of projectiles
totalProjectiles += numberOfArrows;
//add to the total arrows
totalArrows += numberOfArrows;
//return the total arrows
return totalArrows;
}
//declare our delegate prototype
public delegate int AddProjectiles(int x);
//declare the variable to hold a function
public static AddProjectiles projectileAdder;
static void Main(string[] args)
{
//assign the functions to the delegate
projectileAdder = AddBullets;
projectileAdder += AddArrows;
//we can call the delegated function like so:
projectileAdder(5);
//print the results to the console
Console.WriteLine($"Total Bullets: {totalBullets}");
Console.WriteLine($"Total Arrows: {totalArrows}");
Console.WriteLine($"Total Projectiles: {totalProjectiles}");
}
}
Here's our output:
Total Bullets: 5
Total Arrows: 5
Total Projectiles: 10
Keep in mind that while both the function in the example perform the same function, that doesn't have to be true for you. The only thing that needs to match is the function prototype!
Lambda Expressions
Lambda expressions are a sort of mini function. If you've used other programming languages before, you may have heard them referred to as "arrow functions" and in C# we still use the =>
operator to designate them.
Lambdas can even team up with delegates to save you having to write out a short function just to store it in the delegate. Example:
public class Program
{
//the total number of projectiles counted
public static int totalProjectiles;
//declare our delegate prototype
public delegate int AddProjectiles(int x);
//declare the variable to hold a function
public static AddProjectiles projectileAdder;
static void Main(string[] args)
{
//assign a lambda function to the delegate
projectileAdder = x => totalProjectiles += x;
//we can call the delegated function like so:
projectileAdder(5);
//print the result to the console
Console.WriteLine($"Total Projectiles: {totalProjectiles}");
}
}
Note that while x
is used in the lambda expression x => totalProjectiles += x
it can be anything and still work. For instance, without changing any other code, this would work:
projectileAdder = firstParameter => totalProjectiles += firstParameter
The portion before the =>
is just the definition of the variables to be used inside the function body, the function body is the portion after the =>
. The function also has an implied return statement... it will return the result of what happens after the =>
.
You can add more than a single line of code to your function body, and optionally have more than one parameter passed into that function body... just like any other function! Here's a quick example:
projectileAdder = (arrowsToAdd, bulletsToAdd) =>
{
//add both the arrows and bullets to the total
totalProjectiles += (arrowsToAdd + bulletsToAdd);
//add the arrows to the arrow total
totalArrows += arrowsToAdd;
//add the bullets to the bullet total
totalBullets += bulletsToAdd;
//return the total for all projectiles
return totalProjectiles;
};
Notice that the parameter list has to be enclosed inside ()
when there is more than one parameter (it's implied with just one), and that we now have to use the return
keyword to return at the end. Also keep in mind that this example uses a different prototype than the one we defined before, so we would need to adjust our delegate definition to accept this new prototype like so:
public delegate int AddProjectiles(int x, int y);
Generics
Generics are also known as template functions in other languages like C++ and are very helpful in increasing your code reusability.
A generic function defines it's generic using the <>
on the function name. You can then use that generic type as a parameter input, or output as you need. Here's a quick example:
public class Program
{
//generic function, we can pass in any type
static void PrintValue<T>(T value)
{
//get the ToString() value and print to the console
Console.WriteLine(value?.ToString());
}
static void Main(string[] args)
{
//test with multiple types
PrintValue<int>(42);
PrintValue<string>("hello");
PrintValue<DateTime>(DateTime.Now);
}
}
The output of this is as you might expect:
42
hello
11/20/2022 11:26:48 AM
You can also define a generic class in much the same way:
public class Program
{
static void Main(string[] args)
{
//create an instance of the generic class with int as the type
var intT = new TestClass<int>();
//set the value to 16
intT.Value = 16;
//ask the class to write that value to the console
intT.WriteValueToConsole();
}
}
//generic class definition, allows us to use T anywhere inside
public class TestClass<T>
{
//public parameter
public T Value { get; set; }
public void WriteValueToConsole()
{
//writes the value to the console
Console.WriteLine(Value?.ToString());
}
}
Upon executing this program, you get the expected outcome:
16
Multithreading
Multithreading is a wonderfully useful concept for modern computing. It's extremely simple to setup and get working but can be painful when bugs crop up.
Basics
Creating a thread is simple, you need only include the correct using statement and then instantiate your thread passing in the function you want it to run. Here's a quick example:
using System.Threading;
public class Program
{
private static void DoStuffOnAnotherThread()
{
Console.WriteLine("This is not happening on the primary execution thread");
}
static void Main(string[] args)
{
//create a new thread, pass in the function above to it
Thread newThread = new Thread(DoStuffOnAnotherThread);
//start the thread execution
newThread.Start();
//write to the console so we know when each thread executes
Console.WriteLine("This is happening on the primary thread");
}
}
The output may not be what you're expecting...
This is happening on the primary thread
This is not happening on the primary execution thread
What happened? Didn't we start the thread before writing out to the console?
We did! But the primary thread finished first still. Behavior like that is great when our thread doesn't rely on anything being executed in our primary thread but can cause all kinds of problems as we'll get into a little later.
Thread Actions
There are a few important actions we can make our thread take as well.
Sleep
We can temporarily stop the execution of any thread (including our main thread) by calling the static function Thread.Sleep()
which takes a single integer parameter for the number of milliseconds to sleep for.
Join
The instance function Join
tells the program to wait for the thread in question to finish before continuing... thereby joining the thread in question with our main thread at that point. Quick example:
using System.Threading;
public class Program
{
private static void DoStuffOnAnotherThread()
{
Console.WriteLine("This is not happening on the primary execution thread");
}
static void Main(string[] args)
{
//create a new thread
Thread newThread = new Thread(DoStuffOnAnotherThread);
//start the new thread
newThread.Start();
//wait for the new thread to finish and join us back on the main thread
newThread.Join();
Console.WriteLine("This is happening on the primary thread");
}
}
You'll notice this is almost the exact same example from the basics section, but we added the line newThread.Join();
telling it to let that thread run before continuing. This changes our output to:
This is not happening on the primary execution thread
This is happening on the primary thread
Which as you can see causes the thread to finish first, switching the order of the statements in the output!
Yield and Sleep(0)
Using the Thread.Yield()
static method, you can cause the thread calling the function to give up its remaining time slice to the Operating System, which allows the Operating System to schedule another process for that time slice and rescheduling the current thread's execution for the next available for its priority. Note that this functionality is limited to within the same processor, no matter what other processors are doing.
Using Thread.Sleep(0)
does almost the same thing but is not limited to working within the same processor's resources.
Problems
For all the good multithreading can do, it does have its share of problems. The most notable of which is that it can cause race conditions.
A race condition is when one thread is accessing and changing data that another thread is using, which can cause bugs if the wrong thread modifies the data first.
How do we fix it? We can lock down sections of code to prevent a thread from hitting them while any other thread is currently executing it. We do that by creating an arbitrary System.Object instance and using the lock
keyword on it. Let's see this in action:
using System.Threading;
public class Program
{
//create a lock object, all it does it lock out other threads
private static object _lock = new object();
//create a variable to modify with 2 threads at the same time
private static string _stringToPrint;
private static void UpdateStringToPrint(string newString)
{
//set a lock so only 1 thread can use this at a time
lock (_lock)
{
//assign the value to the variable
_stringToPrint = newString;
}
}
private static void DoStuffOnAnotherThread()
{
//call the update function
UpdateStringToPrint("newThread updated the String");
Console.WriteLine(_stringToPrint);
}
static void Main(string[] args)
{
//create a new thread
Thread newThread = new Thread(DoStuffOnAnotherThread);
//start the thread
newThread.Start();
//update and print the string
UpdateStringToPrint("Main Thread updated the String");
Console.WriteLine(_stringToPrint);
}
}
The console output is mostly irrelevant with this example, the important thing here is that it will still print out both statements, and is considered "Thread Safe" which is important when dealing with multithreading.
What does "Thread Safe" mean?
Thread Safe means that the code being executed by the threads in a program cannot fall victim to race conditions, and can therefore be executed safely.
What about Unity?
The Unity game engine itself runs multithreaded, but script execution by default happens on a single thread. Functions in the Unity API are not considered Thread Safe, and you should use caution when calling anything in the Unity API from any thread other than the main execution thread.
Conclusion
This was a lot of material to cover, I know. These more advanced C# programming topics will help quite a bit with making better and more efficient games with Unity. Not everything presented here will immediately make sense to use in Unity, and that's okay, just keep it in mind and you'll eventually run into a problem that can be better solved or more efficiently solved using one of these techniques!
Further Reading & References
Coroutines
Delegates
Multicasting Delegates
Lambda Expressions
- Microsoft C# Guide on => Operator
- Microsoft C# Guide on Lambda Expressions
- Geeks for Geeks: Lambda Expressions in C#
Generics
- Microsoft C# Guide on Generics
- Microsoft C# Guide on Generic Methods
- Microsoft C# Guide on Generic Classes
- Geeks for Geeks: Generics - Introduction