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.