Chapter 16: Win and Lose Conditions

Now that we have a basic gameplay experience, it's time to make the game end sometime, both in the cases of winning and losing. One common way to implement this is through separated components with the responsibility of overseeing a set of Objects to detect certain situations that need to happen, such as the Player life becoming 0 or all of the waves being cleared. We will implement this through the concept of Managers, components that will manage several Objects, monitoring them.

In this chapter, we will examine the following Manager concepts:

  • Creating Object Managers
  • Creating Game Modes
  • Improving our code with events

With this knowledge, you will be able to not only create the victory and loose condition of the game, but also do that in a properly structured way using design patterns such as Singleton and Event Listeners. These skills are not only useful for creating the code for the winning and losing functions of the game, but any code in general.

Creating Object Managers

Not every Object in the scene should be something that can be seen, heard, or collided with. Some Objects can also exist with a conceptual meaning, not something tangible. Imagine you need to keep a count of the number of enemies, where do you save that? You also need someplace to save the current score of the Player, and you may be thinking it could be on the Player itself, but what happens if the Player dies and respawns? The data would be lost! In such scenarios, the concept of a Manager can be a useful way of solving this in our first games, so let's explore it.

In this chapter, we are going to see the following Object Manager concepts:

  • Implementing the Singleton design pattern
  • Creating Managers with Singleton

We will start by discussing what the Singleton design pattern is and how it helps us simplify the communication of Objects. With it we will create Managers Objects, which will allow us to centralize information of a group of Objects, among other things. Let's start discussing the Singleton design pattern.

Implementing the Singleton design pattern

Design patterns are usually described as common solutions to common problems. There are several coding design decisions you will have to make while you code your game, but luckily, the way to tackle the most common situations are well known and documented. In this section, we are going to discuss one of the most common design patterns, the Singleton, a very controversial but convenient one to implement in simple projects.

A Singleton pattern is used when we need a single instance of an Object, meaning that there shouldn't be more than one instance of a class and that we want it to be easily accessible (not necessarily, but useful in our scenario). We have plenty of cases in our game where this can be applied, for example, ScoreManager, a component that will hold the current score. In this case, we will never have more than one score, so we can take advantage of the benefits of the Singleton Manager here.

One benefit is being sure that we won't have duplicated scores, which makes our code less error-prone. Also, so far, we have needed to create public references and drag Objects via the Editor to connect two Objects or look for them using GetComponent, but with this pattern, we will have global access to our Singleton component, meaning you can just write the name of the component and you will access it. In the end, there's just one ScoreManager component, so specifying which one via the Editor is redundant. This is similar to Time.deltaTime, the class responsible for managing time—we have just one time.

Important note

If you are an advanced programmer, you may be thinking about code testing and dependency injection now, and you are right, but remember, we are trying to write simple code so far, so we will stick to this simple solution.

Let's create a Score Manager Object, responsible for handling the score, to show an example of a Singleton by doing the following:

  1. Create an empty GameObject (GameObject | Create Empty) and call it ScoreManager; usually, Managers are put in empty Objects, separated from the rest of the scene Objects.
  2. Add a script called ScoreManager to this Object with an int field called amount that will hold the current score.
  3. Add a field of the ScoreManager type called instance, but add the static keyword to it; this will make the variable global, meaning it can be accessed anywhere by just writing its name:
    Figure 16.1 – A static field that can be accessed anywhere in the code

    Figure 16.1 – A static field that can be accessed anywhere in the code

  4. In Awake, check whether the instance field is not null, and in that case, set ourselves as the instance reference using the this reference.
  5. In the else clause of the null checking if statement, print a message indicating that there's a second ScoreManager instance that must be destroyed:
    Figure 16.2 – Checking whether there's only one Singleton instance

    Figure 16.2 – Checking whether there's only one Singleton instance

    The idea is to save the reference to the only ScoreManager instance in the instance static field, but if by mistake the user creates two objects with the ScoreManager component, this if statement will detect it and inform the user of the error, asking them to take action. In this scenario, the first ScoreManager instance to execute Awake will find that there's no instance set (the field is null) so it will set itself as the current instance, while the second ScoreManager instance will find the instance is already set and will print the message. Remember that instance is a static field, the one shared between all classes, unlike regular reference fields, where each component will have its own reference, so in this case, we have two ScoreManagers added to the scene, and both will share the same instance field.

    To improve the example a little bit, it would be ideal to have a simple way to find the second ScoreManager in the game. It will be hidden somewhere in the Hierarchy and it would be difficult to find. We can replace print with Debug.Log, which is basically the same but allows us to pass a second argument to the function, which is an Object, to highlight when the message is clicked in the console. In this case, we will pass the gameObject reference to allow the console to highlight the duplicated Object:

    Figure 16.3 – Printing messages in the console with Debug.Log

    Figure 16.3 – Printing messages in the console with Debug.Log

  6. After clicking the log message, this GameObject will be highlighted in the Hierarchy:
    Figure 16.4 – The highlighted Object after clicking the message

    Figure 16.4 – The highlighted Object after clicking the message

  7. Finally, a little improvement can be made here by replacing Debug.Log with Debug.LogError, which will also print the message but with an error icon. In a real game, you will have lots of messages in the console, and highlighting the errors over the information messages will help us to identify them quickly:
    Figure 16.5 – Using LogError to print an error message

    Figure 16.5 – Using LogError to print an error message

  8. Try the code and observe the error message in the console:
Figure 16.6 – An error message in the console

Figure 16.6 – An error message in the console

The next step would be to use this Singleton somewhere, so in this case, we will make the enemies give points when they are killed by doing the following:

  1. Add a script to the Enemy Prefab called ScoreOnDeath with an int field called amount, which will indicate the number of points the Enemy will give when killed. Remember to set the value to something other than 0 in the Editor for the Prefab.
  2. Create the OnDestroy event function, which will be automatically called by Unity when this Object is destroyed; in our case, the Enemy:
    Figure 16.7 – The OnDestroy event function

    Figure 16.7 – The OnDestroy event function

    Important note

    Consider that the OnDestroy function is also called when we change scenes or the game is quitting, so in this scenario, maybe we will get points when changing scenes, which is not correct. So far, this is not a problem in our case, but later in this chapter, we will see a way to prevent this.

  3. Access the Singleton reference in the OnDestroy function by writing ScoreManager.instance, and add the amount field of our script to the amount field of the Singleton to increase the score when an Enemy is killed:
    Figure 16.8 – Full ScoreOnDeath component class contents

    Figure 16.8 – Full ScoreOnDeath component class contents

  4. Select the ScoreManager in the hierarchy, hit play, and kill some enemies to see the score increase with every kill. Remember to set the amount field of the ScoreOnDeath component of the Prefab.

As you can see, the Singleton simplified a lot the way to access ScoreManager and prevented us from having two versions of the same Object, which will help us to reduce errors in our code. Something to take into account is that now you will be tempted to just make everything a Singleton, such as the Player life or Player bullets and use it just to make your life easier to create gameplay such as power-ups, and while that will totally work, remember that your game will change, and I mean, change a lot; any real project will suffer that. Maybe today, the game will have just one Player, but maybe in the future, you will want to add a second Player or an AI companion, and you want the power-ups to affect them too, so if you abuse the Singleton pattern, you will have trouble handling those scenarios. Maybe the companion will try to get the pickup but the main Player will be healed instead!

The point here is to try to use the pattern as few times as you can, in cases where you don't have any other way to solve the problem. To be honest, there are always ways to solve problems without Singleton, but they are a little bit more difficult to implement for beginners, so I prefer to simplify your life a little bit to keep you motivated. With enough practice, you will reach a point where you will be ready to improve your coding standards.

Now that we know how to create Singletons, let's finish some other Managers that we will need later in the game.

Creating Managers with Singleton

Sometimes, we need a place to put together information about a group of similar Objects, for example, an Enemy Manager, to check the number of enemies and potentially access an array of them to iterate over them and do something, or maybe MissionManager, to have access to all of the active missions in our game. Again, these cases can be considered Singletons, single Objects that won't be repeated (in our current game design), so let's create the ones we will need in our game, that is, EnemyManager and WaveManager.

In our game, EnemyManager and WaveManager will just be places to save an array of references to the existent enemies and waves in our game, just as a way to know the current amount of them. There are ways to search all Objects of a certain type to calculate the count of them, but those functions are expensive and not recommended to use unless you really know what you are doing. So, having a Singleton with a separate updated list 
of references to the target Object type will require more code but will perform better. Also, as the game features increase, these Managers will have more functionality and helper functions to interact with those Objects.

