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 or a method is used, a concrete type will be assigned, but the code itself treats it as a generic type. You'll see generic programming most often in custom collection types that need to be able to perform the same operations on elements regardless of type. While this might not conceptually make sense yet, it will once when we look at a concrete example in the next section.

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

Generic objects

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.

Time for action  creating a generic collection

Let's create a more complete generic list class to store some fictional inventory items with the following steps:

  1. Create a new C# script in the Scripts folder, name it InventoryList, and update its code to the following:
 using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 1
public class InventoryList<T>
{
// 2
public InventoryList()
{
Debug.Log("Generic list initalized...");
}
}
  1. Create a new instance of InventoryList in GameBehavior:
 public class GameBehavior : MonoBehaviour, IManager
{
// ... No changes needed ...

void Start()
{
Initialize();

// 3
InventoryList<string> inventoryList = new
InventoryList<string>();

}

// ... No changes to Initialize or OnGUI ...
}

Let's break down the code:

  1. Declares a new generic class named InventoryList with a T type parameter
  2. Adds a default constructor with a simple debug log to print out whenever a new InventoryList instance is created
  1. Creates a new instance of InventoryList in GameBehavior to hold string values:

Nothing new has happened here yet in terms of functionality, but Visual Studio recognizes InventoryList as a generic class because of its generic type parameter, T. This sets us up to include additional generic operations in the InventoryList class itself.

Generic methods

A standalone generic method needs to 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!");

However, 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 https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-methods.

Your next task is to create a new generic item and use it in the InventoryList script.

Time for action  adding a generic item

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 InventoryList and update the code as follows:
public class InventoryList<T>
{
// 1
private T _item;
public T item
{
get { return _item; }
}

public InventoryList()
{
Debug.Log("Generic list initialized...");
}

// 2
public void SetItem(T newItem)
{
// 3
_item = newItem;
Debug.Log("New item added...");
}
}
  1. Go into GameBehavior and add an item to inventoryList:
 public class GameBehavior : MonoBehaviour, IManager
{
// ... No changes needed ...

void Start()
{
Initialize();
InventoryList<string> inventoryList = new
InventoryList<string>();

// 4
inventoryList.SetItem("Potion");
Debug.Log(inventoryList.item);
}

public void Initialize()
{
// ... No changes needed ...
}

void OnGUI()
{
// ... No changes needed ...
}
}

Let's break down the code:

  1. Adds a public item property of the T type with a private backing variable of the same type
  2. Declares a new method inside InventoryList, named SetItem, that takes in a T type parameter
  3. Sets the value of _item to the generic parameter passed into SetItem() and debugs out a success message
  4. Assigns a string value to the item property on inventoryList using SetItem() and prints out a debug log:

We wrote SetItem() to take in a parameter of whatever type our generic InventoryList instance is created with and assign it to a new class property using a public and private backing approach. Since inventoryList was created to hold string values, we assigned the "Potion" string to the item property without any issues. However, this method works equally well with any type that an InventoryList instance might hold.

Constraining type parameters

One of the great things about generics is that their type parameters can be limited. This might seem counterintuitive to what we've learned about generics so far, but just because a class can contain any type, doesn't mean it should necessarily be allowed to. 

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

Time for action  limiting generic elements

Just for fun, let's constrain InventoryList to only be able to accept types that are classes:

  1. Open up InventoryList and add in the following code:
 // 1
public class InventoryList<T>
where T: class
{
// ... No changes needed ...
}

Since our inventoryList instance, the example uses strings, and strings are classes, so there's no problem with our code. However, if we were to change the type constraint to a struct or an interface name, our generic class would throw an error. This kind of situation is especially useful when you need to safeguard your generic classes or methods against types that you don't want to support.

Delegating actions

There will be times when you need to pass off, or delegate, the actual execution of a method. 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. 

Basic syntax

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# skills, but I promise you that being able to store and execute methods as variables will come in handy down the road.

Time for action  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:

  1. Open up GameBehavior and add in the following code:
public class GameBehavior : MonoBehaviour, IManager
{
// ... No other changes needed ...

// 1
public delegate void DebugDelegate(string newText);

// 2
public DebugDelegate debug = Print;

// ... No other changes needed ...

void Start()
{
// ... No changes needed ...
}

public void Initialize()
{
_state = "Manager initialized..";
_state.FancyDebug();

// 3
debug(_state);
}

// 4
public static void Print(string newText)
{
Debug.Log(newText);
}

void OnGUI()
{
// ... No changes needed ...
}
}

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:

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. We've already worked with a real-world example of delegation with the OnCollisionEnter() and OnCollisionExit() methods, which are Unity methods that are called through delegation.

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.

Time for action  using a delegate argument

Let's see how a delegate type can be used as a method parameter:

  1. Update GameBehavior with the following code:
public class GameBehavior : MonoBehaviour, IManager
{
// ... No changes needed ...

void Start()
{
// ... No changes needed ...
}

public void Initialize()
{
_state = "Manager initialized..";
_state.FancyDebug();

debug(_state);

// 1
LogWithDelegate(debug);
}

public static void Print(string newText)
{
// ... No changes needed ...
}

// 2
public void LogWithDelegate(DebugDelegate del)
{
// 3
del("Delegating the debug task...");
}

void OnGUI()
{
// ... No changes needed ...
}
}

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:

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().

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 we talked about events and how they can be used to efficiently communicate information between multiple scripts.

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.

Basic syntax

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");

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

Time for action  creating an event

Let's create an event to fire off any time our player jumps:

  1. Open up PlayerBehavior and add in 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()
{
_vInput = Input.GetAxis("Vertical") * moveSpeed;
_hInput = Input.GetAxis("Horizontal") * rotateSpeed;
}


