Having trouble harnessing the power of Inheritance. (best practices advice requested)

Started by
19 comments, last by WozNZ 9 years, 1 month ago

It seems every time I try to use inheritance to save time, it ends up being way more trouble than its worth, and I end up ripping out the inheritance code and doing it without.

Heres a great example. This is C# in case that matters.

I'm making a game which has a lot of different types of bad guys. So naturally I thought inheritance. I created a Baddie class which had some very basic stuff, like X,Y position, hitpoints and movement speed. Then I began creating all sorts of specific bad guy classes that inherit from Baddie. I've got Slimer, Knight, Wizard, and Snake bad guys for example, and a lot more, that all extend Baddie.

While they all share the basic position, hitpoints and speed, the big problem is they are quite different other than that. The Wizard casts spells, the Snake just moves around randomly, and the Knight charges up his swing and then chops. I'm finding that I have to have specific code depending on the bad guy type, which if I'm not mistaken defeats the point of using inheritance in the first place!

So for example I have instances of all these different bad guy types in a single List<Baddie> list. I'm looping through the list and I want to update each baddie, regardless of the type. However I'm having to use all kinds of type-specific code like this:

foreach (Baddie theBaddie in baddieList) {

if (theBaddie is Knight) {...}

if (theBaddie is Wizard) {...}

}

Its becoming quite a mess and I'm wondering why I don't just scrap the whole Inheritance idea, and simply have a separate list for each baddie type:

List<Knight>

List<Slimer>

List<Wizard>

...etc

And separate update loops for each list.

Am I using inheritance wrong? Is this not an appropriate place to use inheritance? I'm starting to think that inheritance is pretty useless unless all of your classes are unbelievably similar. As soon as a class is more than just trivially different, inheritance is not going to help.

Advertisement

I'm finding that I have to have specific code depending on the bad guy type

Can you show real example?

See Strategy Pattern: http://obviam.net/index.php/design-in-game-entities-object-composition-strategies-part-1/
And: http://gameprogrammingpatterns.com/component.html

Hi.

maybe all you need to have is some virtual methods.

here or here second post explains it well.

Having trouble harnessing the power of Inheritance. (best practices advice requested)


Step 1: Don't.

Am I using inheritance wrong? Is this not an appropriate place to use inheritance? I'm starting to think that inheritance is pretty useless unless all of your classes are unbelievably similar. As soon as a class is more than just trivially different, inheritance is not going to help.


Yup, you got the right of it. Look up the words "aggregation" and "composition." There are plenty of good times to use inheritance, but this isn't one of them.

Its becoming quite a mess and I'm wondering why I don't just scrap the whole Inheritance idea, and simply have a separate list for each baddie type:
<snip>
And separate update loops for each list.


This isn't too far off from how many AAA games work. Many older games did exactly what you outlined while newer ones often use a component-based approach rather than distinct monolithic objects (if for no other reason than because they don't wnat to hardcode their list of enemies in the game and force designers to wait for engineer support to make any simple change).

Sean Middleditch – Game Systems Engineer – Join my team!


List<Knight>
List<Slimer>
List<Wizard>
...etc

And separate update loops for each list.

Am I using inheritance wrong? Is this not an appropriate place to use inheritance? I'm starting to think that inheritance is pretty useless unless all of your classes are unbelievably similar. As soon as a class is more than just trivially different, inheritance is not going to help.

The difficulty is that it doesn't extend.

Let's say you have a bunch of these monster classes. Gnome, Soldier, Centaur, Dragon, Angel, Demon, Monkey, Ape...

And then you look over it. What kind of difference is there between monkey and ape? So you make a base class and inherit there, too.

Then you decide you want some of those to be archers and some to be melee, so you get GnomeArcher, SoldierArcher, ApeArcher, GnomeSwordsman, SoldierSwordsman, DemonSwordsman. Then you decide to make something else, and very soon you're got a combinatorial explosion with thousands of classes.

If you use composition you avoid that. You have a character class. It is composed of a creature type (gnome, ape, dragon, demon, whatever), and a set of components. It may have a locomotion component with parameters describing how it moves. It may have an Archery component allowing it to shoot some type of items, perhaps arrows, perhaps lighting bolts, perhaps fireballs. It may have a Melee component allowing it to use an attached weapon.