Let's start with the enemies Manager by doing the following:

  1. Add a script called Enemy to the Enemy Prefab; this will be the script that will connect this Object with EnemyManager in a moment.
  2. Create an empty GameObject called EnemyManager and add a script to it called EnemiesManager.
  3. Create a public static field of the EnemiesManager type called instance inside the script and add the Singleton repetition check in Awake as we did in ScoreManager.
  4. Create a public field of the List<Enemy> type called enemies:
    Figure 16.9 – List of Enemy components

    Figure 16.9 – List of Enemy components

    A list in C# represents a dynamic array, an array capable of adding and removing Objects. You will see that you can add and remove elements to this list in the Editor, but keep the list empty; we will add enemies another way. Take into account that List is in the System.Collections.Generic namespace; you will find the using sentence at the beginning of our script. Also, consider that you can make the list private and expose it to the code via a getter instead of making it a public field; but as usual, we will make our code as simple as possible for now.

    Important note

    Remember that List is a class type, so it must be instantiated, but as this type has exposing support in the Editor, Unity will automatically instantiate it. You must use the new keyword to instantiate it in cases where you want a non-Editor-exposed list, such as a private one or a list in a regular non-component C# class.

    The C# list internally is implemented as an array. if you need a linked list, look at the LinkedList collection type.

  5. In the Start function of the Enemy script, access the EnemyManager Singleton and using the Add function of the enemies list, add this Object to the list. This will "register" this Enemy as active in the Manager, so other Objects can access the Manager and check for the current enemies. The Start function is called after all of the Awake function calls, and this is important because we need to be sure that the Awake function of the Manager is executed prior to the Start function of the Enemy to ensure that there is a Manager set as the instance.

    Important Note

    The problem we solved with the Start function is called a race condition, that is, when two pieces of code are not guaranteed to be executed in the same order, whereas Awake execution order can change due to different reasons. There are plenty of situations in code where this will happen, so pay attention to the possible race conditions in your code. Also, you might consider using more advanced solutions such as lazy initialization here, which can give you better stability, but again, for the sake of simplicity and exploring the Unity API, we will use the Start function approach for now.

  6. In the OnDestroy function, remove the Enemy from the list to keep the list updated with just the active ones:
Figure 16.10 – The Enemy script to register ourselves as an active Enemy

Figure 16.10 – The Enemy script to register ourselves as an active Enemy

With this, now we have a centralized place to access all of the active enemies in a simple but efficient way. I challenge you to do the same with the waves, using WaveManager, which will have the collection of all active Waves to later check whether all waves finished their work to consider the game as won. Take some time to solve this; you will find the solution in the following screenshots, starting with WavesManager:

Figure 16.11 – The full WavesManager script

Figure 16.11 – The full WavesManager script

You will need also the WavesSpawner script:

Figure 16.12 – The modified WaveSpawner script to support WavesManager

Figure 16.12 – The modified WaveSpawner script to support WavesManager

As you can see, WaveManager is created the same way EnemyManager was, just a Singleton with a list of WaveSpawner references, but WaveSpawner is different. We execute the Add function of the list in the Start event of WaveSpawner to register the wave as an active one, but the Remove function needs more work.

The idea is to deregister the wave from the active waves list when it finishes spawning all enemies when the spawner finishes its work. Before this modification, we used Invoke to call the CancelIncoke function after a while to stop the spawning, but now we need to do more after the end time. Instead of calling CancelInvoke after the specified wave end time, we will call a custom function called EndSpawner, which will call CancelInvoke to stop the spawner, Invoke Repeating, but also will call Remove from WavesManager list function to make sure the removing from the list is called exactly when WaveSpawner finishes its work.

Using Object Managers, we have now centralized information about a group of Objects, and we can add all sorts of Objects group logic here, but besides having this information for updating the UI (which we will do in the next chapter), we can use this information to detect whether the Victory and Lose conditions of our game are met, creating a Game Mode Object to detect that.

Creating Game Modes

We have created Objects to simulate lots of gameplay aspects of our game, but the game needs to end sometime, whether we win or lose. As always, the question is where to put this logic and that leads us to further questions. The main questions would be, will we always win or lose the game the same way? Will we have a special level with different criteria than to kill all of the waves, such as a timed survival? Only you know the answer to those questions, but if right now the answer is no, it doesn't mean that it won't change later, so it is advisable to prepare our code to adapt seamlessly to changes.

Important note

To be honest, preparing our code to adapt seamlessly to changes is almost impossible; there's no way to have perfect code that will consider every possible case, and we will always need to rewrite some code sooner or later. We will try to make the code as adaptable as possible to changes; always doing that doesn't consume lots of developing time and it's sometimes preferable to write simple code fast then complex code slow that might not be necessary, and so balance your time budget wisely.

