Revisiting Types, Methods, and Classes

Now that you've programmed the game 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 be 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:

  • Intermediate modifiers
  • Method overloading 
  • Using the out and ref parameters
  • Working with interfaces
  • Abstract classes and overriding
  • Extending class functionality
  • Namespace conflicts 
  • Type aliasing

Let's get started!

Access Modifier redux

While we've gotten into the habit of pairing the public and private access modifiers with our variable declarations, 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 OOP redux section:

  • const
  • readonly
  • static
  • abstract
  • override

Let's start with the first three access modifiers provided in the preceding list.

Constant and read-only properties

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. A good candidate for a constant value is our maxItems in the GameBehavior class:

public const int maxItems = 4; 

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:

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.

Using the static keyword 

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

Time for action  creating a static class

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:

  1. Create a new C# script in the Scripts folder and name it Utilities.
  2. Open it up and add the following code:
 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;
}
}
  1. Delete RestartLevel() from GameBehavior and modify the OnGUI() method with the following code:
  void OnGUI()
{
// ... No other changes needed ...

if (showWinScreen)
{
if (GUI.Button(new Rect(Screen.width/2 - 100,
Screen.height/2 - 50, 200, 100), "YOU WON!"))
{
// 5
Utilities.RestartLevel();
}
}

if(showLossScreen)
{
if (GUI.Button(new Rect(Screen.width / 2 - 100,
Screen.height / 2 - 50, 200, 100), "You lose..."))
{
Utilities.RestartLevel();
}
}
}

Let's break down the code:

  1. First, it adds the SceneManagement using directive so that we can access the LoadScene() method.
  2. Then, it declares Utilities as a public static class that does not inherit from MonoBehavior because we won't need it to be in the game scene.
  3. Next, it creates a public static variable to hold the number of times our player has died and restarted the game.
  4. After, it declares a public static method to hold our level restart logic, which is currently hardcoded in GameBehavior.
  5. Finally, it calls RestartLevel() from the static Utilities class when either the win or lose GUI 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 out of GameBehavior and put it into its static class, which makes it easier to reuse across our codebase. 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 means it's time to move on to methods and their intermediate capabilities, which includes method overloading and ref and out parameters.

Methods redux

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.

Overloading methods

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 offers added flexibility when you need more than one option for a given operation. 

The RestartLevel() method in GameBehavior 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 expanded 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.

Time for action  overloading the level restart

Let's add an overloaded version of RestartLevel():

  1. Open up 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;
}
}
  1. Open GameBehavior and update one of the calls to Utilities.RestartLevel() in the OnGUI() method to the following:
if (showWinScreen)
{
if (GUI.Button(new Rect(Screen.width/2 - 100,
Screen.height/2 - 50, 200, 100), "YOU WON!"))
{
// 4
Utilities.RestartLevel(0);
}
}

Let's break down the code:

  1. First, it declares an overloaded version of the RestartLevel() method that takes in an int parameter and returns a bool
  2. Then, it calls LoadScene() and passes in the sceneIndex parameter instead of manually hardcoding that value. 
  1. Next, it returns true after the new scene is loaded and the timeScale property has been reset.
  2. Finally, it calls the overloaded RestartLevel() method and passes in a sceneIndex of 0 when the win button is pressed. Overloaded methods are automatically detected by Visual Studio and are displayed by number, as shown here:

The functionality in the RestartLevel() method is now much more customizable and can account for additional situations you may need later.

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.

Ref 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. While this works for most cases, there are situations where you'll want to pass in a method argument by reference; 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:

  • Arguments have to be initialized before being passed into a method.
  • You don't need to initialize or assign the reference parameter value before ending the method.
  • Properties with get or set accessors can't be used as ref or out arguments.

Let's try this out by adding some logic to keep track of how many times a player has restarted the game.

Time for action  tracking player restarts

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()
{
SceneManager.LoadScene(0);
Time.timeScale = 1.0f;

// 3
Debug.Log("Player deaths: " + playerDeaths);
string message = UpdateDeathCount(ref playerDeaths);
Debug.Log("Player deaths: " + playerDeaths);
}

public static bool RestartLevel(int sceneIndex)
{
// ... No changes needed ...
}
}

Let's break down the code:

  1. First, it declares a new static method that returns a string and takes in an int passed by reference.
  2. Then, it updates the reference parameter directly, incrementing its value by 1 and returning a string that contains the new value.
  3. Finally, it debugs the playerDeaths variable in RestartLevel() before and after it is passed by reference to UpdateDeathCount().

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:

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.

