undo Back to Articles Index

Advanced C# Concepts for use in Unity Part 3

By: John Kidd Jr on 12/05/2022 12:33 AM

Unity Events

As you write your game, you may get to a point where you want to perform an action in response to something else happening... but you don't want to waste the processing power checking for that condition every single frame. That's where Events come in!

Unity has it's own built-in event system, that works similar to the one built into the C# language... but with one huge benefit. You can see Unity Events in the designer, meaning you can create scripts that allow designers to make changes to functionality on their own!

Using them is pretty straightforward too, you need only include the correct namespace in your using section: UnityEngine.Events and then create an event object. Let' check out a quick example:

using UnityEngine;
using UnityEngine.Events;

public class EventTest : MonoBehaviour
{
    [SerializeField]
    private UnityEvent OnFourthFrame;

    private int frameCounter = 0;

    private void Update()
    {
        // increments frame counter by 1, then checks
        // if it's evenly divisible by 4
        if(++frameCounter % 4 == 0)
        {
            // calls all the functions stored in the event
            // from the designer
            OnFourthFrame.Invoke();
            // reset to 0 to prevent any overflow errors
            frameCounter = 0;
        }
    }
}

This will show up in the designer like so:

Unity Designer with Events

From here anyone can add functions form any object in the scene to the event. They will execute when the Invoke() function is called on the event.

You can also add or remove functions from the event in code! It works similar to how it works with delegates or standard C# events:

// add a function
OnFourthFrame += MyNewFunction;

// remove a function
OnFourthFrame -= MyNewFunction;

Functions use a void prototype with no arguments. Need to add arguments? You can also do that for up to 4 arguments by creating a custom class that inherits from UnityEvent like so:

using System;
using UnityEngine.Events;

[Serializable]
public class MyCustomEvent : UnityEvent<int, int, bool> { }

In the above example, we created a custom event that accepts 3 arguments... 2 integers and a boolean!

Object Pooling

Like the name suggests Object Pooling is creating a large cache of objects to pull from, rather than creating them on the fly each time. This can help save a lot of resources in larger games where you may need to instantiate dozens or hundreds of objects every second.

I know what you're thinking. What's the cool built in Unity way to do this? Well, there isn't one... so we'll have to make it on our own. Unlike a lot of the previous sections in these articles, this will be a walkthrough on creating an object pool.

First we need to create a Unity component to house the code in. For the sake of ease, we'll call it ObjectPoolManager. We'll then create a static object of our own class and call it instance, this way we always use the same Manager. If you aren't familiar, this is one way to do the singleton pattern. This will give us a mostly empty class so we'll add the 4 functions we are going to need as well and leave those empty for now, Awake(), Init(), CreateObject(), and GetObject(GameObject objectPrefab, Vector3 position, Quaternion rotation). What you have now should look like this:

using UnityEngine;

public class ObjectPoolManager : MonoBehaviour
{
    // singleton instance
    public static ObjectPoolManager instance;

    private void Awake()
    {
    }

    private void Init()
    {
    }

    private GameObject CreateObject()
    {
    }

    public GameObject GetObject(GameObject objectPrefab, Vector3 position, Quaternion rotation)
    {
    }
}

Next we'll need an actual pool of objects, an exposed variable to let the designer set the number of objects to start with, an exposed variable to store a base prefab to use for the pool, we'll also add the functionality to Awake to setup the singleton, and the code to initialize the manager by creating a ton of blank objects from a prefab. Remember to respect access modifiers, if no one needs to directly modify our structures, use private. In our case the only things that need to be accessed outside this class are the instance itself and the GetObject() function. Once done we'll have this:

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class ObjectPoolManager : MonoBehaviour
{
    public static ObjectPoolManager instance;
    private List<GameObject> _pool;
    [SerializeField] private int _startingPoolSize;
    [SerializeField] private GameObject _poolPrefab;

    private void Awake()
    {
        //check for an existing instance
        if (instance == null)
        {
            // if none exist, use this to create one
            instance = this;
            // set don’t destroy on load to preserve manager
            DontDestroyOnLoad(gameObject);
            // initialize the manager
            Init();
        }
        else
        {
            // if an instance exists, we don't need a new one
            Destroy(gameObject);
        }
    }

    private void Init()
    {
        // instantiate the pool
        _pool = new List<GameObject>();

        // simple loop to fill the pool with objects
        for (int i = 0; i < _startingPoolSize; i++)
        {
            // add to pool
            _pool.Add(CreateObject());
        }
    }

    private GameObject CreateObject()
    {
        // create an object
        var blankObject = Instantiate(_poolPrefab);
        // deactivate it immediately to prevent any resource usage from it
        blankObject.SetActive(false);

        return blankObject;
    }

    public GameObject GetObject(GameObject objectPrefab, Vector3 position, Quaternion rotation)
    {
    }
}