With that system you can build everything from a dragon (flying locomotion, archery with fireball), wizards (character locomotion, archery with low power lighting bolts), soldiers (character locomotion, melee attack with sword) a trebuchet (slow moving locomotion, archery with big stick), archer towers (near-instant rotation only locomotion, fires arrows), cannon towers (slow moving rotation only locomotion, fires cannonballs).

As for when to use inheritance, its when all the main things work the same and a small number of internals are changing.

You normally want an inheritance tree to be shallow and wide. That is, you start with one base class and have a large number of derived classes, maybe twenty or fifty or even more, depending on your situation. Usually the classes should only modify internal behavior.

Over the years, I've also found the best inheritance trees have no virtual public members. The member functions can call protected and private member functions, but virtual functions shouldn't be called by others. In practice over time a public virtual method will take on additional meaning and violate the IS-A rule. Instead every instance should do the same basic thing and only look up protected member functions to do whatever slightly modified functionality is necessary.

There are many common examples of it. This one stolen from Wikipedia's Factory Method example, then modified slightly to C++.


class MazeGame {
    public:

    /* NOT virtual */
    void BuildMaze() {
        Room* room1 = makeRoom();
        Room* room2 = makeRoom();
        room1->connect(room2);
        this->addRoom(room1);
        this->addRoom(room2);
    }
 
    protected:
     virtual Room* makeRoom() {
        return new OrdinaryRoom();
    }
}

class MagicMazeGame : public MazeGame {

    protected:
    virtual Room* makeRoom() {
        return new MagicRoom();
    }
}

If we allowed BuildMaze to be virtual then each derived class could change it to be whatever details they wanted. In practice if BuildMaze were virtual it would start out okay, but over time new features would be added, and MiniGolfMazeGame::BuildMaze() might change some game states, DragonWarfareMazeGame::BuildMaze() will modify some UI elements, and soon you'll find changes to the base game also mean rewriting most of the derived classes as well.

-----

Note that ECS-based games (Entity Component Systems) tend to use both composition and inheritance.

The individual components tend to be derived from a base class, building up whatever behaviors you need. There may be a large number in a shallow but deep inheritance tree. Perhaps hundreds, perhaps thousands, of component building pieces. There will be a variety of different locomotion components, respawn components, animation components, event trigger components, and on and on and on.

These components are composed together inside a game object. The game object has a collection of components or behaviors, rather than inheriting from them with multiple inheritance or whatever you would have done.

-----

This doesn't mean that inheritance trees and object interfaces doesn't work as a strategy. It can work, and it has worked for many major games. However, it tends to be fragile and difficult to extend, it also tends to require more careful and thoughtful design to work successfully. It is easier and less error prone to build through composition.

Am I using inheritance wrong? Is this not an appropriate place to use inheritance? I'm starting to think that inheritance is pretty useless unless all of your classes are unbelievably similar. As soon as a class is more than just trivially different, inheritance is not going to help.


Yup, you got the right of it. Look up the words "aggregation" and "composition." There are plenty of good times to use inheritance, but this isn't one of them.

I don't quite understand why you say this. I'm familiar with inheritance vs composition, and IS-A vs HAS-A. It seems to me that this is clearly an IS-A situation.
Wizard IS-A Baddie

Knight IS-A Baddie

Slimer IS-A Baddie

etc.

This is exactly what I was thinking when I decided to use inheritance for this problem. Yet this is apparently not a good time to use inheritance. Can you explain why everyone always says to do the IS-A vs HAS-A test, when it fails in this extremely simple case? I did search the forums before posting and I'd say the number one piece of advice given is to think about IS-A vs HAS-A to determine whether inheritance or composition is more appropriate. Where is my thinking going wrong here? How is my Baddie heirarchy a HAS-A relationship instead of an IS-A ?

Anyway, everyeone else has posted some great information too. I'll digest it all and post back soon. Thanks!

The way I think of it is this: it has the properties of a baddie so therefore it's implied that it is a baddie and can be treated as one.

It's basically a form of duck typing, which is an alternate to using interfaces. Both are forms of polymorphism, but since we want our objects to be easily changed (essentially making them dynamically typed) we use duck typing instead of interfaces and inheritance. This of course goes along with avoiding problems related to the deadly diamond and virtual function calls.

"So there you have it, ladies and gentlemen: the only API I’ve ever used that requires both elevated privileges and a dedicated user thread just to copy a block of structures from the kernel to the user." - Casey Muratori

boreal.aggydaggy.com

The IS-A replacement must be an EXACT replacement as far as external systems are concerned.

It must behave the same as an external specification and the same as far as notable behavior characteristics.

