Suppose I'm creating a game played on a 2D coordinate grid. The game has 3 types of enemies which all move in different ways:
Drunkard
: moves using type 1 movement.Mummy
: moves using type 1 movement, except when it's near the main character, in which case it will use type 2 movement.Ninja
: moves using type 3 movement.
Here are the ideas I've come up with in organizing the class hierarchy:
Proposal 1
A single base class where each enemy is derived from:
abstract class Enemy:
show() // Called each game tick
update() // Called each game tick
abstract move() // Called in update
class Drunkard extends Enemy:
move() // Type 1 movement
class Mummy extends Enemy:
move() // Type 1 + type 2 movement
class Ninja extends Enemy:
move() // Type 3 movement
Problems:
- Violates DRY since code isn't shared between
Drunkard
andMummy
.
Proposal 2
Same as proposal 1 but Enemy does more:
abstract class Enemy:
show() // Called each game tick
update() // Called each game tick
move() // Tries alternateMove, if unsuccessful, perform type 1 movement
abstract alternateMove() // Returns a boolean
class Drunkard extends Enemy:
alternateMove(): return False
class Mummy extends Enemy:
alternateMove() // Type 2 movement if in range, otherwise return false
class Ninja extends Enemy:
alternateMove() // Type 3 movement and return true
Problems:
Ninja
really only has one move, so it doesn't really have an "alternate move." Thus,Enemy
is a subpar representation of all enemies.
Proposal 3
Extending proposal 2 with a MovementPlanEnemy
.
abstract class Enemy:
show() // Called each game tick
update() // Called each game tick
abstract move() // Called in update
class MovementPlanEnemy:
move() // Type 1 movement
abstract alternateMove()
class Drunkard extends MovementPlanEnemy:
alternateMove() // Return false
class Mummy extends MovementPlanEnemy:
alternateMove() // Tries type 2 movement
class Ninja extends Enemy:
move() // Type 3 movement
Problems:
- Ugly and possibly over-engineered.
Question
Proposal 1 is simple but has a lower level of abstraction. Proposal 3 is complex but has a higher level of abstraction.
I understand the whole thing about "composition over inheritance" and how it can solve this whole mess. However, I have to implement this for a school project which requires us to use inheritance. So given this restriction, what would be the best way to organize this class hierarchy? Is this just an example of why inheritance is inherently bad?
I guess since my restriction is that I have to use inheritance, I'm really asking the broader question: in general, when is it appropriate to introduce a new layer of abstraction at the cost of complicating the program architecture?
Best Answer
I've built a 2D roguelike from pretty much scratch, and after lots of experimentation, I used an entirely different approach. Essentially an entity component architecture.
Each game object is an
Entity
, and anEntity
has many attributes which control how it responds to stimuli from the player and the environment. One of these components in my game is aMovable
component (other examples areBurnable
,Harmable
, etc, my GitHub has the full list):Different types of enemies are distinguised by injecting different basic components at object creation time. So something like:
and
Each component stores a reference to its owner
Entity
for information like position.The components know how to receive messages from the game world, process them, then generate more messages for the results. These messages land in a global queue, and there is a main loop each turn which pops messages off the queue, process them, then pushes and resulting messages back on the queue. So, for example, a
Movable
component does not actually edit the position attributes of the owningentity
, it generates a message to the game engine that they should be changed, along with the position in which the owner should be moved to.There's essentially no class hierarchy for the basic game entities, and I did not find myself missing it. Behavior is distinguished entirely by what components a entity has. This works for every entity in the game world, player, enemy, or object.