2

Introduction to my problem

I'm trying to design a very simple video game where there are several Animals players (they all inherit from Animal):

  • Cat
  • Dog
  • Rabbit

There are also some performable actions (only one per Animal) they can perform such as:

  • Bite: done only by Dogs (if an animal gets bitten, it will lose an amount of life)
  • Kick: done only by Rabbits (if an animal gets kicked they won't be able to move for X seconds)
  • Scratch: done only by Cats (if an animal gets scratched they will reduce their speed movement by -50% for Y seconds)

I was thinking about a design that could be extensible in the future: having new animals (or even new players such as aliens/plants/humans), actions and even being able to randomly assign an action to a player.

My solution so far

Interfaces

First of all, every player that would like to play the game, must implement the Playable interface (In this case, Animal would implement Playable):

The Playable interface

interface Playable {
  move(distance: number): void,
  getPosition(): number,
  attack(): void,
  beingBitten(): void,
  beingKicked(): void,
  beingScratched(): void
}

The Performer interface

Also, I thought about having an interface called Performer for every action a player could perform over another player:

interface Performer {
  perform(enemy: Playable): void
}

Implementations

This way, our Rabbit's .attack() implementation would be something like the following:

class Rabbit extends Animal {

  private action: Performer;

  // ... etc ... 

  public attack(enemy: Playable): void {
    action.perform(enemy);
  }

  // ... etc ... 

}

And Kick performable action would look something like this:

class Kick implements Performer {

  public perform(enemy: Playable): void {
    enemy.beingKicked();
  }

}

My concerns

In terms of organization and being clearer, I was thinking that I could break the Playable interface in three:

The Attacker interface

Actions related to attacking would go here.

interface Attacker { attack(enemy: Playable): void }

The Attackable interface

Actions related to being attackable would go here.

interface Attackable {
  beingBitten(): void,
  beingKicked(): void,
  beingScratched(): void
}

The Playable interface

Actions related to being able to move around would go here.

interface Playable extends Attacker, Attackable {
  move(distance: number): void,
  getPosition(): number
}

I don't have that much experience at all when it comes to OOP, so every time I end up overthinking things and designing some different alternatives but I struggle in deciding which one I should go for.

How do you see all of this? Does it make sense at all splitting Playable into three in order to make things clearer?

3
  • 1
    Perhaps you should think about how you'd like this to look from the outside. cat.attack(dog)? dog.beAttackedBy(cat)? Does it matter what specific attack is being dealt, to either party?
    – jonrsharpe
    Mar 5, 2017 at 16:55
  • @jonrsharpe your point sounds interesting... beingAttackedBy(enemy: Playable) can be nice to avoid having to extend the Attackable interface and implementations each time a new action would be added. What I don't see here is the way to achieve that when someAnimal.beAttackedBy(otherAnimal) happens, the otherAnimal action is performed over someAnimal. Any ideas? Mar 5, 2017 at 17:10
  • 1
    Eric Lippert discusses this at length
    – JBSnorro
    Jan 30, 2018 at 13:06

3 Answers 3

1

I strongly recommend abstaining from all OOP techniques, as these do not do a good job of modeling the type of data you typically work with in a game.

The simplest example is any point in your game where you will be dealing with more than a small handful of "objects." In the OOP solution, you've conveyed the idea of managing each one as a discrete and separate ideas that inherit and/or override properties from one another. The reality of the data is that these properties can be expressed as structures of arrays, and more efficiently processed with your CPU's SIMD extensions.

See the presentation from Mike Acton, Senior Architect at Insomniac games for a thorough explanation of battle-tested game design.

Casey Muratori's blog at mollyrocket "Working on the Witness, part 11" and onward has expert analysis from a game development veteran about the fallacy of trying to apply OOP principles in a high-performance gaming system, and offers an alternative way of thinking about games programming. Also see Casey's streaming tutorials, Handmade Hero for a live, real-time coding of a fully featured game from scratch.

2
  • This strikes me as good advice if and only if the asker is trying to make a video game that performs well while handling a lot of objects at once. If there are only a small handful of objects, aren't the usual OOP techniques totally fine? Mar 6, 2017 at 6:05
  • That isn't the only impediment OOP causes. For instance, you could to edit the game code, recompile, and reload it live while the game is running by loading the game at a fixed base address, but vtable lookups would break this. A better design strategy would be to only use OO features that you could definitively prove were 1) not harmful, and 2) provided some benefit to the game when included.
    – k2t0f12d
    Mar 6, 2017 at 6:47
0

Not sure how much it helps you, but your question reminds me of this video, which stuck into my mind:

Composition over Inheritance

https://www.youtube.com/watch?v=wfMtDGfHWpA

The example is in JavaScript which is very malleable, many languages do not have this flexibility.

I just realized you asked this almost a year ago. I am curious which path did you choose.

0

From my perspective, your question highlights a pitfall of OOP. I would echo k2t0f12d's overall thoughts here. Even without considering the near-endless ways you could extend your design, just your basic design requirements here:

Bite: done only by Dogs (if an animal gets bitten, it will lose an amount of life) Kick: done only by Rabbits (if an animal gets kicked they won't be able to move for X seconds) Scratch: done only by Cats (if an animal gets scratched they will reduce their speed movement by -50% for Y seconds)

... would give the teams I've worked with a real struggle to design things right on the very first try without fiddling endlessly with abstract interfaces trying to generalize their behaviors and breaking existing code left and right or deprecating old interfaces and introducing new ones in their place.

And of course, we could simply fault ourselves as humans for somehow misusing our tools and paradigms. I see no shortage of OO purists who have nothing resembling even a single AAA title under their belt faulting anyone who finds OO to be problematic in their domain in this way.

But it can be a whole lot easier to reduce your animals to just data (one concrete data type to represent dogs, rabbits, cats, and anything else), for example, and have an AI system (it could be an object) that allows dogs to bite and rabbits to kick and cats to scratch almost as easily as you described it in English without second-guessing ourselves and constantly fiddling with designs trying to make a cat scratch a scratchable, and a dog bite a bitable, and rabbits to kick a kickable and getting so caught up and potentially spending enormous time designing and redesigning abstract interfaces for said objects while, simultaneously, trying to introduce whole new ones with whole new capabilities.

People like Mike Acton really do hammer in how efficient data-oriented design can be in contrast to object-oriented code, but I see a failing in hammering in how much they can also save needless grief in iteratively changing our designs over and over and updating all dependent code. There seems to be an assumption that data-oriented design is a productivity compromise for max computational efficiency when, in some cases, they can substantially boost productivity as well.

The way you described your system in English wants like a zoomed-out god-level view of your world when OOP reduces you to thinking about the generality of your designs in a highly zoomed-in, localized object-level view of it. If you want to design with a god-level view of your simulation and that's how you want to talk about your designs, then it helps not to be confined to teeny encapsulated, polymorphic objects of this sort. It's similar to implementing an AD&D rulebook. AD&D rulebooks are communicated in a god-level view of the world when they say some lightning spell bounces off creatures, causes extra damage to amphibians, and causes a larger area of effect damage in indoor environments. That's easy to write a function for without the barriers of encapsulation and information hiding. It can be extremely difficult to do it, especially if this is a new design requirement that you didn't anticipate in advance, if you are trying to avoid creating monolithic, abstract, object-oriented interfaces or avoid creating endless micro-interfaces that require going back and reimplementing for all your relevant concrete objects.

And don't get me wrong. I still think OOP is awesome in certain contexts. I just think your example highlights a scenario where maybe we should be asking ourselves more questions about whether we're actually improving productivity or hindering it by reaching for objects.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.