If I need to replace my spark plugs, I can use a Bosche brand, Champion brand, Autolite brand, or Autozone brand spark plug. As long as I make sure all the critical specs are the same, I don't need to know or care about the internal details. Internally some may have better seals, may have better insulators, may have longer lifetimes, but that doesn't matter to the engine. They are interchangeable as long as all of them have the same external specification and the same behavior characteristics.

However, I cannot replace my 12mm spark plug with an 18mm spark plug. Externally these plugs are different. The 18mm spark plug might have all the same parts with electrodes and a ceramic shell, but it has different observable characteristics.


In your case, sometimes you are right that a wizard and a knight are all Baddies as far as the system is concerned. They share some characteristics. Like the spark plugs, they share the electrodes and the insulator and the terminal and the case and seals. They are mostly alike.

But you also pointed out in your code, sometimes they are not an IS-A relationship. Specifically with this code:


if (theBaddie is Knight) { ... }
if (theBaddie is Wizard) { ... }

That proves without a doubt that they are NOT equivalent.

It is possible to make them behave equivalently by reversing your queries into behaviors.

Let's change it slightly to this:


if (theBaddie is Knight) { DoKnightKerjigger(theBaddie); }  // Obviously Knight and Wizard are not equivalent. They do different things
if (theBaddie is Wizard) { DoWizardKerjigger(theBaddie); }  // One is a 12mm spark plug, the other is an 18mm spark plug.

With that we can invert the subject and the action and everything works just fine:


theBaddie.DoKerjigger();  // Their behavior may be different internally, but as far as I care they are equivalent

If you need to know the most derived type of an object, it generally means you are doing something wrong with inheritance. The objects should be functionally equivalent for all critical behavior. If you need to identify which kind it is then they are probably not functionally equivalent.

I don't quite understand why you say this. I'm familiar with inheritance vs composition, and IS-A vs HAS-A. It seems to me that this is clearly an IS-A situation.
Wizard IS-A Baddie
Knight IS-A Baddie
Slimer IS-A Baddie


The problem is that you use IS-A in the way we normally use the term in natural, spoken language. However in OOP the term has very specific meaning. I personally blame bad teachers for this misunderstanding, as inheritance is often taught with contrived real world examples that never actually occur in real software projects. (Plant-Flower-Tree-Bush for example)

Make yourself familiar with the Liskov substitution principle. This should be the benchmark to decide wether you should use inheritance or composition. As a general rule of thumb: When in doubt, use composition.

I don't quite understand why you say this. I'm familiar with inheritance vs composition, and IS-A vs HAS-A. It seems to me that this is clearly an IS-A situation.
Wizard IS-A Baddie
Knight IS-A Baddie
Slimer IS-A Baddie
etc.


You're thinking like a person, not a computer. Your real-world taxonomies don't mean a thing to a mathemetical computation engine crunching through logical structure of algorithms. Programming languages are made to execute code, not to model dictionary definitions of real-world objects.

Why would your program care in any way, shape, or form that some object is a Baddie? What does that possibly mean to your code or your computer? Your program cares about what data that object has and which operations are legal to perform on that data. e.g., is it something that can perform an attack against another game object? Because there's certainly not something that only a Baddie can do. Is it something that can be drawn? That's certainly not specific to a Baddie. Does it have AI? So would a bot controlling a friendly NPC, so it's certainly not limited to a Baddie. And so on; you are very likely to find that almost nothing in your code is in any way specific to a Baddie; on the other hand, you're likely to find a lot of functionality that makes sense on a diverse cross-section of different types of game objects, which means you either need to bloat up all game objects to put that functionality into a single univeral base class (so everything IS-A GameObject) or using composition to give that functionality only to the objects that need it (so a Wizard HAS-A RenderableComponent... but an AmbientSoundSource or ScriptTriggerZone or so on wouldn't).

Trying to express things like "an orange is a fruit" is the first sin of bad object-oriented programming (and it's so depressingly prevalent because every terrible Java/C# school and book starts off with almost that exact bad example...) and has directly led to the poor reputation OOP has unfairly earned. "Wizard is a Baddie" is a squishy-human-brain form of categorization, not something that's relevant to what program actually needs to do.

Use IS-A and inheritance to model functional interfaces and abstractions that you program against, e.g. FmodAudioService IS-A(n) AudioService, and not to model your real-world taxonomies.

Sean Middleditch – Game Systems Engineer – Join my team!

This topic is closed to new replies.

Advertisement