void FixedUpdate()
{
if(IsGrounded() && Input.GetKeyDown(KeyCode.Space))
{
_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 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.

Time for action  subscribing to an event

Now that our event is firing every time the player jumps, we need a way to capture that action:

  1. Go back to GameBehavior and update the following code:
public class GameBehavior : MonoBehaviour, IManager
{
// ... No changes needed ...

void Start()
{
// ... No changes needed ...
}

public void Initialize()
{
_state = "Manager initialized..";
_state.FancyDebug();

debug(_state);
LogWithDelegate(debug);

// 1
GameObject player = GameObject.Find("Player");

// 2
PlayerBehavior playerBehavior =
player.GetComponent<PlayerBehavior>();


// 3
playerBehavior.playerJump += HandlePlayerJump;
}

// 4
public void HandlePlayerJump()
{
debug("Player has jumped...");
}

// ... No changes in Print,
LogWithDelegate, or
OnGUI ...

}

Let's break down the code:

  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 a local variable
  3. Subscribes to the playerJump event declared in PlayerBehavior with a method named HandlePlayerJump
  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:

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. Since the player is never destroyed in our prototype, there's no need to unsubscribe to playerJump, but don't forget to take that step if the situation calls for it.

That wraps up our discussion on events, but 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 preceding System class shown. C# comes with many built-in exception types, but we're not going to dig too deeply into those as that's something you can do once you understand the basics of the overall system.

A full list of C# exceptions can be found under the Choosing Standard Exceptions heading at https://docs.microsoft.com/en-us/dotnet/api/system.exception?view=netframework-4.7.2#Standard.

Time for action  checking negative scene indexes

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 in the following code to the overloaded version of RestartLevel():
public static class Utilities 
{
public static int playerDeaths = 0;

public static string UpdateDeathCount(out int countReference)
{
// ... No changes needed ...
}

public static void RestartLevel()
{
// ... No changes needed ...
}

public static bool RestartLevel(int sceneIndex)
{
// 1
if(sceneIndex < 0)
{
// 2
throw new System.ArgumentException("Scene index cannot
be negative");

}

SceneManager.LoadScene(sceneIndex);
Time.timeScale = 1.0f;

return true;
}
}
  1. Change RestartLevel() in the OnGUI() method of GameBehavior to take in a negative scene index and lose the game:
if(showLossScreen)
{
if (GUI.Button(new Rect(Screen.width / 2 - 100,
Screen.height / 2 - 50, 200, 100), "You lose..."))
{
// 3
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:

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. This will put a stop to our game as we don't have any other button options at this point, but the safeguard works as expected 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 that might be thrown from a single method.

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.

Time for action  catching restart errors

Now that we have an exception that is thrown when we lose the game, let's handle it safely:

  1. Update GameBehavior with the following code and lose the game again:
public class GameBehavior : MonoBehaviour, IManager
{
// ... No variable changes needed ...

// ... No changes needed in Start
Initialize,
Print,
or LogWithDelegate ...


void OnGUI()
{
// ... No other changes to OnGUI needed ...

if(showLossScreen)
{
if (GUI.Button(new Rect(Screen.width / 2 - 100,
Screen.height / 2 - 50, 200, 100), "You lose..."))
{
// 1
try
{
Utilities.RestartLevel(-1);
debug("Level restarted successfully...");
}
// 2
catch (System.ArgumentException e)
{
// 3
Utilities.RestartLevel(0);
debug("Reverting to scene 0: " +
e.ToString());

}
// 4
finally
{
debug("Restart handled...");
}
}
}
}
}

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 e as the local variable name
  1. 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 e and converted into a string with the ToString() method
Since e 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:

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 in the habit of putting them in 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.

Before we close out the chapter, there's one more topic that you need to be introduced to, and that's design patterns. While we won't go into the actual code for these patterns (there are entire books on that), we will talk about their purpose and usability in programming.

Design pattern primer

Before we wrap up the chapter, I want to talk about a concept that will play a huge part in your programming career if you choose to continue: design patterns. Googling design patterns or software programming patterns will give you a host of definitions and examples, which can be overwhelming if you've never encountered them before. Let's simplify the term and define a design pattern as follows:

A template for solving programming problems or situations that you'll run into on a regular basis during any kind of application development. These are not hardcoded solutions—they're more like tested guidelines and best practices that can be adapted to fit a specific situation.

There's a lot of history behind how design patterns became an integral part of the programming lexicon, but that excavation is up to you. If this concept strikes a chord with your programming brain, start with the book Design Patterns: Elements of Reusable Object-Oriented Software and its authors, the Gang of Four.

The design patterns we're talking about only apply to object-oriented programming (OOP) languages and paradigms, so they're not universally applicable if you are working in non-OOP environments. 

Common game patterns

While there are over 35 documented design patterns broken up over four subdomains of functionality, only a few of them are uniquely suited to game development. We'll spend a little time here briefly introducing them so that you get the hang of what we've been talking about so far:

  • The Singleton pattern ensures that a given class only has one instance in a program, paired with one global access point (very useful for game manager classes). 
  • The Observer pattern lays out a blueprint for notification systems, alerting subscribers to changes in behavior through events. We've seen this in action on a small scale with our delegate/event examples, but it can be expanded to cover much more ground.
  • The State pattern allows an object to change its behavior based on what state it's in. This is extremely useful in creating smart enemies who appear to change tactics based on player actions or environmental conditions.
  • The Object Pool pattern recycles objects that aren't being used anymore, instead of having the program create new ones each time. This would be a great update to the shooting mechanic in Hero Born since there could potentially be a lag if too many bullets were spawned on a machine with low processing power.

If you're still unsure of how important design patterns are, know that Unity is built along the lines of the Composite (sometimes called the Component) pattern, which allows us to construct complex objects made up of separate functional pieces.

Again, this barely scratches the surface of what design patterns can do in real-world programming situations. I highly encourage you to dig into their history and application as soon as you finish up the next chapter—they'll be one of your best resources going forward.

Summary

While this chapter brings us to the end of our adventure into C# and Unity 2020, I hope that your journey into game programming and software development has just begun. You've learned everything from creating variables, methods, and building class object 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, which 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?
..................Content has been hidden....................

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