Inheritance in UNITY using C#

A major staple of object-oriented programming is the concept of inheritance. Inheritance is when types of data (such as classes) adopt fields – such as variables and methods – from another data type.Say you have two classes that both share nearly all the same fields, but one has an extra field or an extra method that the other does not have. Coding each one separately is tedious. They behave in the same way for the most part. If you want to change something that both classes have, you need to make the change twice and make sure it’s consistent. It becomes even more of a problem if you have many classes that share most of their functionality. Inheritance is the solution to this. You can create a base class holding the functionality which both classes are meant to share, all in one place. Then, other classes inherit from the base class to automatically share its variables and methods without having to rewrite them. Everything stays declared in one place, keeping the functionality consistent across all the inheriting classes.

Fig. Inheritance in Unity

Inheritance in Action: RPG Items

A classic example of inheritance is the concept of items in a roleplaying game (RPG). Every item has certain fields:

  • An int for its worth, which is how many golden coins it is worth in the shop
  • A bool depicting whether it canBeSold at a shop
  • A string for the item name
  • A string for the item description
  • An int for the item weight

But then we have more specific types of items. For now, let’s say we have armor that goes in certain equipment slots and weapons that go in other equipment slots.To implement this, we use inheritance. A base class stores the members that all items have. It resembles any sort of item, so we just call it Item. Now we can create classes that inherit from Item but have more particular uses.Let’s say every piece of equipment, whether it’s a piece of armor or a weapon, has a durability value that wears down as the equipment is used. Armor loses durability as you take damage while wearing it, and weapons lose durability as you whack enemies with them.We create an inheriting class called Equipment. Since it inherits Item, it has all those fields we give to the Item class, like weight and worth and so on, but we also give it an int for current durability and another for maximum durability. The current durability will go down by a point every so many hits a piece of armor takes or a weapon gives. The equipment breaks when the current durability hits 0. Repairing it will raise the current durability back to the maximum durability so we can start wearing it down again.Now we make a class Weapon which inherits Equipment, adding fields like minimum and maximum damage, how fast the weapon attacks, what kind of weapon it is (an enum with options like axe, sword, hammer, knife, a particularly sharp stone, and whatever else), and perhaps a bool for whether it deals sharp damage or blunt damage, if we want to make some mechanical use of that in our game.We have a separate class named Armor, which inherits from Equipment as well. We add a field for how much defense the armor provides. We’ll also need an enum for the type – boots, girdle, gloves, chest, or helmet – which we use to dictate which equipment slot the armor is allowed in.If we wanted to also have a Consumable class for items that can be consumed, like drinking a potion or eating some food, we could have another class that inherits directly from Item. Ultimately, we end up with a hierarchy of classes looking like this, where classes that are indented further to the right are inheriting from the upper class:

Item
    Equipment        
    Armor        
    Weapon

ConsumableLet’s get the terminology down. Lower types, or subclasses, are more specific than their upper type, or superclass. Item is the superclass, and more specific versions of it, like Consumable or Equipment, are lower types of it. Yet more specific versions of Equipment are Weapon and Armor. This creates a hierarchy of classes, becoming more and more particular as we go down through the subclasses.

Declaring Our Classes

We can use the Item class we already declared as our base class. We’ll add a few members to it (weight and description) and leave it where it is, nested inside our good old MyScript class. We’ll also cut out any of the old code we had lying around, giving us a fresh start with no constructor and no methods:

public class MyScript:MonoBehaviour
{
    class Item
    {
        public string name = "Unnamed Item";
        public string description = "Undescribed item.";
        public int worth = 1;
        public bool canBeSold = true;
        public int weight = 0;
    }
}

This is our base class – the least specific kind of item. We declare our variables that all items have, and we give them default values. Later, we’ll be giving them constructors, so the default values shouldn’t be necessary because they should always be set by a constructor, but it won’t hurt to have them anyway.Let’s declare the class for Equipment – which will serve as the upper type for Weapon and Armor. We’ll put it in the same code block that Item is nested in, just under the Item class. Of course, this means it’s not a child of Item, but it’s a sibling – they’re both nested in the same block:

