Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Last OOPsie we looked at creating classes and inheritance. Now we look at how to customize our inherited classes to make them something completely new (and completely undead!).
In the last OOPsie entry we looked into the basics behind object-oriented programming (OOP) and learned quite a bit about classes, objects, and inheritance. The big question that defined inheritance was: 'How can we reuse code to give a handful of objects similar details?' The example we used was a series of cars, all of which shared similar details like having 4 wheels and being able to start up. And, while we customized the color of the Ferrari car class using 2 objects, both performed the same. Upon starting up and honking the horn they both sounded the same. This time around we'll append to our question that defines inheritance: 'How can we reuse code to give a handful of objects similar details while keeping their individuality?'
Individuality
First, before we continue, we should define individuality in the context of this article. Surprisingly, I'm not referring to your awkward years in high school when I mention individuality. There are two types of individuality we need to look at, class individuality and object individuality. In this context, class individuality will refer to different classes (remember classes are like blueprints) that have their own unique features. Even though 2 classes inherit from a base class, there's a great chance they inherit in order to provide their own unique details (in fact, this always should be the case) even though they both have similiar functionality. For the simple reason that it makes little to no sense to inherit from a base class only to provide the same functionality, we will assume every class mentioned herein is somehow unique and promotes the idea of class individuality.
On the other hand, object individuality will refer to differences between objects. For instance, if we had two House objects and each was painted differently, each house would have object individuality.
Quick side note: The Ferrari objects mentioned in the last OOPsie were NOT class individualistic but were object individualistic. The two Ferrari car objects had different color data stored but the Ferrari class had no unique functionality compared to Car (which it inherited from).
Is-A Relationships
Remember from last OOPsie, we said inheritance provides a sub class with a is-a relationship to the parent. For example, a Ferrari is-a Car (this is how we modelled our classes). However, notice that every Car is not a Ferrari. This is an extremely fundamental piece to the abstract idea of inheritance and I should have mentioned it in the first article, but as we'll see, this one-way relationship will be a big focus in a later portion of this article.
While on the topic of relationships, especially when it comes to this article, let's take a very brief glance at object relationships and equality. With objects, since we are instantiating (creating) objects off a blueprint, we could end up with multiple objects with all the same properties. Like we learned last article, we can customize our object by passing in arguments to a constructor. So, if we create a two green Ferrari's, we can assume they are equal because they share the same properties (even though they are, in fact, two actually seperate entities and are stored in seperate memory locations). In this case, the two Ferrari's are individualistic against all other objects that don't share all the same properties (such as a Red or Yellow Ferrari) but are not individualistic compared to each other and can be considered equal.
Function Overriding with Inheritance
One of the awesome key features we haven't discussed regarding to inheritance thusfar has been the concept of function overriding (sometimes referred to as a virtual function). Function overriding allows subclasses (classes that inherit from another class) to define it's own implementation of a function defined in the base class. This will come in much more useful later when we get into Polymorphism, but lets look at an example using a screen cap from Dead Rising 2.
Dead Rising 2
In the above picture, we find our main character (in the IJIEK Racing jacket) up against a horde of zombie enemies. The three zombies on the right are all unique, we have a male zombie with a plaid shirt, a woman zombie, and a male zombie with a brown shirt (left to right). For our example, lets reference them as 'Plaid-Zombie', 'Woman-Zombie', and 'Brown-Shirt-Zombie' (note: these are just used to reference them in the article, these are not classes or class names).
Now, let's also assume each of these zombies moves differently based on their type, let's have the Plaid-Zombie walk slowly, Woman-Zombie stumble around, and Brown-Shirt-Zombie yell and run. To recap:
Plaid-Zombie should walk slowly
Woman-Zombie should stumble around
Brown-Shirt-Zombie should yell and run
With these game requirements, lets write some basic inherited classes based on what we've already learned with inheritance:
public class Zombie
{
private String zombieType = "Regular-Zombie";
public Zombie(String zombieTypeToCreate)
{
zombieType = zombieTypeToCreate;
}
public String ZombieType
{
get { return zombieType; }
}
public String MovementType()
{
return "Jump and sing!";
}
}
public class PlaidZombie : Zombie
{
public PlaidZombie
: base ("Plaid-Zombie")
{
}
}
public class WomanZombie : Zombie
{
public WomanZombie
: base ("Woman-Zombie")
{
}
}
public class BrownShirtZombie : Zombie
{
public BrownShirtZombie
: base ("Brown-Shirt-Zombie")
{
}
}
public class DeadRisingGame
{
static void Main(string[] args)
{
//Create 3 new zombies
PlaidZombie plaid = new PlaidZombie();
WomanZombie woman = new WomanZombie();
BrownShirtZombie brownShirt = new BrownShirtZombie();
//Print some statements
Console.WriteLine("Type: " + plaid.ZombieType);
Console.WriteLine("Movement Type: " + plaid.MovementType());
Console.WriteLine("Type: " + woman.ZombieType);
Console.WriteLine("Movement Type: " + woman.MovementType());
Console.WriteLine("Type: " + brownShirt.ZombieType);
Console.WriteLine("Movement Type: " + brownShirt.MovementType());
}
}
OUTPUT:
Type: Plaid-Zombie
Movement Type: Jump and sing!
Type: Woman-Zombie
Movement Type: Jump and sing!
Type: Brown-Shirt-Zombie
Movement Type: Jump and sing!
This example has a simple base Zombie class that stores a zombie type string and allows us to call it. It also provides a function to retrieve how the zombie moves. If we created a regular zombie it would be of Type: Regular-Zombie and should have Movement Type: Jump and sing!
We then created 3 classes, one to define each type of zombie. Even though I mentioned this was a bad thing (creating all these inherited classes for a single property change), it'll allow us to transfer to the fixed version of this logic much more simply and you'll see why we want seperate zombie type classes.
Finally in the Main() method we simply create a zombie of each type and print out all it's details. But wait, look at that output, all the zombies move the same! We need to change that, our requirements told us they all have different movement styles. Feast your eyes on the corrected code with function overriding:
public class Zombie
{
private String zombieType = "Regular-Zombie";
public Zombie(String zombieTypeToCreate)
{
zombieType = zombieTypeToCreate;
}
public String ZombieType
{
get { return zombieType; }
}
public virtual String MovementType()
{
return "Jump and sing!";
}
}
public class PlaidZombie : Zombie
{
public PlaidZombie
: base ("Plaid-Zombie")
{
}
public override String MovementType()
{
return "Walk slowly";
}
}
public class WomanZombie : Zombie
{
public WomanZombie
: base ("Woman-Zombie")
{
}
public override String MovementType()
{
return "Stumble around";
}
}
public class BrownShirtZombie : Zombie
{
public BrownShirtZombie
: base ("Brown-Shirt-Zombie")
{
}
public override String MovementType()
{
return "Yell and run";
}
}
public class DeadRisingGame
{
static void Main(string[] args)
{
//Create 3 new zombies
PlaidZombie plaid = new PlaidZombie();
WomanZombie woman = new WomanZombie();
BrownShirtZombie brownShirt = new BrownShirtZombie();
//Print some statements
Console.WriteLine("Type: " + plaid.ZombieType);
Console.WriteLine("Movement Type: " + plaid.MovementType());
Console.WriteLine("Type: " + woman.ZombieType);
Console.WriteLine("Movement Type: " + woman.MovementType());
Console.WriteLine("Type: " + brownShirt.ZombieType);
Console.WriteLine("Movement Type: " + brownShirt.MovementType());
}
}
OUTPUT:
Type: Plaid-Zombie
Movement Type: Walk slowly
Type: Woman-Zombie
Movement Type: Stumble around
Type: Brown-Shirt-Zombie
Movement Type: Yell and run
Success! Now our output displays the correct zombie type and the correct related movement type. This is done using the virtual keyword in the base classes MovementType() function definition. The virtual keyword (typically) means any inheriting subclasses can define their own implementation of that function. I say typically because there are some instances (like C++'s virtual destructor; C# automatically assumes all destructors are virtual) where the virtual keyword actually has a somewhat differing performance compared to what I've shown. In any case, don't worry about other differing usages of the virtual keyword right now.
After giving the subclasses the ability to override we need to take advantage of this by defining the function in our subclass with the override keyword and writing our own implementation (which we've done above by returning a different string for each zombie).
Quick side note: The base keyword allows you to access a parent class's data from in a subclass. Thus, you can write virtual functions that get overridden with implementations that call base will run the code in the virtual function. For example, we could have the Brown-Shirt-Zombie have a movement type of 'Yell, run, Jump and sing!' by changing the overridden function to the following:
public override String MovementType()
{
return "Yell, run, " + base.MovementType();
}
Polymorphism
Finally we've hit a point where we can begin talking about the idea that sparked the first OOPsie, polymorphism. Polymorphism is the practice of grouping objects using a similar, shared type. The key to polymorphism is the is-a relationship, and some of the best uses we can get out of it come from function overriding (also, interfaces and abstraction are great helps when using polymorphism, but that's another article for another time).
Let's continue with the undead example above. Remember, each of the zombie subclasses inherit from the Zombie base class. So, PlaidZombie is-a Zombie, WomanZombie is-a Zombie, and BrownShirtZombie is-a Zombie. Also remember, before we specifically called every ZombieType property and MovementType() function on each specific type of zombie. So essentially we were calling the following methods: PlaidZombie.ZombieType, PlaidZombie.MovementType(), WomanZombie.ZombieType, WomanZombie.MovementType(), BrownShirtZombie.ZombieType, and BrownShirtZombie.MovementType().
Whew, that was a mouthful. Now imagine having hundreds, or even thousands of zombies on screen at once. You could have up to 2,000 lines of code just to write each line!
Dead Rising
Our solution comes in the form of polymorphism, which I've done a poor job of really explaining so far, so let's just look at a refactored example of the above code (make sure to look at the Main() function):
public class Zombie
{
private String zombieType = "Regular-Zombie";
public Zombie(String zombieTypeToCreate)
{
zombieType = zombieTypeToCreate;
}
public String ZombieType
{
get { return zombieType; }
}
public virtual String MovementType()
{
return "Jump and sing!";
}
}
public class PlaidZombie : Zombie
{
public PlaidZombie
: base ("Plaid-Zombie")
{
}
public override String MovementType()
{
return "Walk slowly";
}
}
public class WomanZombie : Zombie
{
public WomanZombie
: base ("Woman-Zombie")
{
}
public override String MovementType()
{
return "Stumble around";
}
}
public class BrownShirtZombie : Zombie
{
public BrownShirtZombie
: base ("Brown-Shirt-Zombie")
{
}
public override String MovementType()
{
return "Yell and run";
}
}
public class DeadRisingGame
{
static void Main(string[] args)
{
List zombies = new List<Zombie>();
//Create 3 new zombies
zombies.Add(new PlaidZombie());
zombies.Add(new WomanZombie());
zombies.Add(new BrownShirtZombie());
//Print some statements
foreach(Zombie zomb in zombies)
{
Console.WriteLine("Type: " + zomb.ZombieType);
Console.WriteLine("Movement Type: " + zomb.MovementType());
}
}
}
OUTPUT:
Type: Plaid-Zombie
Movement Type: Walk slowly
Type: Woman-Zombie
Movement Type: Stumble around
Type: Brown-Shirt-Zombie
Movement Type: Yell and run
Brilliant! The only code changes have been in Main() and showcase polymorphism 101. We have a list of objects of the Zombie type. And remember, since PlaidZombie, WomanZombie, and BrownShirtZombie are-all Zombies, they can be stored in a list where the objects within have the type of Zombie. Think of each subclass being able to 'morph' into it's parent type (hence poly-morph-ism).
As with other object-oriented ideas, polymorphism is relatable to real life. Assume we have 2 boxes, one box can hold all different types of fruit, one box can only hold apples. Logically it makes sense that Apples can go in both boxes where as Oranges can only go in the fruit box. In our case above we have a big box of zombies wearing different clothing!
Conclusion
Wow, we covered so much today! Like I mentioned last time, as you begin using more advanced concepts (like polymorphism) you start seeing exceptional results. If you're confused, either I've written this poorly or your brain is bleeding from all the great info we've talked about. At this point, we've discussed classes, objects, inheritance, function (or method) overriding, and polymorphism! Just remember, it's all about the relationships between classes and objects that we get all this great functionality and awesome power.
Read more about:
BlogsYou May Also Like