13

Exploring Generics, Delegates, and Beyond

The more time you spend programming, the more you start thinking about systems. Structuring how classes and objects interact, communicate, and exchange data are all examples of systems we’ve worked with so far; the question now is how to make them safer and more efficient.

Since this will be the last practical chapter of the book, we’ll be going over examples of generic programming concepts, delegation, event creation, and error handling. Each of these topics is a large area of study in its own right, so take what you learn here and expand on it in your projects. After we complete our practical coding, we’ll finish up with a brief overview of design patterns and how they’ll play a part in your programming journey going forward.

We’ll cover the following topics in this chapter:

  • Generic programming
  • Using delegates
  • Creating events and subscriptions
  • Throwing and handling errors
  • Understanding design patterns

Introducing generics

All of our code so far has been very specific in terms of defining and using types. However, there will be cases where you need a class or method to treat its entities in the same way, regardless of its type, while still being type-safe. Generic programming allows us to create reusable classes, methods, and variables using a placeholder, rather than a concrete type.

When a generic class instance is created at compile time or a method is used, a concrete type will be assigned, but the code itself treats it as a generic type. Being able to write generic code is a huge benefit when you need to work with different object types in the same way, for example, custom collection types that need to be able to perform the same operations on elements regardless of type, or classes that need the same underlying functionality.

We’ve already seen this in action with the List type, which is a generic type. We can access all its addition, removal, and modification functions regardless of whether it’s storing integers, strings, or individual characters.

While you might be asking yourself why we don’t just subclass or use interfaces, you’ll see in our examples that generics help us in a different way.

Generic classes

Creating a generic class works the same as creating a non-generic class but with one important difference: its generic type parameter. Let’s take a look at an example of a generic collection class we might want to create to get a clearer picture of how this works:

public class SomeGenericCollection<T> {}

We’ve declared a generic collection class named SomeGenericCollection and specified that its type parameter will be named T. Now, T will stand in for the element type that the generic list will store and can be used inside the generic class just like any other type.

Whenever we create an instance of SomeGenericCollection, we need to specify the type of values it can store:

SomeGenericCollection<int> highScores = new SomeGenericCollection<int>();

In this case, highScores stores integer values and T stands in for the int type, but the SomeGenericCollection class will treat any element type the same.

You have complete control over naming a generic type parameter, but the industry standard in many programming languages is a capital T. If you are going to name your type parameters differently, consider starting the name with a capital T for consistency and readability.

Let’s create a more game-focused example next with a generic Shop class to store some fictional inventory items with the following steps:

  1. Create a new C# script in the Scripts folder, name it Shop, and update its code to the following:
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
     
    // 1
    public class Shop<T>
    {
        // 2
        public List<T> inventory = new List<T>();
    }
    
  2. Create a new instance of Shop in GameBehavior:
    public class GameBehavior : MonoBehaviour, IManager
    {
        // ... No other changes needed ...
          
        public void Initialize()
        {
            // 3
            var itemShop = new Shop<string>();
            // 4
            Debug.Log("Items for sale: " + itemShop.inventory.Count);
        }
    }
    

Let’s break down the code:

  1. Declares a new generic class named IShop with a T type parameter
  2. Adds an inventory List<T> of type T to store whatever item types we initialize the generic class with
  3. Creates a new instance of Shop<string> in GameBehavior and specifies string values as the generic type
  4. Prints out a debug message with the inventory count:

Figure 13.1: Console output from a generic class

To provide a complete view of the Unity editor, all our screenshots are taken in full-screen mode. For color versions of all book images, use the link below: https://packt.link/7yy5V.

Nothing new has happened here yet in terms of functionality, but Visual Studio now recognizes Shop as a generic class because of its generic type parameter, T. This sets us up to include additional generic operations like adding inventory items or finding how many of each item is available.

It’s worth noting here that generics aren’t supported by the Unity Serializer by default. If you want to serialize generic classes, as we did with custom classes in the last chapter, you need to add the Serializable attribute to the top of the class, as we did with our Weapon class. You can find more information at: https://docs.unity3d.com/ScriptReference/SerializeReference.html.

