What goes in a GameObject class

On gamedev.net recently there have been several questions about game objects. What are they? How do they work with the rest of the system? What belongs in the object? When should they be created and destroyed? This week I’ll be addressing a few of these.

SOLID Programming

Let’s begin with a reminder of some good programming principles. A few key principles have a convenient acronym: SOLID.  Each of these principles has a strong interplay with the others. Following one will make it easier to follow the others, and failing to follow one makes it more difficult to follow the others.

S = Single Responsibility Principle. SRP means a class should always have a single responsibility.  Having more than one responsibility makes it more confusing to use an object, and more difficult to build extensible systems. Give one primary task for each object. That one responsibility can be to act as a cache, or a loader, or to spawn items, or whatever else the thing needs to do. When applied to bigger modules the entire module should have a single responsibility; a module for 3D rendering, a module for physics, a module for audio, a module for serialization (loading and saving stuff), a module for running the simulation steps. For every thing, let it have only a single responsibility.

O = Open/Closed Principle. This one trips up a many beginner programmers, and sadly many teachers and old reference books don’t follow the principle. OCP means code should be open for extension, this allows others to implement features in whatever way they want or need. Code should also be closed for modification, meaning you should not be able to modify the interfaces.  Object families have a single task that is well-defined, subclasses can implement that task as they need.

L = Liskov Substitution Principle. The LSP basically means that your code should be written so objects are interchangeable. If you have any object that implements an interface, every object that has the interface should be interchangeable.  Users of an object family should be able to substitute freely. For example they should not care if the graphics card is an Nvidia GeForce 700 series, and AMD Radeon 7000 series, or an Intel Broadwell integrated chip, all of them implement the Direct3D 12 interface. Substitute one for the other and the code will still work.

I = Interface Segregation Principle. ISP means that interfaces for one set of tasks should be separate from other task interfaces. Closely related to the SRP, it applies to functionality. A set of interfaces for an event listener should only have tasks associated with listening to events. Game Objects have a high risk of becoming a “God Class”, a class with an enormous number of interfaces that can do everything; do your best to avoid it because it is a maintenance nightmare. A set of interfaces for objects that can be used in an inventory should only have tasks associated with inventory use. A set of interfaces for items that broadcast sound should only have tasks associated with sound broadcasting.  It is possible for classes to implement multiple interfaces but still have a single responsibility. A boombox stereo might have interfaces to broadcast sound and interfaces to be placed in an inventory and interfaces to be broken, but still have the single responsibility of implementing the specialized behavior of the boombox object.

D = Dependency Inversion Principle. Systems should depend on the abstractions rather than concrete implementations. This follows closely from LSP and ISP. If it doesn’t matter what the individual types are, and the objects have an interface that does what is needed, the programmer should depend on the abstraction. I can depend on an abstract IDirect3DDevice abstraction without needing to know if internally the vendor has made a class for D3D12GeForce760GTX, or D3D12HD7970, or D3D12Iris6200.

SOLID for game objects

With that in mind, what does SOLID mean when it comes to game objects?

The general purpose “GameObject” class in games had an evolution around the year 2000. In most systems I’ve observed and discussed over the past 15 years, the role of a game object is one of a container.

The single responsibility is to contain all the details needed for other systems to do their jobs. There are a few options in achieving it.  The most direct is a container class that implements interfaces. The more flexible path is a container for other objects that implement those interfaces. The second is often called an “ECS” or “Entity-Component System”.

You are probably already familiar with container classes. There are linked lists, dynamic arrays, hash maps, data dictionaries, and others that are quite commonly used.

In the case of modern game engines, the GameObject containers contain “components” or “behaviors” or similarly-named objects.

The most common component is the transformation, or the object’s position and orientation in 3D space. Because it is nearly universal the transformation is often implemented as a mandatory element, a member variable rather than an item  linked to inside the container.

There might also be a friendly object name used by programmers, designers, and others developing the game. The name has no relationship to what players see. Some engines don’t use name fields, others do.

The GameObject next implements simple container operations, probably leveraging an existing structure for most of the work. Depending on your object ownership model and lifetime requirements you may want to store different things.

In C++, if your design has the GameObject as the owner of the object’s life, it may make sense to have the component’s lifetime completely controlled with a unique_ptr (std::vector<std::unique_ptr<Component> >).  If your design makes components that are always pulled from a place that is guaranteed to outlive any GameObject, you may store a direct pointer to the object (std::vector<Component *>).  If another system is controlling object lifetimes and you are using a more complex pattern of shared ownership, a weak_ptr (std::vector<std::weak_ptr<Component> >) or shared_ptr (std::vector<std::shared_ptr<Component> >) may be more appropriate. In general shared ownership adds some serious complications, so avoid it if you can.