To do this, we will separate the Victory and Lose conditions logic in its own Object, which I like to call the "Game Mode" (not necessarily an industry standard). This will be a component that will oversee the game, checking conditions that need to be met in order to consider the game over. It will be like the referee of our game. The Game Mode will constantly check the information in the Object Managers and maybe other sources of information to detect the needed conditions. Having this Object separated from other Objects allows us to create different levels with different Game Modes; just use another Game Mode script in that level and that's all.

In our case, we will have a single Game Mode for now, which will check whether the number of waves and enemies becomes 0, meaning that we have killed all of the possible enemies and the game is won. Also, it will check whether the life of the Player reaches 0, considering the game as lost in that situation. Let's create it by doing the following:

  1. Create a GameMode empty Object and add a WavesGameMode script to it. As you can see, we called the script with a descriptive name considering that we can add other game modes.
  2. In its Update function, check whether the number of enemies and waves reached 0 by using the Enemy and Wave Managers; in that case, just print a message in the console for now. All lists have a Count property, which will tell you the number of elements stored inside.
  3. Add a public field of the Life type called PlayerLife and drag the Player to that one; the idea is to also detect the lose condition here.
  4. In Update, add another check to detect whether the life amount of the PlayerLife reference has reached 0, and in that case, print a lose message in the console:
    Figure 16.13 – Win and lose condition checks in WavesGameMode

    Figure 16.13 – Win and lose condition checks in WavesGameMode

  5. Play the game and test both cases, whether the Player life reaches 0 or whether you have killed all enemies and waves.

    Important note

    Remember that we don't want two instances of this Object, so we can make it a Singleton also, but as this Object won't be accessed by others, that might be redundant; I will leave this up to you. Anyway, remember that this won't prevent you from having two different GameModes instantiated; for doing so, you can create a GameMode base class, with the Singleton functionality ready to prevent two GameModes in the same scene.

Now, it is time to replace the messages with something more interesting. For now, we will just change the current scene to a Win scene and Lose scene, which will only have a UI with a win and lose message and a button to play again. In the future, you can add a Main Menu scene and have an option to get back to it. Let's do that by doing the following:

  1. Create a new scene (File | New Scene) and save it, calling it WinScreen.
  2. Add a UI Text and center it with the text, You won!.
  3. Add a UI Button right below the text and change its text to Play Again:
    Figure 16.14 – WinScreen

    Figure 16.14 – WinScreen

  4. Select the Scene in the Project View and press Ctrl + D (Cmd + D on Mac) to duplicate the scene. Rename it LoseScreen.
  5. Double-click the LoseScreen scene to open it and just change the You won! text with a You lose! text.
  6. Go to File | Build Settings to open the Scenes in the Build list inside this window.

    The idea is that Unity needs you to explicitly declare all scenes that must be included in the game. You might have test scenes or scenes that you don't want to release yet, so that's why we need to do this. In our case, our game will have WinScreen, LoseScreen, and the scene we have created so far with the game scenario, which I called Game, so just drag those scenes from the Project View to the list of the Build Settings window; we will need this to make the Game Mode script change the scenes properly. Also, consider that the first scene in this list will be the first scene to be opened when we play the game in its final version (known as the build), so you may want to rearrange the list according to that:

    Figure 16.15 – Registering the scenes to be included in the build of the game

    Figure 16.15 – Registering the scenes to be included in the build of the game

  7. In WavesGameMode, add a using statement for the UnityEngine.SceneManagement namespace to enable the scene changing functions in this script.
  8. Replace the console print messages with calls to the SceneManager.LoadScene function, which will receive a string with the name of the scene to load; in this case, it would be WinScreen and LoseScreen. You just need the scene name, not the entire path to the file.

    If you want to chain different levels, you can create a public string field to allow you to specify via the Editor which scenes to load. Remember to have the scenes added to the Build Settings, if not, you will receive an error message in the console when you try to change the scenes:

    Figure 16.16 – Changing scenes with SceneManager

    Figure 16.16 – Changing scenes with SceneManager

  9. Play the game and check whether the scenes change properly.

    Important note

    Right now, we picked the simplest way to show whether we lost or won, but maybe in the future, you will want something more gentle than a sudden change of the scene, such as maybe waiting a few moments with Invoke to delay that change or directly show the winning message inside the game without changing the scenes. Consider that when testing the game with people and checking whether they understood what happens while they play, game feedback is important to keep the Player aware of what is happening and is not an easy task to tackle.