Out parameters

The out keyword does the same job as ref but with different rules:

  • Arguments do not need to be initialized before being passed into a method.
  • The referenced parameter value does need to be initialized or assigned in the calling method before it's returned.

For instance, we could replace 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. 

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. 

OOP redux

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 blueprint and have them perform different actions based on a specific scenario?"

Interfaces

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, it's up to the adopting class or struct to fill in the values and methods outlined in the interface. The great part about interfaces is that both classes and structs can use them, and there's no upper limit regarding how many can be adopted by a single object. 

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 still have the freedom to be separate, but share common functionality.

Time for action  creating a manager interface

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 would hypothetically need to implement to share 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:

  1. First, it declares a public interface called IManager using the interface keyword.
  2. Then, it adds a 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.
  1. Finally, it defines a method named Initialize() with no return type for the adopting class to implement.

Your next task is to use the IManager interface, which means it needs to be adopted by another class.

Time for action  adopting an interface

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

// 4
void Start()
{
Initialize();
}

// 5
public void Initialize()
{
_state = "Manager initialized..";
Debug.Log(_state);
}

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

Let's break down the code:

  1. First, it declares that GameBehavior adopts the IManager interface using a comma and its name, just like with subclassing.
  2. Then, it adds a private variable that we'll use to back the public State value we have to implement from IManager.
  1. Next, it adds the public State variable declared in IManager and uses _state as its private backing variable.
  2. After that, it declares the Start() method and calls the Initialize() method.
  3. Finally, it declares the 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:

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. This opens up a whole world of possibilities for building classes, one of which is abstract classes. 

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. 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' default implementation. 

For example, let's take the IManager interface functionality we just wrote and turn it into an abstract base class instead:

 // 1
public abstract class BaseManager
{
// 2
protected string _state;
public abstract string state { get; set; }

// 3
public abstract void Initialize();
}

Let's break down the code:

  1. First, it declares a new class named BaseManager using the abstract keyword.
  2. Then, it creates two variables:
    • A protected string named _state that can only be accessed by classes that inherit from BaseManager
    • An abstract string named state with get and set accessors to be implemented by the subclass
  3. Finally, it adds Initialize() as an abstract method, also to be implemented in the subclass.

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 = "Manager initialized..";
Debug.Log(_state);
}
}

By breaking down the preceding code, we can see the following:

  1. First, it declares a new class called CombatManager that inherits from the BaseManager abstract class.
  2. Then, it adds the state variable implementation from BaseManager using the override keyword.
  3. Finally, it adds the 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 Lego-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.

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.

Class extensions

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:

  • All extension methods need to be marked as static.
  • The first parameter needs to be the this keyword, followed the name of the class we want to extend and a local variable name:
    • This special parameter lets the compiler identify the method as an extension, and gives us a local reference for the existing class.
    • Any class methods and properties can then be accessed through the local variable.
  • It's common to store extension methods inside a static class, which, in turn, is stored inside its namespace. This allows you to control what other scripts have access to your custom functionality.

Your next task is to put class extensions into practice by adding a new method to the built-in C# String class.

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

  1. First, it declares a namespace named CustomExtensions to hold all the extension classes and methods.
  2. Then, it declares a static class named StringExtensions for organizational purposes; each group of class extensions should follow this setup.
  1. Next, it adds a static method named FancyDebug to the StringExtensions class:
    • The first parameter, this string str, marks the method as an extension.
    • The str 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 literals.
  2. Finally, it prints out a debug message whenever FancyDebug is executed, using str.Length to reference the string variable that the method is called on.

Now that the extension is part of the String class, let's test it out.

Time for action  using an extension method

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 changes needed ...

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

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

// 2
_state.FancyDebug();

Debug.Log(_state);
}

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

Let's break down the code:

  1. First, it adds the CustomExtensions namespace with a using directive at the top of the file.
  2. Then, it calls 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 that FancyDebug() is called on, its length will be printed out properly, as shown here:

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 this book. In the next section, you'll learn the larger role that namespaces play in C# and how to create your type alias.

Namespace redux

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.

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.

Summary

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 of what you've learned in this chapter as all that there is to know on these concepts. Take them 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.

Pop quiz  leveling up

  1. Which keyword would mark a variable as unmodifiable?
  2. How would you create an overloaded version of a base method?
  3. What is the main difference between classes and interfaces?
  4. How would you solve a namespace conflict in one of your classes?
..................Content has been hidden....................

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