public class MyScript:MonoBehaviour
{
    class Item
    {
        public string name = "Unnamed Item";
        public string description = "Undescribed item.";
        public int worth = 1;
        public bool canBeSold = true;
        public int weight = 0;
    }
    class Equipment:Item
    {
        public int currentDurability = 100;
        public int maxDurability = 100;
    }
}

Notice the syntax is pretty much the same as any other class declaration, except that after “class Equipment” we have the colon “:’ to designate that our class will inherit another; then we provide the name of the class we want to inherit from, which is “Item”. That little part of the declaration is all we need to make Equipment inherit the members of Item.Next, let’s declare Armor. While we’re at it, let’s exhibit some of what we learned previously and declare an enum called ArmorType, which differentiates between the different kinds of armor (gloves, helmet, etc.). Place both of these definitions within the MyScript code block, just under the Equipment class:

enum ArmorType
{
    Helmet,
    Chest,
    Gloves,
    Girdle,
    Boots
}
class Armor:Equipment
{
    public ArmorType type = ArmorType.Helmet;
    public int defense = 1;
}

The enum declaration is pretty self-explanatory, and we’ve already gone over that previously.The Armor class inherits from Equipment, which in turn inherits from Item. This creates a chain. Ultimately, Armor inherits all the variables declared in Item, as well as those declared in Equipment. It adds its own members, one of which stores an instance of the ArmorType enum, which we default to Helmet, and an int for the defense rating of the armor, which we default to 1.Now we can declare our Weapon class. Again, put the code within the MyScript code block, beneath the Armor class:

enum WeaponType
{
    Sword,
    Axe,
    Hammer,
    Staff
}
class Weapon:Equipment
{
    public WeaponType type = WeaponType.Sword;
    public int minDamage = 1;
    public int maxDamage = 2;
    public float attackTime = .6f;
    public bool dealsBluntDamage = false;
}

This is the same deal as the Armor class. We declare a WeaponType enum with some basic weapon types in it. We then declare the Weapon class, which inherits Equipment and adds some of its own members.

Constructor Chaining

Now we need to add constructors to all these classes so we can neatly create instances that apply the proper values to all these members we’ve set up. You can probably imagine how painful it can be to declare a unique constructor applying the members for each class, considering each of our lower types will need to have the same code to apply values held in the upper types. We’d have to declare parameters and apply their values for each member declared in Item, like worth and name, in every single lower type as well.Luckily, we have a tool to make this easier, called constructor chaining. The constructors of lower types can “chain” into the upper type constructor, which can in turn chain into its next upper type, and so on. This chaining is pretty much just calling the upper constructor and supplying the parameters on the spot, right in the declaration of the constructor.Think about it like this: every constructor must declare the parameters of all its upper types, but that doesn’t mean we have to apply them all ourselves when we already have these upper constructors that could do it for us. So every constructor will declare all the necessary parameters, including the ones for members of its upper types. But any parameters that are not specific to that lower type will simply be “passed up” the constructor chain, letting the upper constructors handle their assignment.Let’s see it in action. First, let’s write our Item constructor. Write this code within the Item class code block:

public Item(string name,string description,int worth,bool canBeSold,int weight)
{
    this.name = name;
    this.description = description;
    this.worth = worth;
    this.canBeSold = canBeSold;
    this.weight = weight;
}

This is pretty much your average constructor definition, as we learned about before. Because the parameters are named exactly the same as the members we declared in Item, we use “this.” before the member name when applying the value.Now, let’s declare the Equipment constructor. Of course, it goes in the Equipment class:

public Equipment(string name,string description,int worth,bool canBeSold,int weight,int maxDurability):base(name,description,worth,canBeSold,weight)
{
    //Apply max durability:
    this.maxDurability = maxDurability;
    //Make current durability match max durability:
    currentDurability = maxDurability;
}