As a container, you can search for components that match whatever you need.

Finally, many systems will not allow you to directly create a GameObject. The type is often implemented as an abstract class with a hidden implementation class (perhaps called GameObjectPimpl), or there is a parallel interface-only class (perhaps called IGameObject). You can use a factory method to create the object which ensures the object gets created from the proper memory pools and returns a pointer to the abstract class.

So for SOLID:

S – Single responsibility principle, they are a container. Game objects don’t move themselves, load themselves, draw themselves, update themselves, or do anything beyond being a container.
O – Open/Closed principle, you generally don’t need to modify a container. If you happen to need to do so, you aren’t creating a concrete instance but instead using a factory method to create it, so any private modifications on a subclass should be invisible to other developers.
L – Liskov Substitution principle, any instance is interchangeable with another, they are all containers together.
I – Interface Segregation principle, you don’t need to know if someone has specialized the container. It has a transformation, it has a name, and it has a collection of components.
D – Dependency Inversion principle. By restricting access to direct creation, you are strongly encouraged to do the right thing, working only with the abstract base pointer rather than knowing the exact type of the GameObject or knowing the exact type of the components.

SOLID for Components

What does SOLID mean for components?

Components these days do the bulk of the work. They usually implement the mandatory functionality required by their Component base, but also usually implement multiple other Interfaces.  Just like the GameObject discussion above, the Component base class should be abstract so you can’t create one by itself. You will probably add a bunch of utility functionality to the base class, perhaps a way to find the GameObject it is attached to, perhaps a way to mark if the Component is active or set/clear other flags.

Let’s say you’re building a physics system.

You are going to need a way to compare the objects so you might build a CollisionShape interface. It’s job is to enable comparisons by your physics engine. It might have abstract functions GetCollisionMesh(), GetPhysicsMaterial().

You might build a CollisionListener interface. It’s job is to respond to physics collisions. The interface does not need much, it handles events. You might have functions HandleCollisionEnter(CollisionInfo*), HandleCollisionExit(CollisionInfo*), HandleCollisionOverlap(CollisionInfo*), each implemented as virtual functions and passing information about the collision.

With that, you’re able to put together a component. We’ll call it a BoxCollider.

The BoxCollider should derive from Component so you get whatever juicy implementation has been provided by the base class. It should derive from CollisionShape so the physics system knows how it collides. Also derive from ColliderListener so you can hear the events.  The physics collision mesh should be a simple box, this can be coded directly. The physics material is something individual objects may want to customize. Some materials will be bouncy and others absorb motion; some materials will have high friction and others will have low friction. For the collision handlers, these will also probably be set as function pointers or event handlers. When something creates a BoxCollider they can assign a function; when a function like HandleCollisionEnter() is called the object can forward the call on to whatever was entered as the response function.

You could make more of these colliders. A SphereCollider is common. You’ll need a MeshCollider to work with complex shapes. Players are often implemented as capsules, so CapsuleCollider is probably on the list. All of them implement CollisionShape.

Now that you’ve got components, something needs to use it. Somewhere else in the system, a GameObject is created and initialized, a BoxColleder is created an initialized, then added to the GameObject. Your physics system can test to see if any GameObject has a CollisionShape.  This example can be improved quite a bit by reducing the search space and caching results, but for a small system it would work:

for( GameObject* go in World.ActiveObjects()) {
  for( Component* c in go->GetActiveComponents()) {
    if( CollisionShape* cs = dynamic_cast<CollisionShape*>(c) != NULL ) {
      // Do something with cs. You can use GetCollisionMesh() and GetPhysicsMaterial()
      // to figure out how the object collides with other collision objects.
    }
  }
}

Going through SOLID again:
S – Single responsibility, the component only does one thing. It may be layered with multiple tasks, but it only has one responsibility.  It is not like the component is drawing itself, moving itself, colliding itself, it only does one thing.
O – Open/Closed, you can still specialize the behavior further but it strictly follows the interfaces that were given.
L – By following interfaces, anything using the components don’t care what the concrete class is. They are all substituable. Any collision shape can be swapped out with any other collision shape.
I – Interfaces are clusters of tasks. A class may have more than one interface, but each interface has been segregated into task clusters.
D – Dependency inversion is met, we are only dealing with the abstract base interfaces rather than the concrete final types.

Example

At this point, an example of how these work together is in order.

We’ll start by making a locomotion class.  We’ll call it MoveTurnIfWithinRange.  It will derive from Component and Locomotor. We’ll assume all Locomotor objects are pushed forward based on their current speed by the physics system.