Generic methods

A standalone generic method can have a placeholder type parameter, just like a generic class, which allows it to be included inside either a generic or non-generic class as needed:

public void GenericMethod<T>(T genericParameter) {}

The T type can be used inside the method body and defined when the method is called:

GenericMethod<string>("Hello World!");

If you want to declare a generic method inside a generic class, you don’t need to specify a new T type:

public class SomeGenericCollection<T> 
{
    public void NonGenericMethod(T genericParameter) {}
}

When you call a non-generic method that uses a generic type parameter, there’s no issue because the generic class has already taken care of assigning a concrete type:

SomeGenericCollection<int> highScores = new SomeGenericCollection
<int> ();
highScores.NonGenericMethod(35);

Generic methods can be overloaded and marked as static, just like non-generic methods. If you want the specific syntax for those situations, check out the following link: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-methods.

Your next task is to create a method that adds new generic items to the inventory and use it in the GameBehavior script.

Since we already have a generic class with a defined type parameter, let’s add a non-generic method to see them working together:

  1. Open up Shop.cs and update the code as follows:
    public class Shop<T>
    {
        public List<T> inventory = new List<T>();
        // 1
        public void AddItem(T newItem)
        {
            inventory.Add(newItem);
        }
    }
    
  2. Now open GameBehavior.cs and add an item to itemShop:
    public class GameBehavior : MonoBehaviour, IManager
    {
        // ... No other changes needed ...
         
         public void Initialize()
        {
            var itemShop = new Shop<string>();
            // 2
            itemShop.AddItem("Potion");
            itemShop.AddItem("Antidote");
            Debug.Log("Items for sale: " + itemShop.inventory.Count);
        }
    }
    

Let’s break down the code:

  1. Declares a method for adding newItems of type T to the inventory
  2. Adds two string items to itemShop using AddItem() and prints out a debug log:

Figure 13.2: Console output after adding an item to a generic class

We wrote AddItem() to take in a parameter of the same type as our generic Shop instance. Since itemShop was created to hold string values, we add the Potion and Antidote string values without any issues.

However, if you try and add an integer, for example, you’ll get an error saying that the generic type of the itemShop doesn’t match:

Figure 13.3: Conversion error in a generic class

Now that you’ve written a generic method, you need to know how to use multiple generic types in a single class. For example, what if we wanted to add a method to the Shop class that finds out how much of a given item is in stock? We can’t use type T again because it’s already been defined in the class definition, just like we couldn’t declare multiple variables with the same name in the same class. So what do we do?

Add the following method to the bottom of the Shop class:

// 1
public int GetStockCount<U>()
{
    // 2
    var stock = 0;
    // 3
    foreach (var item in inventory)
    {
        if (item is U)
        {
            stock++;
        }
    }
    // 4
    return stock;
}

Let’s break down our new method:

  1. Declares a method that returns an int value for how many matching items of type U we find in the inventory
    • Generic type parameter naming is completely up to you, just like naming variables. Conventionally, they start at T and continue in alphabetical order from there.
  2. Creates a variable to hold the number of matching stock items we find and eventually return from the inventory
  3. Uses a foreach loop to go through the inventory list and increase the stock value every time a match is found
  4. Returns the number of matching stock items

The problem here is that we’re storing string values in our shop, so if we try and look up how many string items we have, we’ll get the full inventory:

Debug.Log("Items for sale: " + itemShop.GetStockCount<string>());

This will print something like the following to the console:

Figure 13.4: Console output from using multiple generic string types

On the other hand, if we tried to look up integer types in our inventory, we’d get no results because we’re only storing strings:

Debug.Log("Items for sale: " + itemShop.GetStockCount<int>());

This will print something like the following to the console:

Figure 13.5: Console output using multiple non-matching generic types

Neither of these scenarios is ideal since we can’t make sure our shop inventory is storing AND can be searched for the same item type. But here’s where generics really shine—we can add rules for our generic classes and methods to enforce the behavior we want, which we’ll cover in the next section.