Now things are getting bulkier. The first thing you’ll probably notice is the “:base” coming after our parameter list. This is constructor chaining. The keyword “base” refers to the upper class, the one that we are inheriting from – in this case, it’s Item. We then have a set of parentheses afterward, which is pretty much equivalent to calling the upper constructor – the constructor of the base class Item.When we call the upper constructor, we pass in all the first parameters that this constructor declares, which are all the same as those declared in the upper constructor. These are the parameters that are not specific to Equipment. They belong to Item, so we give them to the Item constructor. We already declared that constructor to apply all those values earlier, after all – and as programmers, we’d hate to repeat ourselves.We also declare our own extra parameter in this constructor, “maxDurability”. This parameter is for a member that’s specific to Equipment, so we don’t pass it into the Item constructor. Item doesn’t concern itself with durability. We use this parameter in the body of our constructor, to apply the value.You’ll notice we only have maxDurability. We didn’t make a parameter for currentDurability. We apply the maxDurability parameter value first (“this.maxDurability = maxDurability”), and then we simply set our “currentDurability” to the given maxDurability value so the weapon starts out with maximum durability by default. Remember, currentDurability can be accessed simply by typing its name because it’s an instanced member of the class that our code is nested inside. And since we don’t have a parameter named “currentDurability”, we don’t need to type “this.” before it.If you want, you can mix some line breaks in wherever you want in the constructor declaration. It’ll still do the same thing. For example, you could put a line break before the “:base” to visually separate the constructor declaration from the base constructor call. It’s a style thing. If it makes reading it more comfortable for you, go ahead and do it.Now let’s make a chaining constructor for Armor:

public Armor(string name,string description,int worth,bool canBeSold,int weight,int maxDurability,ArmorType type,int defense):base(name,description,worth,canBeSold,weight,maxDurability)
{
    this.type = type;
    this.defense = defense;
}

This is the same concept as before. We have all the same parameters in the same order as the upper types provide them. We can just copy-paste those over. We then chain the constructor to pass all those parameters up the hierarchy. This time, since Equipment is involved, we also add maxDurability to the base constructor call. We have extra parameters for the two members associated with Armor (type and defense), and we apply them in the constructor body.To give you an overview of the way the parameters of each constructor are passed up the chain, here’s a look at each constructor with the new parameters in bold:

public Item(string name,string description,int worth,bool canBeSold,int weight)
    public Equipment(string name,string description,int worth,bool canBeSold,int weight,int maxDurability)
        public Armor(string name,string description,int worth,bool canBeSold,int weight,int maxDurability,ArmorType type,int defense)
        public Weapon(string name,string description,int worth,bool canBeSold,int weight,int maxDurability,WeaponType type,int minDamage,int maxDamage,float attackTime)

Any parameters which are being “passed up the chain” to be handled by the upper type’s constructor will be in normal text, while those which are handled by that specific constructor are bold. The indention also shows how the classes inherit from each other.Now the assigning of currentDurability is automatically handled for Armor and will be for Weapon too. If we weren’t chaining our constructors, we’d have to copy-paste all this code around, making a messy situation that’s dangerously easy to become inconsistent if we ever need to make a change.Moving on, let’s declare a Weapon constructor. You could practically do this one yourself at this point, although we will be doing a little special something with the “dealsBluntDamage” member:

public Weapon(string name,string description,int worth,bool canBeSold,int weight,int maxDurability,WeaponType type,int minDamage,int maxDamage,float attackTime):base(name,description,worth,canBeSold,weight,maxDurability)
{
    this.type = type;
    this.minDamage = minDamage;
    this.maxDamage = maxDamage;
    this.attackTime = attackTime;
    //Set dealsBluntDamage based on weapon type:
    if (type == WeaponType.Sword || type == WeaponType.Axe)
        dealsBluntDamage = false;
    else
        dealsBluntDamage = true;
}

