Now that you’ve programmed the game’s mechanics and interactions with Unity’s built-in classes, it’s time to expand our core C# knowledge and focus on the intermediate applications of the foundation we’ve laid. We’ll revisit old friends—variables, types, methods, and classes—but we’ll target their deeper applications and relevant use cases. Many of the topics we’ll be covering don’t apply to Hero Born in its current state, so some examples will be standalone rather than being applied directly to the game prototype.
I’ll be throwing a lot of new information your way, so if you feel overwhelmed at any point, don’t hesitate to revisit the first few chapters to solidify those building blocks. We’ll also be using this chapter to break away from gameplay mechanics and features specific to Unity by focusing on the following topics:
out
and ref
parametersLet’s get started!
While we’ve gotten into the habit of pairing the public and private access modifiers with our variable declarations, like we did with player health and items collected, there remains a laundry list of modifier keywords that we haven’t seen. We can’t go into detail about every one of them in this chapter, but the five that we’ll focus on will further your understanding of the C# language and give your programming skills a boost.
This section will cover the first three modifiers in the following list, while the remaining two will be discussed later on in the Intermediate OOP section:
const
readonly
static
abstract
override
You can find a full list of available modifiers at: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/modifiers.
Let’s start with the first three access modifiers provided in the preceding list.
There will be times when you need to create variables that store constant, unchanging values. Adding the const
keyword after a variable’s access modifier will do just that, but only for built-in C# types. For example, you couldn’t mark an instance of our Character
class as a constant. A good candidate for a constant value is MaxItems
in the GameBehavior
class:
public const int MaxItems = 4;
The above code would essentially lock the value of MaxItems
at 4
, making it unchangeable. The problem you’ll run into with constant variables is that they can only be assigned a value in their declaration, meaning we can’t leave MaxItems
without an initial value. As an alternative, we can use readonly
, which won’t let you write to the variable, meaning it can’t be changed:
public readonly int MaxItems;
Using the readonly
keyword to declare a variable will give us the same unmodifiable value as a constant, while still letting us assign its initial value at any time. A good place for this would be the Start()
or Awake()
methods in one of our scripts.
We’ve already gone over how objects, or instances, are created from a class blueprint, and that all properties and methods belong to that particular instance, like we had with our very first Character
class instance. While this is great for object-oriented functionality, not all classes need to be instantiated, and not all properties need to belong to a specific instance. However, static classes are sealed, meaning they cannot be used in class inheritance.
Utility methods are a good case for this situation, where we don’t necessarily care about instantiating a particular Utility
class instance since all its methods wouldn’t be dependent on a particular object. Your task is to create just such a utility method in a new script.
Let’s create a new class to hold some of our future methods that deal with raw computations or repeated logic that doesn’t depend on the gameplay:
Scripts
folder and name it Utilities
.using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 1
using UnityEngine.SceneManagement;
// 2
public static class Utilities
{
// 3
public static int PlayerDeaths = 0;
// 4
public static void RestartLevel()
{
SceneManager.LoadScene(0);
Time.timeScale = 1.0f;
}
}
RestartLevel()
from GameBehavior
and instead call the new utility
method with the following code:
// 5
public void RestartScene()
{
Utilities.RestartLevel();
}
Let’s break down the code:
using SceneManagement
directive so that we can access the LoadScene()
method.Utilities
as a public static
class that does not inherit from MonoBehavior
because we won’t need it to be in the game scene.static
variable to hold the number of times our player has died and restarted the game.static
method to hold our level restart logic, which is currently hardcoded in GameBehavior
.GameBehavior
calls RestartLevel()
from the static Utilities
class when the win or the lose button is pressed. Notice that we didn’t need an instance of the Utilities
class to call the method because it’s static—it’s just dot notation.We’ve now extracted the restart logic from GameBehavior
and put it into its static class, which makes it easier to reuse across our code base. Marking it as static
will also ensure that we never have to create or manage instances of the Utilities
class before we use its class members.
Non-static classes can have properties and methods that are static and non-static. However, if an entire class is marked as static, all properties and methods must follow suit.
That wraps up our second visit of variables and types, which will enable you to build out your own set of utilities and tools when managing larger and more complex projects down the road. Now it’s time to move on to methods and their intermediate capabilities, which includes method overloading and ref
and out
parameters.
Methods have been a big part of our code since we learned how to use them in Chapter 3, Diving into Variables, Types, and Methods, but there are two intermediate use cases we haven’t covered yet: method overloading and using the ref
and out
parameter keywords.
The term method overloading refers to creating multiple methods with the same name but with different signatures. A method’s signature is made up of its name and parameters, which is how the C# compiler recognizes it. Take the following method as an example:
public bool AttackEnemy(int damage) {}
The method signature of AttackEnemy()
is written as follows:
AttackEnemy(int)
Now that we know the signature of AttackEnemy()
, it can be overloaded by changing the number of parameters or the parameter types themselves, while still keeping its name. This provides added flexibility when you need more than one option for a given operation.
The RestartLevel()
method in Utilities
is a great example of a situation where method overloading comes in handy. Right now, RestartLevel()
only restarts the current level, but what happens if we expand the game so that it includes multiple scenes? We could refactor RestartLevel()
to accept parameters, but that often leads to bloated and confusing code.
The RestartLevel()
method is, once again, a good candidate for testing out our new knowledge. Your task is to overload it to take in different parameters.
Let’s add an overloaded version of RestartLevel()
:
Utilities
and add the following code:
public static class Utilities
{
public static int PlayerDeaths = 0;
public static void RestartLevel()
{
SceneManager.LoadScene(0);
Time.timeScale = 1.0f;
}
// 1
public static bool RestartLevel(int sceneIndex)
{
// 2
SceneManager.LoadScene(sceneIndex);
Time.timeScale = 1.0f;
// 3
return true;
}
}
GameBehavior
and update the call to the Utilities.RestartLevel()
method to the following:
// 4
public void RestartScene()
{
Utilities.RestartLevel(0);
}
RestartLevel()
method that takes in an int
parameter and returns a bool
.LoadScene()
and passes in the sceneIndex
parameter instead of manually hardcoding that value.true
after the new scene is loaded and the timeScale
property has been reset.GameBehavior
calls the overloaded RestartLevel()
method and passes in 0
as the sceneIndex
. Overloaded methods are automatically detected by Visual Studio and are displayed by number, as shown here:
Figure 10.1: Multiple method overloads in Visual Studio
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.
The functionality in the RestartLevel()
method is now much more customizable and can account for additional situations you may need later. In this case, it is restarting the game from any scene we choose.
Method overloading is not limited to static methods—this was just in line with the previous example. Any method can be overloaded as long as its signature differs from the original.
Next up, we’re going to cover two additional topics that can take your method game to a whole new level—ref
and out
parameters.
When we talked about classes and structs back in Chapter 5, Working with Classes, Structs, and OOP, we discovered that not all objects are passed the same way: value types are passed by copy, while reference types are passed by reference. However, we didn’t go over how objects, or values, are used when they’re passed into methods as parameter arguments.
By default, all arguments are passed by value, meaning that a variable passed into a method will not be affected by any changes that are made to its value inside the method body. This protects us from making unwanted changes to existing variables when we use them as method parameters. While this works for most cases, there are situations where you’ll want to pass in a method argument by reference so that it can be updated and have that change reflected in the original variable. Prefixing a parameter declaration with either the ref
or out
keyword will mark the argument as a reference.
Here are a few key points to keep in mind about using the ref
keyword:
ref
or out
argumentsLet’s try this out by adding some logic to keep track of how many times a player has restarted the game.
Let’s create a method to update PlayerDeaths
to see the method arguments that are being passed by reference in action.
Open up Utilities
and add the following code:
public static class Utilities
{
public static int PlayerDeaths = 0;
// 1
public static string UpdateDeathCount(ref int countReference)
{
// 2
countReference += 1;
return "Next time you'll be at number " + countReference;
}
public static void RestartLevel()
{
// ... No changes needed ...
}
public static bool RestartLevel(int sceneIndex)
{
// 3
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;
}
}
Let’s break down the code:
static
method that returns a string
and takes in an int
passed by reference.1
and returning a string that contains the new value.PlayerDeaths
variable in RestartLevel(int sceneIndex)
before and after it is passed by reference to UpdateDeathCount()
. We also store a reference to the returned string value from UpdateDeathCount()
in the message
variable and print it out.If you play the game and lose, the debug log will show that PlayerDeaths
has increased by 1 inside UpdateDeathCount()
because it was passed by reference and not by value:
Figure 10.2: Example output from ref parameters
For clarity, we could have updated the player death count without a ref
parameter because UpdateDeathCount()
and PlayerDeaths
are in the same script. However, if this wasn’t the case and you wanted the same functionality, ref
parameters are super useful.
We’re using the ref
keyword in this situation for the sake of our example, but we could have also updated PlayerDeaths
directly inside UpdateDeathCount()
or added logic inside RestartLevel()
to only fire UpdateDeathCount()
when the restart was due to a loss.
Now that we know how to use a ref
parameter in our project, let’s take a look at the out
parameter and how it serves a slightly different purpose.
The out
keyword does the same job as ref
but with different rules, which means they’re similar tools but they’re not interchangeable—each has its own use cases:
For instance, we could have replaced ref
with out
in UpdateDeathCount()
as long as we initialized or assigned the countReference
parameter before returning from the method:
public static string UpdateDeathCount(out int countReference)
{
countReference += 1;
return "Next time you'll be at number " + countReference;
}
Methods that use the out
keyword are better suited to situations where you need to return multiple values from a single function, while the ref
keyword works best when a reference value only needs to be modified. It’s also more flexible than the ref
keyword because the initial parameter values don’t need to be set before they’re used in the method. The out
keyword is especially useful if you need to initialize the parameter value before you change it. Even though these keywords are a little more esoteric, it’s important to have them in your C# toolkit for special use cases.
With these new method features under our belts, it’s time to revisit the big one: object-oriented programming (OOP). There’s so much to this topic that it’s impossible to cover everything in a chapter or two, but there are a few key tools that will come in handy early on in your development career. OOP is one of those topics that you’re encouraged to follow up on after finishing this book.
An object-oriented mindset is crucial to creating meaningful applications and understanding how the C# language works behind the scenes. The tricky part is that classes and structs by themselves aren’t the end of the line when it comes to OOP and designing your objects. They’ll always be the building blocks of your code, but classes are limited to single inheritance, meaning they can only ever have one parent or superclass, and structs can’t inherit at all. So, the question you should be asking yourself right about now is simple: “How can I create objects from the same template and have them perform different actions based on a specific scenario?”
To answer this question, we’ll be learning about interfaces, abstract classes, and class extensions.
One of the ways to gather groups of functionality together is through interfaces. Like classes, interfaces are blueprints for data and behaviors, but with one important difference: they can’t have any actual implementation logic or stored values. Instead, they contain the implementation blueprint, and it’s up to the adopting class or struct to fill in the values and methods outlined in the interface.
You can use interfaces with both classes and structs, and there’s no upper limit to how many interfaces a single class or struct can adopt.
Remember, a single class can only have one parent class, and structs can’t subclass at all. Breaking out functionality into interfaces lets you build up classes like building blocks, picking and choosing how you want them to behave like food from a menu. This would be a huge efficiency boost to your code base, breaking away from long, messy subclassing hierarchies.
For example, what if we wanted our enemies to be able to shoot back at our player when they’re in close range? We could create a parent class that both the player and enemy could derive from, which would base them both on the same blueprint. The problem with that approach, however, is that enemies and players won’t necessarily share the same behaviors and data.
The more efficient way to handle this would be to define an interface with a blueprint for what shootable objects need to do, and then have both the enemy and player adopt it. That way, they have the freedom to be separate and exhibit different behaviors while still sharing common functionality.
Refactoring the shooting mechanic into an interface is a challenge I’ll leave to you, but we still need to know how to create and adopt interfaces in code. For this example, we’ll create an interface that all manager scripts might need to implement for sharing a common structure.
Create a new C# script in the Scripts
folder, name it IManager
, and update its code as follows:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 1
public interface IManager
{
// 2
string State { get; set; }
// 3
void Initialize();
}
Let’s break down the code:
IManager
using the interface
keyword.string
variable to IManager
named State
with get
and set
accessors to hold the current state of the adopting class.
All interface properties need at least a get
accessor to compile but can have both get
and set
accessors if necessary.
Initialize()
with no return type for the adopting class to implement. However, you could absolutely have a return type for a method inside an interface; there’s no rule against it.You’ve now created a blueprint for all manager scripts, meaning that each manager script adopting this interface needs to have a state property and an initialize method. Your next task is to use the IManager
interface, which means it needs to be adopted by another class.
To keep things simple, let’s have the game manager adopt our new interface and implement its blueprint.
Update GameBehavior
with the following code:
// 1
public class GameBehavior : MonoBehaviour, IManager
{
// 2
private string _state;
// 3
public string State
{
get { return _state; }
set { _state = value; }
}
// ... No other changes needed ...
void Start()
{
ItemText.text += _itemsCollected;
HealthText.text += _playerHP;
// 4
Initialize();
}
// 5
public void Initialize()
{
_state = "Game Manager initialized..";
Debug.Log(_state);
}
}
GameBehavior
adopts the IManager
interface using a comma and its name, just like with subclassing.State
value we have to implement from IManager
.State
variable declared in IManager
and uses _state
as its private backing variable.Initialize()
method inside the Start()
method.Initialize()
method declared in IManager
with an implementation that sets and prints out the public State
variable.With this, we specified that GameBehavior
adopts the IManager
interface and implemented its State
and Initialize()
members, as shown here:
Figure 10.3: Example output from an interface
The great part of this is that the implementation is specific to GameBehavior
; if we had another manager class, we could do the same thing but with different logic. Just for fun, let’s set up a new manager script to test this out:
DataManager
.IManager
interface:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DataManager : MonoBehaviour, IManager
{
private string _state;
public string State
{
get { return _state; }
set { _state = value; }
}
void Start()
{
Initialize();
}
public void Initialize()
{
_state = "Data Manager initialized..";
Debug.Log(_state);
}
}
Figure 10.4: Data Manager script attached to a GameObject
Figure 10.5: Output from Data Manager initialization
While we could have done all of this with subclassing, we’d be limited to one parent class for all our managers. Instead, we have the option of adding new interfaces if we choose. We’ll revisit this new manager script in Chapter 12, Saving, Loading, and Serializing Data. This opens up a whole world of possibilities for building classes, one of which is a new OOP concept called abstract classes.
Another approach to separating common blueprints and sharing them between objects is the abstract class. Like interfaces, abstract classes cannot include any implementation logic for their methods; they can, however, store variable values. This is one of the key differences from interfaces—in situations where you might need to set initial values, an abstract class would be the way to go.
Any class that subclasses from an abstract class must fully implement all variables and methods marked with the abstract
keyword. They can be particularly useful in situations where you want to use class inheritance without having to write out a base class’s default implementation.
For example, let’s take the IManager
interface functionality we just wrote and see what it would look like as an abstract base class. Don’t change any of the actual code in our project, as we still want to keep things working as they are:
// 1
public abstract class BaseManager
{
// 2
protected string _state = "Manager is not initialized...";
public abstract string State { get; set; }
// 3
public abstract void Initialize();
}
Let’s break down the code:
BaseManager
using the abstract
keyword.protected string
named _state
that can only be accessed by classes that inherit from BaseManager
. We’ve also set an initial value for _state
, something we couldn’t do in our interface.
We also have an abstract string named State
with get
and set
accessors to be implemented by the subclass
Initialize()
as an abstract
method, also to be implemented in the subclass.In doing so, we have created an abstract class that does the same thing as an interface. In this setup, BaseManager
has the same blueprint as IManager
, allowing any subclasses to define their implementations of state
and Initialize()
using the override
keyword:
// 1
public class CombatManager: BaseManager
{
// 2
public override string State
{
get { return _state; }
set { _state = value; }
}
// 3
public override void Initialize()
{
_state = "Combat Manager initialized..";
Debug.Log(_state);
}
}
If we break down the preceding code, we can see the following:
CombatManager
that inherits from the BaseManager
abstract class.State
variable implementation from BaseManager
using the override
keyword.Initialize()
method implementation from BaseManager
using the override
keyword again and sets the protected _state
variable.Even though this is only the tip of the iceberg of interfaces and abstract classes, their possibilities should be jumping around in your programming brain. Interfaces will allow you to spread and share pieces of functionality between unrelated objects, leading to a building block-like assembly when it comes to your code.
Abstract classes, on the other hand, will let you keep the single-inheritance structure of OOP while separating a class’s implementation from its blueprint. These approaches can even be mixed and matched, as abstract classes can adopt interfaces just like non-abstract ones.
As always with complicated topics, your first stop should be the documentation. Check it out at: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract and: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface.
You won’t always need to build a new class from scratch. Sometimes, it’s enough to add the feature or logic you want to an existing class, which is called a class extension.
Let’s step away from custom objects and talk about how we can extend existing classes so that they fit our own needs. The idea behind class extensions is simple: take an existing built-in C# class and add on any functionality that you need it to have. Since we don’t have access to the underlying code that C# is built on, this is the only way to get custom behavior out of objects the language already has.
Classes can only be modified with methods—no variables or other entities are allowed. However limiting this might be, it makes the syntax consistent:
public static returnType MethodName(this ExtendingClass localVal) {}
Extension methods are declared using the same syntax as normal methods, but with a few caveats:
static
.this
keyword, followed by the name of the class we want to extend and a local variable name:Your next task is to put class extensions into practice by adding a new method to the built-in C# String
class.
Let’s take a look at extensions in practice by adding a custom method to the String
class.
Create a new C# script in the Scripts
folder, name it CustomExtensions
, and add the following code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 1
namespace CustomExtensions
{
// 2
public static class StringExtensions
{
// 3
public static void FancyDebug(this string str)
{
// 4
Debug.LogFormat("This string contains {0} characters.", str.Length);
}
}
}
Let’s break down the code:
CustomExtensions
to hold all the extension classes and methods.static
class named StringExtensions
for organizational purposes; each group of class extensions should follow this setup.static
method named FancyDebug
to the StringExtensions
class:this string str
, marks the method as an extensionstr
parameter will hold a reference to the actual text value that FancyDebug()
is called from; we can operate on str
inside the method body as a stand-in for all string literalsFancyDebug
is executed, using str.Length
to reference the string variable that the method is called on.In practice, this will let you add any of your own custom functionality to existing C# classes or even your own custom ones. Now that the extension is part of the String
class, let’s test it out. To use our new custom string method, we’ll need to include it in whatever class we want to have access to it.
Open up GameBehavior
and update the class with the following code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 1
using CustomExtensions;
public class GameBehavior : MonoBehaviour, IManager
{
// ... No other changes needed ...
private string _state;
public string State
{
get { return _state; }
set { _state = value; }
}
void Start()
{
// ... No changes needed ...
}
public void Initialize()
{
_state = "Game Manager initialized..";
// 2
_state.FancyDebug();
Debug.Log(_state);
}
}
Let’s break down the code:
CustomExtensions
namespace with a using
directive at the top of the file.FancyDebug
on the _state
string variable with dot notation inside Initialize()
to print out the number of individual characters its value has.Extending the entire string
class with FancyDebug()
means that any string variable has access to it. Since the first extension method parameter has a reference to whatever string
value FancyDebug()
is called on, its length will be printed out properly, as shown here:
Figure 10.6: Example output from custom extension
A custom class can also be extended using the same syntax, but it’s more common to just add extra functionality directly into the class if it’s one you control.
The last topic we’ll explore in this chapter is namespaces, which we briefly learned about earlier in the book. In the next section, you’ll learn about the larger role that namespaces play in C# and how to create your type alias.
As your applications get more complicated, you’ll start to section off your code into namespaces, ensuring that you have control over where and when it’s accessed. You’ll also use third-party software tools and plugins to save on time implementing a feature from the ground up that someone else has already made available. Both of these scenarios show that you’re progressing with your programming knowledge, but they can also cause namespace conflicts.
Namespace conflicts happen when there are two or more classes or types with the same name, which happens more than you’d think.
Good naming habits tend to produce similar results, and before you know it, you’re dealing with multiple classes named Error
or Extension
, and Visual Studio is throwing out errors. Luckily, C# has a simple solution to these situations: type aliasing.
Defining a type alias lets you explicitly choose which conflicting type you want to use in a given class, or create a more user-friendly name for a long-winded existing one. Type aliases are added at the top of the class file with a using
directive, followed by the alias name and the assigned type:
using AliasName = type;
For instance, if we wanted to create a type alias to refer to the existing Int64
type, we could say the following:
using CustomInt = System.Int64;
Now that CustomInt
is a type alias for the System.Int64
type, the compiler will treat it as an Int64
, letting us use it like any other type:
public CustomInt PlayerHealth = 100;
You can use type aliasing with your custom types, or existing ones with the same syntax, as long as they’re declared at the top of script files with the other using
directives.
For more information on the using
keyword and type aliasing, check out the C# documentation at: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive.
With new modifiers, method overloading, class extensions, and object-oriented skills under our belts, we are only one step away from the end of our C# journey. Remember, these intermediate topics are intended to get you thinking about more complex applications of the knowledge you’ve been gathering throughout this book; don’t think that what you’ve learned in this chapter is all that there is to know about these concepts. Take it as a starting point and continue from there.
In the next chapter, we’ll discuss the basics of generic programming, get a little hands-on experience with delegates and events, and wrap up with an overview of exception handling.
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.
3.12.148.117