Now we have a fully functional simple game, with mechanics and win and lose conditions, and while this is enough to start developing other aspects of our game, I want to discuss some issues with our current Manager approach and how to solve them with events.

Improving our code with events

So far, we used Unity event functions to detect situations that can happen in the game such as Awake and Update. These functions are ways for Unity to communicate two components, as in the case of OnTriggerEnter, which is a way for the Rigidbody to inform other components in the GameObject that a collision has happened. In our case, we are using ifs inside Updates to detect changes on other components, such as GameMode checking whether the number of enemies reached 0. But we can improve this if we are informed by the Enemy Manager when something has changed, and just do the check-in that moment, such as with the Rigidbody telling us the collisions instead of checking collisions every frame.

Also, sometimes, we rely on Unity events to execute logic, such as the score being given in the OnDestroy event, which informs us when the Object is destroyed, but due to the nature of the event, it can be called in situations we don't want to add to the score, such as when the scene is changed or the game is closed. Objects are destroyed in those cases, but not because the Player killed the Enemy, leading to the score being raised when it shouldn't. In this case, it would be great to have an event that tells us that the Player's lives have reached 0 to execute this logic, instead of relying on the general-purpose destroy event.

The idea of events is to improve the model of communication between our Objects, being sure that in the exact moment something happens, the interested parts in that situation are notified to react accordingly. Unity has lots of events, but we can create specific ones to our gameplay logic. Let's start seeing this applied in the Score scenario we discussed earlier; the idea is to make the Life component to have an event to communicate other components that the Object was destroyed because its life reached 0.

There are several ways to implement this, and we will use a little bit of a different approach than the Awake and Update methods; we will use the UnityEvent field type. This is a field type capable of holding references to functions to be executed when we want to, like C# delegates, but with other benefits, such as better Unity Editor integration. To implement this, do the following:

  1. In the Life component, create a public field of the UnityEvent type called onDeath. This field will represent an event where other classes can subscribe to it to be aware of when Life reaches 0:
    Figure 16.17 – Creating a custom event field

    Figure 16.17 – Creating a custom event field

  2. If you save the script and go to the Editor, you can see the event in the Inspector. Unity Events support subscribing methods to them in the Editor so we can connect two Objects together. We will use this in the UI scripting chapter, so let's just ignore this for now:
    Figure 16.18 – UnityEvents showing up in the Inspector

    Figure 16.18 – UnityEvents showing up in the Inspector

    Important note

    You can use the generic delegate action or a custom delegate to create events instead of using UnityEvent, and aside from certain performance aspects, the only noticeable difference is that UnityEvent will show up in the Editor, as demonstrated in step 2.

  3. When life reaches 0, call the Invoke function of the event, and this way, we will be telling anyone interested in the event that it has happened:
    Figure 16.19 – Executing the event

    Figure 16.19 – Executing the event

  4. In ScoreOnDeath, rename the OnDestroy function to GivePoints or whatever name you prefer; the idea here is to stop giving points in the OnDestroy event.
  5. In the Awake function of the ScoreOnDeath script, get the Life component using GetComponent and save it in a local variable.
  6. Call the AddListener function of the onDeath field of the Life reference and pass the GivePoints function as the first argument. The idea is to tell Life to execute GivePoints when the onDeath event is invoked. This way, Life informs us about that situation. Remember that you don't need to call GivePoints, but just pass the function as a field:
    Figure 16.20 – Subscribing to the OnDeath event to give points in that scenario

    Figure 16.20 – Subscribing to the OnDeath event to give points in that scenario

    Important note

    Consider calling RemoveListener in OnDestroy; as usual, it is convenient to unsubscribe listeners when possible to prevent any memory leak (a reference preventing the GC from deallocating memory). In this scenario, it is not entirely necessary because both the Life and ScoreOnDeath components will be destroyed at the same time, but try to get used to that good practice.

  7. Save, select ScoreManager in the Editor, and hit play to test this. Try deleting an Enemy from the Hierarchy while in Play Mode to check how the score doesn't rise because the Enemy was destroyed for any other reason than its life becoming 0; you must destroy an Enemy by shooting at them to see the score being raised.

    Now that Life has an onDeath event, we can also replace the Player's Life check from the WavesGameMode to use the event by doing the following:

  8. Create an OnLifeChanged function on the WavesGameMode script and move the life checking condition from Update to this function.
  9. In Awake, subscribe to this new function to the onDeath event of the Player's Life component reference:
Figure 16.21 – Checking the lose condition with events

Figure 16.21 – Checking the lose condition with events

As you can see, creating custom events allows you to detect more specific situations other than the defaults in Unity, and keeps your code clean, without needing to constantly ask conditions in the Update function, which is not necessarily bad, but the event approach generates clearer code.

Remember that we can lose our game also by the Player's Base Life reaching 0, and we will explore the concept of the Player's base later in this book, but for now, let's create a cube that represents the Object that Enemies will attack to reduce the Base Life, like the Base Core. Taking this into account, I challenge you to add this other lose condition to our script. When you finish, you can check the solution in the following screenshot:

Figure 16.22 – Complete WavesGameMode lose condition

Figure 16.22 – Complete WavesGameMode lose condition

As you can see, we just repeated the life event subscription: remember to create an Object to represent the Player's Base damage point, add a Life script to it, and drag that one as the Player Base Life reference of WavesGameMode.

Now, let's keep illustrating this concept by applying it in the Managers to prevent the Game Mode from checking conditions every frame:

  1. Add an UnityEvent field to EnemyManager called onChanged. This event will be executed whenever an Enemy is added or removed from the list.
  2. Create two functions, AddEnemy and RemoveEnemy, both receiving a parameter of the Enemy type. The idea is that instead of Enemy adding and removing itself from the list directly, it should use these functions.
  3. Inside these two functions, invoke the onChanged event to inform others that the enemies list has been updated. The idea is that anyone who wants to add or remove enemies from the list needs to use these functions:
    Figure 16.23 – Calling events when enemies are added or removed

    Figure 16.23 – Calling events when enemies are added or removed

    Important note

    Here, we have the problem that nothing stops us from bypassing those two functions and using the list directly. You can solve that by making the list private and exposing it using the IReadOnlyList interface. Remember that this way, the list won't be visible in the Editor for debugging purposes.

  4. Change the Enemy script to use these functions:

    Figure 16.24 – Making the Enemy use the Add and Remove functions

    Figure 16.24 – Making the Enemy use the Add and Remove functions

  5. Repeat the same process for WaveManager and WaveSpawner, create an onChanged event, and create the AddWave and RemoveWave functions and call them in WaveSpawner instead of directly accessing the list. This way, we are sure the event is called when necessary as we did with EnemyManager. Try to solve this step by yourself and then check the solution in the following screenshot, starting with WavesManager:
    Figure 16.25 – Wave Manager On Changed event implementation

    Figure 16.25 – Wave Manager On Changed event implementation

    Also, WavesSpawner needs changes:

    Figure 16.26 – Implementing the AddWave and RemoveWave functions

    Figure 16.26 – Implementing the AddWave and RemoveWave functions

  6. In WavesGameMode, rename Update to CheckWinCondition and subscribe this function to the onChanged event of EnemyManager and the onChanged event of WavesManager. The idea is to check for the number of enemies and waves being changed only when it is necessary. Remember to do the subscription to the events in the Start function due to the Singletons being initialized in Awake:
Figure 16.27 – Checking the win condition when the enemies or waves amount is changed

Figure 16.27 – Checking the win condition when the enemies or waves amount is changed

Yes, this way, we have to write more code than before, and in terms of functionality, we didn't obtain anything new, but in bigger projects, managing conditions through Update checks will lead to different kinds of problems as previously discussed, such as race conditions and performances issues. Having a scalable code base sometimes requires more code, and this is one of those cases.

Before we finish, something to consider is that Unity events are not the only way to create this kind of event communication in Unity; you will find a similar approach called Action, the native C# version of Unity events, which I recommend you to look for if you want to see all of the options out there.

Summary

In this chapter, we finished an important part of the game, the ending, either by victory or by defeat. We discussed a simple but powerful way to separate the different layers of responsibilities by using Managers created through Singletons, to guarantee that there's not more than one instance of every kind of manager and simplifying the connections between them through static access (something to consider the day you discover code testing). Also, we visited the concept of events to streamline the communication between Objects to prevent problems and create more meaningful communication between Objects.

With this knowledge, you are now able not only to detect the victory and lose conditions of the game but to also do that in a better-structured way. These patterns can be useful to improve our game code in general, and I recommend you to try to apply it in other relevant scenarios.

In the next chapter, we are going to explore how to create visual and audio feedback to respond to our gameplay, combining scripting and the assets we integrated in Part 2 of this book.

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

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