Now all that's left is deciding what we want to do once we hit the limit of our pool. We could return null and let the calling code figure it out, we could allow the pool to expand indefinitely, or we could let it expand to a certain limit. The choice is yours, but my example below will use an indefinite expansion. This means the last thing we need to write is the GetObject() function to return the first pooled object that isn't already active. Since we added the same parameters as the Instantiate() function we'll use those to do the same basic work that function would do if we were using it instead. This is what we should end up with (for the sake of brevity only the GetObject() function is in the code block below):

public GameObject GetObject(GameObject objectPrefab, Vector3 position, Quaternion rotation)
    {
        // get the first object that is NOT active
        // this is using LINQ, for more on that see the LINQ section
        // of this article
        var nextAvailableObject = _pool.Where(x => !x.activeInHierarchy).FirstOrDefault();

        // check if we got something back, if we didn't that means
        // we ran out of objects in our pool
        if (nextAvailableObject == null)
        {
            // if we ran out, create a new one and add to the pool
            nextAvailableObject = CreateObject();
            _pool.Add(nextAvailableObject);
        }

        // set the appropriate values for our object
        nextAvailableObject.name = objectPrefab.name;
        var objectTransform = nextAvailableObject.GetComponent<Transform>();
        objectTransform.position = position;
        objectTransform.rotation = rotation;

        // set the object to be active
        nextAvailableObject.SetActive(true);

        return nextAvailableObject;
    }

With this we should have a decent working example of a Object Pool. Now there is a lot that can be added to this code to make it work better, like checking to make sure the variables we need set in the designer are set before trying to execute any code. Another good add would be to set a default pool size so the designer only needed to make a change if they wanted something different.

Inversion of Control

Inversion of Control is a programming pattern that inverts the control of the program flow. What that means is that instead of us putting in a command to do something... and it just happens in that order and when the code is executed... it happens when it needs to based on decisions from the user (in this example the user can be anything other than the programmer). A simple thing we've already learned about that does this is Events.

Consider the MouseClicked() event from a regular desktop program. That code exists and can do something, but until someone actually clicks the mouse that code is not executed at all.

This can also be applied to the concept of creating classes to perform similar functions. Rather than creating a base class that contains a set of data our children might use, we would create interfaces with no dependencies such that we wouldn't need to know the exact implementation we have, just what we can do with it.

Think of a interface to control a character in your game, you might build out a Controller base class and have a child class called PlayerController that implements it. But now our child class relies on that parent class... so if we needed to further specialize our controller we're left in the awkward position of having to modify the base and inherit further down. Leading to fragile dependencies where one change can break half the game. The better way forward would be to create an interface, then if we need our implementation class to gain new functionality we could create a new interface that covers that functionality. Here's a quick example:

public interface IController
{
    public void ProcessInput();
}

public class PlayerController : IController
{
    public void ProcessInput()
    {
        // process input here
    }
}

We now have a controller and an interface. But what if we decide later we want to track score in the player controller class... but the AI doesn't need a score? We'd just add a new interface for that:

public interface IController
{
    public void ProcessInput();
}

public interface IScore
{
    public void AddToScore(int);
}

public class PlayerController : IController, IScore
{
    private int score = 0;

    public void ProcessInput()
    {
        // process input here
    }

    public void AddToScore(int amount)
    {
        score += amount;
    }
}

By creating our dependencies this way, we don't break functionality in other classes that inherit from the same parent!

LINQ

Where Link is the hero of Hyrule, LINQ is the hero of quickly searching through data structures in C#. In an earlier section I used LINQ before really saying what it does or how it's used. There are two major ways to use LINQ; using expression syntax which makes it look a lot like SQL or using function syntax which looks a lot more like home in C#. I'll show all my examples both ways so you can pick the one you like better.

Let's take a look at how LINQ can help sort through code starting with the example used earlier, Filtering Data.

Filtering Data

Imagine you have a list or other structure of data, and you need to get only the ones with a specific value, or even just the first one with that value. How would you do it? Without LINQ you'd have to create a loop, and check for that condition and store your result... let's use an example from the Object Pooling section from above where we were looking for inactive objects, that would give us something like this:

List<GameObject> inactiveObjects = new List<GameObject>();

foreach (var obj in _pool)
{
    if (!obj.activeInHierarchy)
    {
        // we found what we're looking for add to the list
        inactiveObjects.Add(obj);
    }
}

So doing this the "normal" way, we have to create a list to store the result, then loop through all the objects, perform a check on the field we're looking for, and add it to our list. Let's see how to do this same thing with LINQ:

// function syntax
var inactiveObjects = _pool.Where(x => !x.activeInHierarchy).ToList();
// expression syntax
var inactiveObjects = (from x in _pool where !x.activeInHierarchy select x).ToList();

Whoa! That simple check was reduced from 5-10 lines depending on your style to a 1 line statement!

If you come from a SQL background, you'll likely find yourself very at home using expression syntax which uses a ton of new keywords you likely haven't used before: from, where, and select. What do these keywords do? In simple terms from sets a indexing variable just like the beginning of a foreach loop does, this is the variable we use to perform any querying inside the expression, in operates exactly the same as in a foreach loop and tells us what collection to use, where is a condition verb... it tells us what to look for, and select tells us what to return from the query itself. The ToList() extension function on the end of both the function and expression syntax is a nicety in LINQ that returns our data in a List<T>.