Constraint type parameters

One of the great things about generics is that their type parameters can be limited. This might contradict what we’ve learned about generics so far, but just because a class can contain any type doesn’t mean it should be allowed to. For example, think of a game where you need to store a list or characters, but you want the character list to be limited to enemy types. You could check each character before adding it to the list, but that wouldn’t be efficient. Instead, we can just say that the list only accepts enemy types and leave it at that.

To constrain a generic type parameter, we need a new keyword and a syntax we haven’t seen before:

public class SomeGenericCollection<T> where T: ConstraintType {}

The where keyword defines the rules that T must pass before it can be used as a generic type parameter. It essentially says SomeGenericClass can take in any T type as long as it conforms to the constraining type. The constraining rules aren’t anything mystical or scary; they’re concepts we’ve already covered:

  • Adding the class keyword would constrain T to types that are classes
  • Adding the struct keyword would constrain T to types that are structs
  • Adding an interface, such as IManager, as the type would limit T to types that adopt the interface
  • Adding a custom class, such as Character, would constrain T to only that class type

If you need a more flexible approach to account for classes that have subclasses, you can use where T : U, which specifies that the generic T type must be of, or derive from, the U type. This is a little advanced for our needs, but you can find more details at: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters.

Just for fun, let’s constrain Shop to only accept a new type called Collectable:

  1. Create a new script in the Scripts folder, name it Collectable, and add the following code:
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
     
    public class Collectable
    {
        public string name;
    }
     
    public class Potion : Collectable
    {
        public Potion()
        {
            this.name = "Potion";
        }
    }
     
    public class Antidote : Collectable
    {
        public Antidote()
        {
            this.name = "Antidote";
        }
    }
    
  2. All we’ve done here is declare a new class called Collectable with a name property, and created subclasses for potions and antidotes. With this structure, we can enforce our Shop to only accept Collectable types, and our stock-finding method to only accept Collectable types as well so we can compare them and find matches.
  3. Open up Shop and update the class declaration:
    public class Shop<T> where T : Collectable
    
  4. Update the GetStockCount() method to constrain U to equal whatever the initial generic T type is:
    public int GetStockCount<U>() where U : T
    {
        var stock = 0;
        foreach (var item in inventory)
        {
            if (item is U)
            {
                stock++;
            }
        }
        return stock;
    }
    
  5. In GameBehavior, update the itemShop instance to the following code:
    var itemShop = new Shop<Collectable>();
    itemShop.AddItem(new Potion());
    itemShop.AddItem(new Antidote());
    Debug.Log("Items for sale: " + itemShop.GetStockCount<Potion>();
    
  6. This will result in a debug log showing only one Potion for sale because that’s the Collectable type we specified:

Figure 13.6: Output from updated GameBehavior script

In our example, we can ensure only collectible types are allowed in our shops. If we accidentally try and add non-collectible types in our code, Visual Studio will alert us about trying to break our own rules, as shown in Figure 13.7:

Graphical user interface, text, application  Description automatically generated

Figure 13.7: Error with incorrect generic type

Adding generics to Unity objects

Generics also work with Unity scripts and GameObjects. For example, we can easily create a generic destroyable class to use on any MonoBehaviour or object Component we want to delete from the scene. If this sounds familiar, it’s what our BulletBehavior does for us, but it’s not applicable to anything other than that script. To make this more scalable, let’s make any script that inherits from MonoBehaviour destroyable:

  1. Create a new script in the Scripts folder, name it Destroyable, and add the following code:
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
     
    public class Destroyable<T> : MonoBehaviour where T : MonoBehaviour
    {
        public float OnscreenDelay = 3f;
     
        void Start()
        {
            Destroy(this.gameObject, OnscreenDelay);
        }
    }
    
  2. Delete all the code inside BulletBehavior and inherit from the new generic class:
    public class BulletBehavior : Destroyable<BulletBehavior>
    {
    }
    

We’ve now turned our BulletBehavior script into a generic destroyable object. Nothing changes in the Bullet Prefab, but we can make any other object destroyable by inheriting from the generic Destroyable class. In our example, this would boost code efficiency and reusability if we created multiple projectile Prefabs and wanted them all to be destroyable, but at different times.

Generic programming is a powerful tool in our toolbox, but with the basics covered it’s time to talk about an equally important topic as you progress in your programming journey—delegation!

Delegating actions

There will be times when you need to pass off, or delegate, the execution of a method from one file to another. In C#, this can be accomplished through delegate types, which store references to methods and can be treated like any other variable. The only caveat is that the delegate itself and any assigned method need to have the same signature—just like integer variables can only hold whole numbers and strings can only hold text.

Creating a delegate is a mix between writing a function and declaring a variable:

public delegate returnType DelegateName(int param1, string param2);

You start with an access modifier followed by the delegate keyword, which identifies it to the compiler as a delegate type. A delegate type can have a return type and name as a regular function, as well as parameters if needed. However, this syntax only declares the delegate type itself; to use it, you need to create an instance as we do with classes:

public DelegateName someDelegate;

With a delegate type variable declared, it’s easy to assign a method that matches the delegate signature:

public DelegateName someDelegate = MatchingMethod;
public void MatchingMethod(int param1, string param2) 
{
    // ... Executing code here ...
}

Notice that you don’t include the parentheses when assigning MatchingMethod to the someDelegate variable, as it’s not calling the method at this point. What it’s doing is delegating the calling responsibility of MatchingMethod to someDelegate, which means we can call the function as follows:

someDelegate();

This might seem cumbersome at this point in your C# skill development, but I promise you that being able to store and execute methods as variables will come in handy down the road.

Creating a debug delegate

Let’s create a simple delegate type to define a method that takes in a string and eventually prints it out using an assigned method. Open up GameBehavior and add the following code:

public class GameBehavior : MonoBehaviour, IManager
{
    // ... No other changes needed ...
 
    // 1
    public delegate void DebugDelegate(string newText);
 
    // 2
    public DebugDelegate debug = Print;
 
    public void Initialize() 
    {
        _state = "Game Manager initialized..";
        _state.FancyDebug();
        
        // 3
        debug(_state);
   	     // ... No changes needed ...
    }
    // 4
    public static void Print(string newText)
    {
        Debug.Log(newText);
    }
}

Let’s break down the code:

  1. Declares a public delegate type named DebugDelegate to hold a method that takes in a string parameter and returns void
  2. Creates a new DebugDelegate instance named debug and assigns it a method with a matching signature named Print()
  3. Replaces the Debug.Log(_state) code inside Initialize() with a call to the debug delegate instance instead
  4. Declares Print() as a static method that takes in a string parameter and logs it to the console:

Figure 13.8: Console output from a delegate action

Nothing in the console has changed, but instead of directly calling Debug.Log() inside Initialize(), that operation has been delegated to the debug delegate instance. While this is a simplistic example, delegation is a powerful tool when you need to store, pass, and execute methods as their types.

In Unity, we’ve already worked with examples of delegation by using the OnCollisionEnter() and OnCollisionExit() methods, which are methods that are called through delegation. In the real world, custom delegates are most useful when paired with events, which we’ll see in a later section of this chapter.

Delegates as parameter types

Since we’ve seen how to create delegate types for storing methods, it makes sense that a delegate type could also be used as a method parameter itself. This isn’t that far removed from what we’ve already done, but it’s a good idea to cover our bases.

Let’s see how a delegate type can be used as a method parameter. Update GameBehavior with the following code:

public class GameBehavior : MonoBehaviour, IManager
{
    // ... No changes needed ...
    public void Initialize() 
    {
        _state = "Game Manager initialized..";
        _state.FancyDebug();
        debug(_state);
        // 1
        LogWithDelegate(debug);
    }
    // 2
    public void LogWithDelegate(DebugDelegate del)
    {
        // 3
        del("Delegating the debug task...");
    }
}

Let’s break down the code:

  1. Calls LogWithDelegate() and passes in our debug variable as its type parameter
  2. Declares a new method that takes in a parameter of the DebugDelegate type
  3. Calls the delegate parameter’s function and passes in a string literal to be printed out:

Figure 13.9: Console output of a delegate as a parameter type

We’ve created a method that takes in a parameter of the DebugDelegate type, which means that the actual argument passed in will represent a method and can be treated as one. Think of this example as a delegation chain, where LogWithDelegate() is two steps removed from the actual method doing the debugging, which is Print(). Creating a delegation chain like this isn’t always a common solution in a game or application scenario, but when you need to control levels of delegation it’s important to understand the syntax involved. This is especially true in scenarios where your delegation chain is spread across multiple scripts or classes.

It’s easy to get lost with delegation if you miss an important mental connection, so go back and review the code from the beginning of the section and check the docs at: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/.

Now that you know how to work with basic delegates, it’s time to talk about how events can be used to efficiently communicate information between multiple scripts. Honestly, the best use case for a delegate is being paired with events, which we’ll dive into next.

Firing events

C# events allow you to essentially create a subscription system based on actions in your games or apps. For instance, if you wanted to send out an event whenever an item is collected, or when a player presses the spacebar, you could do that. However, when an event fires, it doesn’t automatically have a subscriber, or receiver, to handle any code that needs to execute after the event action.

Any class can subscribe or unsubscribe to an event through the calling class the event is fired from; just like signing up to receive notifications on your phone when a new post is shared on Facebook, events form a kind of distributed-information superhighway for sharing actions and data across your application.

Declaring events is similar to declaring delegates in that an event has a specific method signature. We’ll use a delegate to specify the method signature we want the event to have, then create the event using the delegate type and the event keyword:

public delegate void EventDelegate(int param1, string param2);
public event EventDelegate eventInstance;

This setup allows us to treat eventInstance as a method because it’s a delegate type, which means we can send it out at any time by calling it:

eventInstance(35, "John Doe");

Unity also has its own built-in event type called UnityAction that can be customized however you need. Check out the following link for more information and code: https://docs.unity3d.com/2022.2/Documentation/ScriptReference/Events.UnityAction.html.

Your next task is to create an event of your own and fire it off in the appropriate place inside PlayerBehavior.

Creating and invoking events

Let’s create an event to fire off any time our player jumps. Open up PlayerBehavior and add the following changes:

public class PlayerBehavior : MonoBehaviour 
{
    // ... No other variable changes needed ...
    // 1
    public delegate void JumpingEvent();
    // 2
    public event JumpingEvent playerJump;
    void Start()
    {
        // ... No changes needed ...
    }
    void Update() 
    {
        // ... No changes needed ...
    }
    void FixedUpdate()
    {
        if(_isJumping && IsGrounded())
        {
            _rb.AddForce(Vector3.up * jumpVelocity,
               ForceMode.Impulse);
            // 3
            playerJump();
        }
    }
    // ... No changes needed in IsGrounded or OnCollisionEnter
}

Let’s break down the code:

  1. Declares a new delegate type that returns void and takes in no parameters
  2. Creates an event of the JumpingEvent type, named playerJump, that can be treated as a method that matches the preceding delegate’s void return and no parameter signature
  3. Calls playerJump after the force is applied in Update()

We have successfully created a simple delegate type that takes in no parameters and returns nothing, as well as an event of that type to execute whenever the player jumps. Each time the player jumps, the playerJump event is sent out to all of its subscribers to notify them of the action.

After the event fires, it’s up to its subscribers to process it and do any additional operations, which we’ll see in the Handling event subscriptions section, next.

Handling event subscriptions

Right now, our playerJump event has no subscribers, but changing that is simple and very similar to how we assigned method references to delegate types in the last section:

someClass.eventInstance += EventHandler;

Since events are variables that belong to the class they’re declared in, and subscribers will be other classes, a reference to the event-containing class is necessary for subscriptions. The += operator is used to assign a method that will fire when an event executes, just like setting up an out-of-office email.

Like assigning delegates, the method signature of the event handler method must match the event’s type. In our previous syntax example, that means EventHandler needs to be the following:

public void EventHandler(int param1, string param2) {}

In cases where you need to unsubscribe from an event, you simply do the reverse of the assignment by using the -= operator:

someClass.eventInstance -= EventHandler;

Event subscriptions are generally handled when a class is initialized or destroyed, making it easy to manage multiple events without messy code implementations.

Now that you know the syntax for subscribing and unsubscribing to events, it’s your turn to put this into practice in the GameBehavior script.

Now that our event is firing every time the player jumps, go back and update GameBehavior.cs with the following code to capture the action:

public class GameBehavior : MonoBehaviour, IManager
{
    // 1
    public PlayerBehavior playerBehavior;
 
    // 2
    void OnEnable()
    {
        // 3
        GameObject player = GameObject.Find("Player");
        // 4
        playerBehavior = player.GetComponent<PlayerBehavior>();
        // 5
        playerBehavior.playerJump += HandlePlayerJump;
        debug("Jump event subscribed...");
    }
 
    // 6
    public void HandlePlayerJump()
    {
         debug("Player has jumped...");
    }
    // ... No other changes ...
}

Let’s break down the code:

  1. Creates a public variable of type PlayerBehavior
  2. Declares the OnEnable() method, which is called whenever the object the script is attached to becomes active in the scene

    OnEnable is a method in the MonoBehaviour class, so all Unity scripts have access to it. This is a great place to put event subscriptions instead of Awake because it only executes when the object is active, not just in the process of loading.

  1. Finds the Player object in the scene and stores its GameObject in a local variable
  2. Uses GetComponent() to retrieve a reference to the PlayerBehavior class attached to the Player and stores it in the playerBehavior variable
  3. Subscribes to the playerJump event declared in PlayerBehavior with a method named HandlePlayerJump using the += operator
  4. Declares the HandlePlayerJump() method with a signature that matches the event’s type and logs a success message using the debug delegate each time the event is received:

Figure 13.10: Console output from a delegate event subscription

To correctly subscribe and receive events in GameBehavior, we had to grab a reference to the PlayerBehavior class attached to the player. We could have done this all in one line, but it’s much more readable when it’s split up. We then assigned a method to the playerJump event that will execute whenever the event is received, and complete the subscription process.

Now each time you jump, you’ll see a debug message with the event message:

Figure 13.11: Console output from a delegate event firing

Since event subscriptions are configured in scripts, and scripts are attached to Unity objects, our job isn’t done yet. We still need to handle how we clean up subscriptions when the object is destroyed or removed from the scene, which we’ll cover in the next section.

Cleaning up event subscriptions

Even though our player is never destroyed in our prototype, that’s a common feature in games when you lose. It’s always important to clean up event subscriptions because they take up allocated resources, as we discussed with streams in Chapter 12, Saving, Loading, and Serializing Data.

We don’t want any subscriptions hanging around after the subscribed object has been destroyed, so let’s clean up our jumping event. Add the following code to GameBehavior after the OnEnable method:

// 1
private void OnDisable()
{
    // 2
    playerBehavior.playerJump -= HandlePlayerJump;
    debug("Jump event unsubscribed...");
}

Let’s break down our new code addition:

  1. Declares the OnDisable() method, which belongs to the MonoBehavior class and is the companion to the OnEnable() method we used earlier
    • Any cleanup code you need to write should generally go in this method, as it executes when the object the script is attached to is inactive
  2. Unsubscribes the playerJump event from HandlePlayerJump using the -= operator and prints out a console message

Now our script properly subscribes and unsubscribes to an event when the GameObject is enabled and disabled, leaving no unused resources in our game scene.

That wraps up our discussion on events. Now you can broadcast them to every corner of your game from a single script and react to scenarios like a player losing life, collecting items, or updating the UI. However, we still have to discuss a very important topic that no program can succeed without, and that’s error handling.

Handling exceptions

Efficiently incorporating errors and exceptions into your code is both a professional and personal benchmark in your programming journey. Before you start yelling “Why would I add errors when I’ve spent all this time trying to avoid them?!”, you should know that I don’t mean adding errors to break your existing code. It’s quite the opposite—including errors or exceptions and handling them appropriately when pieces of functionality are used incorrectly makes your code base stronger and less prone to crashes, not weaker.

Throwing exceptions

When we talk about adding errors, we refer to the process as exception throwing, which is an apt visual analogy. Throwing exceptions is part of something called defensive programming, which essentially means that you actively and consciously guard against improper or unplanned operations in your code. To mark those situations, you throw out an exception from a method that is then handled by the calling code.

Let’s take an example: say we have an if statement that checks whether a player’s email address is valid before letting them sign up. If the email entered is not valid, we want our code to throw an exception:

public void ValidateEmail(string email)
{
    if(!email.Contains("@"))
    {
        throw new System.ArgumentException("Email is invalid");
    }
}

We use the throw keyword to send out the exception, which is created with the new keyword followed by the exception we specify. System.ArgumentException() will log the information about where and when the exception was executed by default, but can also accept a custom string if you want to be more specific.

ArgumentException is a subclass of the Exception class and is accessed through the System class shown previously. C# comes with many built-in exception types, including subclasses for checking for null values, out-of-range collection values, and invalid operations. Exceptions are a prime example of using the right tool for the right job. Our example only needs the basic ArgumentException, but you can find the full descriptive list at: https://docs.microsoft.com/en-us/dotnet/api/system.exception#Standard.

Let’s keep things simple on our first foray into exceptions and make sure that our level only restarts if we provide a positive scene index number:

  1. Open up Utilities and add the following code to the overloaded version of RestartLevel(int):
    public static class Utilities 
    {
        // ... No changes needed ...
        public static bool RestartLevel(int sceneIndex) 
        {
            // 1
            if(sceneIndex < 0)
            {
                // 2
                throw new System.ArgumentException("Scene index cannot be negative");
            }
     
            Debug.Log("Player deaths: " + PlayerDeaths);
            string message = UpdateDeathCount(ref PlayerDeaths);
            Debug.Log("Player deaths: " + PlayerDeaths);
            Debug.Log(message);
       
            SceneManager.LoadScene(sceneIndex);
            Time.timeScale = 1.0f;
     
            return true;
        }
    }
    
  2. Change RestartLevel() in GameBehavior to take in a negative scene index and lose the game:
    // 3
    public void RestartScene()
    {
        Utilities.RestartLevel(-1);
    }
    

Let’s break down the code:

  1. Declares an if statement to check that sceneIndex is not less than 0 or a negative number
  2. Throws an ArgumentException with a custom message if a negative scene index is passed in as an argument
  3. Calls RestartLevel() with a scene index of -1:

Figure 13.12: Console output when an exception is thrown

When we lose the game now, RestartLevel() is called, but since we’re using -1 as the scene index argument, our exception is fired before any of the scene manager logic is executed. We don’t have any other scenes configured in our game at the moment, but this defensive code acts as a safeguard and doesn’t let us take an action that might crash the game (Unity doesn’t support negative indexes when loading scenes).

Now that you’ve successfully thrown an error, you need to know how to handle the fallout from the error, which leads us to our next section and the try-catch statement.

Using try-catch

Now that we’ve thrown an error, it’s our job to safely handle the possible outcomes that calling RestartLevel() might have because, at this point, this is not addressed properly. The way to do this is with a new kind of statement, called try-catch:

try
{
    // Call a method that might throw an exception
}
catch (ExceptionType localVariable)
{
    // Catch all exception cases individually
}

The try-catch statement is made up of consecutive code blocks that are executed on different conditions; it’s like a specialized if/else statement. We call any methods that potentially throw exceptions in the try block—if no exceptions are thrown, the code keeps executing without interruption. If an exception is thrown, the code jumps to the catch statement that matches the thrown exception, just like switch statements do with their cases. catch statements need to define what exception they are accounting for and specify a local variable name that will represent it inside the catch block.

You can chain as many catch statements after the try block as you need to handle multiple exceptions thrown from a single method, provided they are catching different exceptions. For example:

try
{
    // Call a method that might throw an exception
}
catch (ArgumentException argException)
{
    // Catch argument exceptions here
}
catch (FileNotFoundException fileException)
{
    // Catch exceptions for files not found here
}

There’s also an optional finally block that can be declared after any catch statements that will execute at the very end of the try-catch statement, regardless of whether an exception was thrown:

finally
{
    // Executes at the end of the try-catch no matter what
}

Your next task is to use a try-catch statement to handle any errors thrown from restarting the level unsuccessfully. Now that we have an exception that is thrown when we lose the game, let’s handle it safely. Update GameBehavior with the following code and lose the game again:

public class GameBehavior : MonoBehaviour, IManager
{
    // ... No variable changes needed ...
    public void RestartScene()
    {
        // 1 
        try
        {
            Utilities.RestartLevel(-1);
            debug("Level successfully restarted...");
        }
        // 2
        catch (System.ArgumentException exception)
        {
            // 3
            Utilities.RestartLevel(0);
            debug("Reverting to scene 0: " + exception.ToString());
        }
        // 4
        finally
        {
            debug("Level restart has completed...");
        }
    }
}

Let’s break down the code:

  1. Declares the try block and moves the call to RestartLevel() inside with a debug command to print out if the restart is completed without any exceptions
  2. Declares the catch block and defines System.ArgumentException as the exception type it will handle and exception as the local variable name
  3. Restarts the game at the default scene index if the exception is thrown:
    • Uses the debug delegate to print out a custom message, plus the exception information, which can be accessed from exception and converted into a string with the ToString() method

      Since exception is of the ArgumentException type, there are several properties and methods associated with the Exception class that you can access. These are often useful when you need detailed information about a particular exception.

  1. Adds a finally block with a debug message to signal the end of the exception-handling code:

Figure 13.13: Console output of a complete try-catch statement

When RestartLevel() is called now, our try block safely allows it to execute, and if an error is thrown, it’s caught inside the catch block. The catch block restarts the level at the default scene index and the code proceeds to the finally block, which simply logs a message for us.

It’s important to understand how to work with exceptions, but you shouldn’t get into the habit of putting them everywhere in your code. This will lead to bloated classes and might affect the game’s processing time. Instead, you want to use exceptions where they are most needed—invalidation or data processing, rather than game mechanics.

C# allows you the freedom to create your exception types to suit any specific needs your code might have, but that’s beyond the scope of this book. It’s just a good thing to remember for the future: https://docs.microsoft.com/en-us/dotnet/standard/exceptions/how-to-create-user-defined-exceptions.

Summary

While this chapter brings us to the end of our practical adventure into C# and Unity 2022, I hope that your journey into game programming and software development has just begun. You’ve learned everything from creating variables, methods, and class objects to writing your game mechanics, enemy behavior, and more.

The topics we’ve covered in this chapter have been a level above what we dealt with for the majority of this book, and with good reason. You already know your programming brain is a muscle that you need to exercise before you can advance to the next plateau. That’s all generics, events, and design patterns are: just the next rung up the programming ladder.

In the next chapter, I will leave you with resources, further reading, and lots of other helpful (and, dare I say, cool) opportunities and information about the Unity community and the software development industry at large.

Happy coding!

Pop quiz—intermediate C#

  1. What is the difference between a generic and non-generic class?
  2. What needs to match when assigning a value to a delegate type?
  3. How would you unsubscribe from an event?
  4. Which C# keyword would you use to send out an exception in your code?

Don’t forget to check your answers against mine in the Pop Quiz Answers appendix to see how you did!

Join us on discord!

Read this book alongside other users, Unity game development experts and the author himself.

Ask questions, provide solutions to other readers, chat with the author via. Ask Me Anything sessions and much more.

Scan the QR code or visit the link to join the community.

https://packt.link/csharpwithunity

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.142.128