We’ll add a few data members to the MoveTurnIfWithinRange.

  • target. This points to another game object we should be comparing against.
  • rangeMin, rangeMax. If the target is within this range, we do the motion.
  • turnSpeedMin, turnSpeedMax. What are the fastest speeds we can turn. Pick a scale like radians per second.
  • turnSpeedDelta. How fast we can change the turning speed.
  • thrustSpeedMin, thrustSpeedMax. How fast we can move. Probably in meters per second.
  • thrustSpeedDelta. How fast we can change the thrust speed.
  • turnSpeedCurrent, thrustSpeedCurrent. Where we are now.

During updates, the code will need to test the target to see if it is within the acceptable range. If so, it will attempt to apply a turn toward the object, and attempt to add to the speed until it gets to the maximum speed. If outside the range, it will reduce the turning speed and thrust speed until they get to the minimum.

Now let’s create a component called LaunchProjectileIfWithinRange.

It will have some data members as well.

  • target. This points to another game object we should be comparing against.
  • rangeMin, rangeMax. If the target is within this range, we shoot.
  • projectile. Points to another game object that we are shooting.
  • shotDelay. Delay between shooting projectiles.

If the target is within the range, even if we are not pointed at the target, start shooting. If it is too close or too distant, stop shooting. When shooting, create a new projectile object based on the template, add it to the world, then sit idle for a few moments.

We just created TWO components.

Projectile is a game object with several components. The components will probably include a graphics mesh, a collision object, and its own locomotor.

Now for the data in our example.

Two components. Let’s see what we can make.

Let’s create a game object called “Arrows”. Give it a mesh of some arrows, a collision object of a small capsule, and a straight locomotor. On collision it does some damage.

Let’s create a game object called “Cannonball”. Give it a mesh of a small ball, a collision object of a ball, and a straight locomotor moving quickly. On collision it does big damage.

Let’s create a game object called “Fireball”. Don’t give this one a mesh, give it a fancy particle system component that makes a fireball and smoke trail. Give it a collision object of a ball. On collision it does small damage and creates fire. These fireballs are smart, so instead of a straight forward locomotor, give them a MoveTurnIfWithinRange locomotor. Since this object will be cloned, give a targeting range for the fireball to see the target (e.g. 0 to 20), a slow max turning rate (e.g. 0 to pi/4 radians per second), a fixed speed range (40 to 40).

FINALLY we can build the fun items.

Let’s create a game object called ArcherTurret. Add a MoveTurnIfWithinRange component. Every update it searches for a new Target, and as long as the target is in range it stays targeting that one. Since the turret just sits there, thrustSpeed has a no speed (0 to 0). The archers in the turret can turn quickly, so turn rate (0 to pi) that can turn a half circle per second (pi). Add a LaunchProjectileIfWithinRange component.  Give it a range for when to start shooting, point the projectile to the Arrows object, and give it a shot of fire, maybe 2 shots per second.

Let’s create a game object called CannonTurret. Same as above, MoveTurnIfWithinRange that has no movement, a slow turn speed. A LaunchProjectileIfWithinRange component that shoots cannon balls and a slow rate of fire.

Again, MageTurret. Stationary but turns, launches fireballs.

We can also do automated turrets that are constantly firing in a circle. Use the MoveTurnIfWithinRange as above, but set the maximum range to infinity (or whatever high number represents infinity in your world) so it is always turning. Then set the turning speed to a fixed range, perhaps pi/2 so it turns a quarter circle per second at a fixed rate.

You can make turrets that never move and never turn, only pointing forward and firing when someone gets in range.

We can do more than turrets.

Let’s make an object called FollowTank. Give it a MoveTurnIfWithinRange component setting speed and turn rates appropriate for a tank.

Like the fireballs, you can make objects that move and turn, like a guided missile (or red shell). No turn but a bit of acceleration gives a dumbfire missile (or green shell).

You make projectiles that also shoot projectiles.

Summing up

In modern games, a GameObject class is a container object. It holds components, and that’s it. Components have small, well-defined pieces of behavior. Both can be implemented with good programming practices that keep implementation easy. Other software patterns, such as data-driven development patterns, can still be followed with careful thought.

With only two simple Component classes we can leverage the power of game objects as containers to create an enormous number of projectile-shooting, potentially self-aiming, potentially moving objects.

Game designers tend to love component based architectures. A small number of well-designed components can be leveraged into seemingly emergent behaviors and thought-provoking complex worlds using only a few simple rules.

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload CAPTCHA.