For something small like this, function syntax looks easier. But if we added more conditions and restrictions, or if we wanted to join multiple collections together... it starts to get unwieldy to use. Using function syntax we just have to use the Where() extension function and pass in a delegate that tells it how to search the data. The delegated function MUST return a boolean result in a Where() clause.

Sorting Data

Now that we know the basics of LINQ, it'll be much faster to show how to sort data. Just like in SQL and other query languages we can sort by any field in our object and in either direction; ascending or descending. Let's look at a quick example of integers to see how this works:

var listToSort = new List<int>() { 6, 7, 1, 800, -2, 42, 3 };
// function syntax
var ascendingList = listToSort.OrderBy(x => x);
// result: -2, 1, 3, 6, 7, 42, 800
var descendingList = listToSort.OrderByDescending(x => x);
// result: 800, 42, 7, 6, 3, 1, -2

// expression syntax
var ascendingListExp = from x in listToSort orderby x select x;
// result: -2, 1, 3, 6, 7, 42, 800
var descendingListExp = from x in listToSort orderby x descending select x;
// result: 800, 42, 7, 6, 3, 1, -2

You can even do secondary sorts by adding a comma and the secondary express using expression syntax, or by using the ThenBy() or ThenByDescending() functions for function syntax like so:

// function syntax
var doubleSortedObjectsFnc = _pool.OrderByDescending(x => x.activeInHierarchy).ThenBy(x => x.name);

// expression syntax
var doubleSortedObjects = from x in _pool orderby x.activeInHierarchy descending, x.name select x;

Note that each condition can be either ascending or descending. Ascending is the default, but descending requires you to add the descending keyword after the field to activate.

Distinct / Contains / Union / Intersect / Except

Distinct

The distinct clause removes duplicates, plain and simple.

var listToSort = new List<int>() { 5, 4, 3, 7, 6, 5 };
// function syntax
var noDuplicates = listToSort.Distinct();
// result: 5, 4, 3, 7, 6

Note that the distinct clause has no expression equivalent and is always added as a function. This means if you used LINQ expressions to generate your sorted or filtered data, you'd enclose the expression in parenthesis and end it with .Distinct(). Like this:

// assume listToSort now contains a List of objects that contain
// 2 fields, a title and a customType
var type0Items = (from x in listToSort where customType == 0 select x).Distinct();

Contains

The Contains() extension function in LINQ gives us a boolean result letting us know if an object is in a list. Similar to Distinct() there is no expression equivalent to this function.

var listOfInt = new List<int>() { 5, 4, 3, 7, 6, 5 };

if(listOfInt.Contains(5))
{
    // do something because we found a 5
}

Except

The LINQ extension function Except() gives us a result collection that contains items from the first collection, that do not appear in the second collection. The first collection is the one we call the extension from, and the second is passed as a parameter, like this:

var firstListOfInt = new List<int>() { 4, 5, 6, 7, 1, 2, 90 };
var secondListOfInt = new List<int>() { 1, 2, 3, 99, 4, 5, 6 };

var resultingList = firstListOfInt.Except(secondListOfInt);
// result: 7, 90

Intersect

The LINQ extension function Intersect() is basically the opposite from Except(). It gives us items from the first collection that are in common with the items from the second collection:

var firstListOfInt = new List<int>() { 4, 5, 6, 7, 1, 2, 90 };
var secondListOfInt = new List<int>() { 1, 2, 3, 99, 4, 5, 6 };

var resultingList = firstListOfInt.Intersect(secondListOfInt);
// result: 4, 5, 6, 1, 2

Union

A LINQ Union joins two collections into one, keeping only the distinct objects. For base types like int and string this requires nothing special and can be implemented easily like so:

var firstListOfInt = new List<int>() { 4, 5, 6, 7, 1, 2, 90 };
var secondListOfInt = new List<int>() { 1, 2, 3, 99, 4, 5, 6 };

var resultingList = firstListOfInt.Union(secondListOfInt);
// result: 4, 5, 6, 7, 1, 2, 90, 3, 99

However, if you need to do a Union between collections of objects you've created, you'll need to implement the IEquitable<T> interface on the object, you will also need to override the Equals() and GetHashCode() functions built into the object base type. Here's a quick example of how to implement that:

public class EquatableTest : IEquatable<EquatableTest>
{
    public int CustomType { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }

    public bool Equals(EquatableTest other)
    {
        if (other == null)
        {
            return false;
        }

        return this.CustomType == other.CustomType && this.Title == other.Title && this.Description == other.Description;
    }

    public override bool Equals(object obj) => Equals(obj as EquatableTest);
    // we override GetHashCode() because by default all instances of an
    // object will have a unique hash code... even if all their data is
    // the same! This will fix that.
    public override int GetHashCode() => (CustomType, Title, Description).GetHashCode();
}

With that code in place, you can use a Union in exactly the same way as with other objects!

undo Back to Articles Index