This time, we declare a parameter for all the Weapon-specific members except for dealsBluntDamage. And again, we chain the constructor, just as we did with Armor, to pass up the parameters that aren’t specific to Weapon.Rather than specifying whether the weapon deals blunt or sharp damage by a parameter, we want to just automatically determine that based on the setting provided for the WeaponType. We use a simple “if” block which effectively reads “if the weapon type is Sword or the weapon type is Axe.” If so, we set dealsBluntDamage to false – the weapon deals sharp damage, or whatever you want to call it. If not, we know the weapon will be of type Hammer or Staff, so we set dealsBluntDamage to true.Okay, that’s the last of it. Now we have all our constructors and data set up for Item, Equipment, Armor, and Weapon.

Subtypes and Casting

When dealing with classes that inherit from each other, we often need to refer to them by an upper type when storing them and then figure out what lower type they are on the spot and react accordingly.For example, it’s fine for us to store our player’s equipped armor as Armor references and their weapon as a Weapon reference, but if they have an inventory full of items, it can store any sort of item, so it would look at them merely as instances of Item.When you do this, you can store any subtype of Item in those references and simply get it as an ambiguous pointer to an Item. You can access their Item-specific members like name, description, worth, and so on, but if you want to access members of a lower type like Weapon or Armor, you must first typecast the reference to the expected type.A typecast is how we tell the compiler what we expected a type to be. It can then look at a reference to some generic type, like Item, as a more specific type, like Weapon.Take this following code, where we create a Weapon. We provide some generic and unimportant values for its parameters, but the important part is that we store it in a local variable of type Item, not Weapon:

void Start()
{
    Item item = new Weapon("Rusty Axe", "A beat-up rusty axe.", 4, true, 8, 40, WeaponType.Axe, 4, 9, .6f);
}

If you’re wondering why we’ve written an “f” at the end of that last parameter value (“.6f”), we’ll get to that in a little bit.The reason we can store a Weapon in an Item variable is because Weapon is a lower type of Item. A Weapon is more specific but can still be summed up as an Item because it has all the same members, even if it has some extra ones tacked on as well. This wouldn’t be allowed the other way around – we can’t store an instance of Item or Equipment in a variable of type Weapon or Armor, for example.This is because when we reference a Weapon or Armor instance, we expect them to have all the members associated with those types. If they’re instead storing a less specific type, that’s just inviting unsavory errors. This is why our compiler won’t let us do it in the first place. Part of the reason we use strongly typed languages is to enforce these rules upon us, to keep our code good and clean and to stop us from doing things we probably shouldn’t even be doing in the first place.Now that we’ve stored our Weapon instance as an Item reference, let’s try getting it back to a Weapon reference and see what happens. We’ll add a line of code declaring a local variable of type Weapon, and we’ll assign the Item value to this variable:

void Start()
{
    Item item = new Weapon("Rusty Axe","A beat-up rusty axe.", 4, true, 8, 40, WeaponType.Axe, 4, 9, .6f);
    Weapon weapon = item;
}

Save and check Unity, and you’ll see an error saying this:Cannot implicitly convert type ‘MyScript.Item’ to ‘MyScript.Weapon’. An explicit conversion exists (are you missing a cast?)What we’re trying to do here is mentioned in the error message: implicitly converting a type.Converting types can be done implicitly or explicitly.The difference is simply in whether we, as the programmer, have manually ordered the conversion. In this case, we haven’t used any special syntax to tell the compiler “I want to convert this type to that other type.” So that makes this an implicit conversion, because if it’s going to happen, it’s going to happen without us necessarily telling it to.These errors sort of act as guards set up to prevent us from accidentally, unknowingly doing type conversions that maybe shouldn’t be done in the first place. The compiler doesn’t know if that Item stores a Weapon. We may know because we just declared it, but compilers aren’t in the habit of making assumptions, even when the context is just one line of code above the error.We must make an explicit conversion by casting the type. This is an on-the-spot conversion that happens at runtime (meaning while the game is playing).There are two ways to make this conversion. They each do the same thing but behave a little differently in the case where the types aren’t actually compatible as we’re expecting them to be.The first way is to write the name of the type you want to cast to, wrapped in a set of parentheses, right before the “item” reference:

Weapon weapon = (Weapon)item;

This method will throw an exception at runtime if the types aren’t compatible. Otherwise, it converts the given object to the type in the parentheses. The second method is with the “as” operator:

Weapon weapon = item as Weapon;

This method will not throw an exception if the types aren’t compatible. Instead, it simply returns null, which is the equivalent of a reference that points at nothing. If it succeeds, it returns the type we expect, which is Weapon. Of course, a failure, in this case, will usually result in an error afterward anyway, because you’re likely going to go ahead and use the “weapon” variable as if it’s actually storing a weapon. You’ll reach in and try to grab some data from it or run a method in it, and since it’s null, an error will be generated – just a different kind of error.

Number Value Types

I promised I’d explain what the “f” means when it’s placed at the end of a number value, for example, “.6f”. It’s known as a suffix. A suffix can be tacked onto the end of number values to denote what value type we want the number to be stored as. So far, we’ve learned of “int” and “float”, but there are a variety of different types which have differing limitations on how high or low a number they can store. Other number types exist which either store a larger range of numbers but take up more memory or store a lower range and take up less memory. For example, an “sbyte” is a much smaller “int”. Where “int” can store a value over 2 billion at the highest (and negative 2 billion at the lowest), an “sbyte” can store no lower than −128 and no higher than 127. As a result, an sbyte takes up less space on the computer, but it’s not going to be able to store a high enough value in many situations.On top of that, there are “unsigned” versions of data types which can’t store a negative value (their lowest value is 0), but as a result can store twice as high a positive value. For example, the “s” in “sbyte” means “signed.” An unsigned version of the same type is just “byte.” It stores a value between 0 and 255. There’s also an unsigned version of int: “uint”. It can go over 4 billion, but still can’t go under 0.Some of these types have a suffix you can use to easily write a number out as that type, like “f” to make a float. Some don’t, and you have to use an explicit conversion to make them a certain type, for example, “(byte)120” to make a byte instead of an int.By default, a value with no fraction will be an “int”, unless it stores a value outside the range of an int, in which case it goes up to the next largest type that can store the value. A value with a fraction will store a “double,” which is twice as large as a float, but generally more accurate with its fraction value. If a parameter expects a float value, but we pass in a number like .6, we’re really giving it a double. That will give us an error, so we tack that “f” suffix on the end to make it a float instead, fixing the error.For most purposes, “int” and “float” will serve you perfectly well, and pretty much all of the built-in methods in the Unity engine will expect one of those two. Should you ever need to use another value, however, Table 11-1 gives a rundown of the values for integer types (no decimal value) and their suffix, if any. Table 11-1Integer value data types and their associated suffixes

Data TypeValue RangeSuffix
sbyte–128 to 127
byte0 to 255
short–32,768 to 32,767
ushort0 to 65,535
int–2,147,483,648 to 2,147,483,647
uint0 to 4,294,967,295U
long–9,223,372,036,854,775,808 to –9,223,372,036,854,775,807L
ulong0 to 18,446,744,073,709,551,615UL

On top of that, there are three different value types we can use for number values with a decimal – also known as “floating point” values, which is what “float” is short for:

  • “float” uses the “f” or “F” suffix.
  • “double” is the default when no suffix is used, although you can also use “D” or “d”.
  • “decimal” uses the “m” or “M” suffix.

Doubles are called double because they’re double the size of a float. A decimal doubles the size yet again, making them four times the size of a float.To make a somewhat complicated topic short and sweet, floating point values are not totally accurate all the time, particularly when storing high values in them. You might set their value to something and then get the value back, and it’s slightly different, off by a little fraction. Data types which are larger than “float” can store larger values and remain more accurate throughout. Again, “float” will likely serve you just fine in most situations you’ll encounter.

Type Checking

In the test cases we’ve dealt with so far, it’s obvious to us what types we’re dealing with, so we don’t really have to worry about errors. But often, you’ll first need to verify whether the type of a reference is actually what you think it is before you interact with it.Say you’ve got a reference to an Item. For the sake of explanation without getting into a whole other set of problems, let’s just assume we have an Item reference spat at us by some code that manages our inventory, and we need to check what lower type it is to determine what sort of functionality it should have.There are several ways of doing this. Assuming we have a variable or parameter “item” of type Item, how do we check if it’s a Weapon or Armor?One method is with the “is” operator. This takes some value on its left-hand side and a direct reference to a type on its right-hand side. If the value is either exactly the same as the type or is a lower (more specific) type of it, the operator returns true. Otherwise, it returns false:

if (item is Weapon)
    Debug.Log("Item is a weapon.");
else if (item is Armor)
    Debug.Log("Item is an armor piece.");
else if (item is Equipment)
    Debug.Log("Item is some kind of equipment, but not Armor or Weapon.");

In this example, we log one message depending on whether the “item” is a Weapon or Armor, and if it’s neither, but is a piece of equipment, we have a generic message to log instead.Another method to check if the Item reference is a more specific type would be to use the “as” operator we demonstrated earlier to assign the item to a new variable of that more specific type and then simply do an “if” to test if the result is null. If it was null, then we know the item is not that type. If it was not null, the item was that type:

Weapon weapon = item as Weapon;
if (weapon == null)
{
    //Cast failed, 'item' is not a Weapon instance
}
else
{
    //Cast succeeded, we can proceed to use 'weapon' reference
}

Sometimes, you might want to check if the instance is exactly the type you’re comparing it with, not a more specific lower type. In this case, the code looks a little whacky compared to our other examples. You call the instance method “GetType” on your object to get a reference to its exact type. You can compare that type with the “==” operator to another to see if they exactly match. But when referring directly to a type like this, you must wrap the type name between the parentheses of “typeof(…)”, as shown in the following:

if (item.GetType() == typeof(Equipment))
    Debug.Log("Item type is exactly Equipment.");

When we declared “item”, we assigned a Weapon instance to it. Since we’re checking to see if the item is exactly Equipment, this will return false and the message won’t be logged, because while the Weapon is technically Equipment in that it’s a more specific, lower type, it’s not exactly Equipment.Using these three methods, you can cover pretty much any situation you might come across where you need to check a type.

Virtual Methods

One final aspect of inheritance is the concept of virtual methods. We’ll only get into the theory here, not the syntax. We won’t use virtual methods until further into the book, so we’ll wait until then to write them ourselves. For now, let’s learn what they are and what their purpose is, while we’ve still got inheritance on the brain.You can mark methods as virtual, which means that they can be overridden by lower types so that the lower type can tack on its own functionality or even completely overwrite the upper type’s functionality.An example of the purpose of this might be if we had a few extra classes for item types. Let’s say we had classes Consumable:Item and Food:Consumable.The Consumable class is meant to represent things like potions or other such items that can be “used” to consume them on the spot for some effect. It has a virtual method declared inside it called Use. This method takes a “target” parameter pointing at a specific entity in the game – a player or an NPC. When the player or an NPC uses a potion, they call Use and provide a reference to themselves as the target. The virtual method will determine what happens to the target.We can then make some classes like HealthPotion:Consumable and ManaPotion:Consumable. We override the virtual method Use, declaring a new version of it in each of these classes. Using the supplied “target” parameter, each implementation of the virtual method can do its own thing. The health potion will restore the target entity’s health, and the mana potion will restore their mana. They each provide their own definition of Use.We could then make a Food:Consumable which overrides Use to simply decrease the target’s hunger value by some member variable “float tastiness” specific to the Food class.Then, given a reference to a Consumable, we can simply call Use on any target, without concerning ourselves with what exact lower type the consumable is. The correct method override will be used automatically – the food will sate hunger, and the potions will restore health or mana based on their type.

Written by

XR Developer responsible for end-to-end development of XR solutions spanning multiple domains, by using various XR and WebXR libraries.

Leave a Reply