Type Objects

Learn to create flexible game entities using the Type Object pattern for data-driven design.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

View Full CourseGet Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

In the previous lessons, we learned some techniques that allow objects in our C++ program to acopt characteristics defined in some external location. This might be a file created by a different program, a stream of data coming from the internet, or simply a text file that we can edit.

For example, we could let designers on our team or our end users add monsters to our game by editing a JSON file:

{
  "name": "Goblin",
  "level": 3,
  "health": 100,
  "damage": 15,
  "armor": 0.2,
  "art": "goblin.png",
  "position": { "x": 1, "y": 3 }
}

However, with this simple system, we'll soon encounter some design problems. For example, if we were to create a dungeon with some monsters, we'll end up with a lot of duplication:

[{
  "name": "Goblin",
  "level": 3,
  "health": 100,
  "damage": 15,
  "armor": 0.2,
  "art": "goblin.png",
  "position": { "x": 1, "y": 3 }
}, {
  "name": "Goblin",
  "level": 3,
  "health": 100,
  "damage": 15,
  "armor": 0.2,
  "art": "goblin.png",
  "position": { "x": 2, "y": 1 }
}, {
  "name": "Goblin",
  "level": 3,
  "health": 100,
  "damage": 15,
  "armor": 0.2,
  "art": "goblin.png",
  "position": { "x": 4, "y": 7 }
}]

Duplication in our serialized data is just as much of a problem as it is in our code. It makes changes quite difficult - if we need to change the health that goblins have, we have to update every copy.

Inefficient data organisation also has a runtime performance cost in a way that duplicate code often doesn't. Large blobs of data takes longer to deserialize and transfer.

Revisting Types

We probably recognise this as a familar problem - when groups of objects have a set of similar characteristics, we can represent them as being part of the same class, or having the same type.

We could represent those shared set of characteristics as a single value, which we'll call type:

[{
  "type": "Goblin",
  "position": { "x": 1, "y": 3 }
}, {
  "type": "Goblin",
  "position": { "x": 2, "y": 1 }
}, {
  "type": "Goblin",
  "position": { "x": 4, "y": 7 }
}]

Then, in the C++ code that deserializes our data, we can examine the requested type of each object, and map it to a corresponding class:

// Defining a class
class Goblin {
public:
  Goblin(int x, int y) : PositionX{x}, PositionY{y} {}
  int PositionX;
  int PositionY;
  
  std::string Name{"Goblin"};
  int Level{3};
  int Health{100};
  int Damage{15};
  float Armor{0.2};
  std::string Art{"goblin.png"};
};
// Creating class instances
Goblin A{1, 3};
Goblin B{2, 1};
Goblin C{4, 7};

The tradeoff here is that, having moves values that were previously defined in data back into our C++ code, we've lost some flexibility

Problems with Classes

The problem with relying solely on C++ classes is that it ties our type definitions directly to our compiled code. This introduces several limitations:

Firstly, modifying or adding new types of monsters now requires C++ programming knowledge. A game designer or even a player who wants to mod the game can't simply tweak a configuration file; they'd need access to the source code, and the C++ knowledge to modify it.

Secondly, this approach can slow down iteration. If we want to experiment with a new monster type or adjust the stats of an existing one, we have to stop the game, change the C++ class, recompile the entire project, and then restart. This cycle can be time-consuming, especially when fine-tuning game balance or exploring creative ideas.

Finally, if our game has a vast bestiary with many unique monsters, each differing only slightly in stats or appearance, we could end up with an explosion of C++ classes. This can make the codebase harder to manage and navigate, even if many of these classes are very similar.

While the C++ type system is incredibly powerful and forms the backbone of our programs, for scenarios demanding high flexibility and external configuration of object properties, layering another system on top can be highly beneficial.

Types as Data

So, we want to return granular control of our objects back outside of our program where they can be easily modified, but without causing the duplicate data problem we had before. The natural idea that probably comes to us is to have those types themselves defined as data that our program deserializes.

Elsewhere in our JSON file, or in a different file, we can define the types of enemies that our program will be using. These look very similar to the classes we had in our C++ code, except now they're represented as serialized data:

{
  "Goblin": {
    "name": "Goblin",
    "level": 3,
    "health": 100,
    "damage": 15,
    "armor": 0.2,
    "art": "goblin.png",
  },
  "Dragon": {
    "name": "Dragon",
    "level": 12,
    "health": 1300,
    "damage": 75,
    "armor": 0.4,
    "art": "dragon.png",
  }
}

We have just discovered the concept of a type object - a popular design pattern for solving this exact problem.

Type Objects

The Type Object pattern offers a way to define the shared characteristics of our game objects using data, rather than hardcoding everything into C++ classes.

This means we can have one general C++ class for many game entities, and their specific "type" (like being a Goblin or a Dragon) is determined by a separate "type object" they hold as a member variable.

This "type object" holds all the common properties for entities of that kind, like default health, damage, or artwork. Our individual entity instances will then store their own unique data (like current position or current health) and hold a reference to their type object.

This allows the C++ aspect of our type system to involve just two classes - one for the primary objects, which we'll call Entity in our examples, and a second class for the type objects, which we'll call MonsterType.

First, let's define our simple MonsterType struct, which we'll use later to instantiate our type objects:

// MonsterType.h
#pragma once
#include <string>

struct MonsterType {
  std::string Name;
  int Level;
  // Shared maximum health for this type
  int MaxHealth;
  int Damage;
  std::string ArtFile;
};

Next, our Entity class will be quite generic. It stores its unique properties (like position and current health) and holds a reference to its MonsterType:

// Entity.h
#pragma once
#include "MonsterType.h"

class Entity {
public:
  Entity(const MonsterType& Type, int x, int y)
  : Type{Type}, X_Pos{x}, Y_Pos{y},
    CurrentHealth{Type.MaxHealth} {}

  // Member variables
  const MonsterType& Type; // See note below 
  
  // Instance-specific values
  int X_Pos;
  int Y_Pos;
  int CurrentHealth;
};

Type Object Naming

In the previous example, we've called the member variable storing the type object simply Type. However, the word "type" can become a bit overloaded when discussing systems like this. When we talk about an entity's "type", we could be referring to its C++ class (e.g., Entity) or the specific type object it references (e.g., the MonsterType instance stored in its Type variable).

Usually, the meaning is clear from the context. However, to avoid ambiguity, it's sometimes helpful to use a more specific term for the data-driven type that the type object represents.

Common alternatives for the concept embodied by the type object include "metatype", "kind", "prototype", or "archetype".

Depending on the specific context in which type objects are being used, even more descriptive names might be suitable. For example, given our main C++ class is representing Monster objects in this example, alternative terms for their type object might be things like Breed or Species.

We'll just stick with Type in this lesson.

Now, let's see how to use our two classes this in main:

// main.cpp
#include "MonsterType.h"
#include "Entity.h"

int main() {
  MonsterType GoblinType{
    "Goblin",      // Name
    3,             // Level
    100,           // MaxHealth
    15,            // Damage
    "goblin.png"   // ArtFile
  };

  MonsterType DragonType{
    "Dragon",      // Name
    12,            // Level
    1300,          // MaxHealth
    75,            // Damage
    "dragon.png"   // ArtFile
  };

  // Create Entity instances, providing
  // their type and position
  Entity GoblinA{GoblinType, 10, 20};
  Entity GoblinB{GoblinType, 15, 25};
  Entity BigDragon{DragonType, 50, 50};
}

Benefits of Type Objects

The Type Object pattern offers several significant advantages, particularly in game development:

  • Rapid Iteration: One of the most compelling benefits is the ability to update types very quickly. Since type definitions (like a monster's stats or abilities) are stored as data, changes can often be made without needing to recompile the C++ code. We can implement our code such that designers can tweak values in a JSON file, for example, and see the effects in-game almost immediately. This dramatically speeds up the design and balancing process.
  • Accessibility for Non-Programmers: Because types are defined externally, individuals without C++ programming knowledge can modify or create new types. Game designers can balance enemy stats, artists can specify visual assets, and even players (in moddable games) can create custom content, all by editing data files.
  • External and Dynamic Type Definitions: Types can be defined and managed entirely outside the core game executable. This data could come from dedicated external tools (like a custom monster editor) or a database. For online games, this unlocks particularly interesting possibilities: new monster types or item properties can be introduced or updated over the network, without requiring players to download and install a new game patch. They may not even need to restart their game.
  • Reduced Code and Class Proliferation: Instead of creating numerous C++ classes for slight variations of entities (e.g., GoblinScout, GoblinWarrior, GoblinShaman), you can have a single Entity class and differentiate them through their type objects. This keeps the C++ class hierarchy cleaner and more manageable.

But fundamentally, the core advantage of type object systems is the flexibility - we can build it however we want, to meet the exact requirements of the program we're designing.

  • Do we want our objects to have multiple types? No problem, just design them to have multiple type objects.
  • Do we need some form of inheritance? We can implement that to our exact requirements.
  • Do our objects need to change their type? Easy - we can add a setter to change the value stored in the Breed. We'll implement this later in the lesson.

But that's also the main disadvantage - the need to build and maintain this stuff ourselves.

If we need inheritance-like behaviour for our class, that's easy. The C++ type system already has built inheritance for us and, to access it, we just need to add : public SomeClass to our definition:

class BigGoblin : public Goblin {
public:
  BigGoblin() {
    // Update the inherited members
    Name = "Big Goblin";
    Health = 200;
  }
};

But if we wanted similar capabilities for our type object system, we'd need to build that ourselves:

{
  "Goblin": {
    "name": "Goblin",
    "level": 3,
    "health": 100,
    "damage": 15,
    "armor": 0.2,
    "art": "goblin.png",
  },
  "BigGoblin": {
    // We'd need to write the C++ to implement this
    "Parent": "Goblin", 
    "name": "Big Goblin",
    "health": 200,
  }
}

Writing the code to make this work would take some effort, and it's not a one-time cost. It leaves us with more code we need to maintain, more complexity in our project, and more things that can go wrong.

As always, we should aim to keep things as simple as possible, and only add complexity when we need it, or when we're relatively certain that the benefits will outweigh the costs.

Constructing Typed Objects

There are a couple of common ways we can instantiate our Entity objects and associate them with their MonsterType.

Standard Constructor Injection

The most straightforward method is to pass the MonsterType object (or a reference/pointer to it) directly into the Entity's constructor. This is the approach we've used so far:

// main.cpp
#include <iostream>
#include "MonsterType.h"
#include "Entity.h"

int main() {
  MonsterType GoblinType{
    "Goblin", 3, 100, 15, "goblin.png"
  };

  // Entity constructor takes the MonsterType
  Entity GoblinInstance{GoblinType, 5, 10}; 

  std::cout << "Created a "
    << GoblinInstance.Type.Name
    << " at (" << GoblinInstance.X_Pos
    << ", " << GoblinInstance.Y_Pos
    << ") with " << GoblinInstance.CurrentHealth
    << " HP.\n";
}
Created a Goblin at (5, 10) with 100 HP.

This approach is clear and follows familiar C++ practices for dependency injection. The Entity explicitly declares its need for a MonsterType.

Factory Method on the Type Object

An alternative is to have the MonsterType object itself be responsible for creating instances of entities that conform to its type. We can add a CreateInstance() or Construct() method to the MonsterType struct:

// MonsterType.h
#pragma once
#include <string>
// Forward declaration for Entity to avoid circular
// dependency if Entity.h also includes MonsterType.h
class Entity; 

struct MonsterType {
  std::string Name;
  int Level;
  int MaxHealth;
  int Damage;
  std::string ArtFile;

  // Factory method
  Entity CreateInstance(int x, int y) const; 
};
// MonsterType.cpp
#include "MonsterType.h"
#include "Entity.h"

Entity MonsterType::CreateInstance(
  int x, int y
) const {
  return Entity{*this, x, y};
}
// main.cpp
#include <iostream>
#include "MonsterType.h"
#include "Entity.h"

int main(int argc, char** argv) {
  MonsterType GoblinType{
    "Goblin", 3, 100, 15, "goblin.png"
  };

  Entity GoblinInstance{GoblinType, 5, 10};
  Entity GoblinInstance{
    GoblinType.CreateInstance(5, 10)};

  std::cout << "Created a "
    << GoblinInstance.Type.Name
    << " at (" << GoblinInstance.X_Pos
    << ", " << GoblinInstance.Y_Pos
    << ") with " << GoblinInstance.CurrentHealth
    << " HP.\n";
    
  return 0;
}
Created a Goblin at (5, 10) with 100 HP.

This factory method approach can be useful if the creation logic is complex or if the MonsterType needs to perform some setup on the Entity that goes beyond simple member initialization. It also keeps the creation knowledge localized within the MonsterType.

Both methods are valid, and the choice depends on the specific requirements of your project, and your preferred coding style.

Defining Behaviour

One significant advantage of the traditional C++ class system is how easily it allows us to define distinct behaviors for different types of objects - particularly when we use virtual functions and polymorphism. For example, a Goblin class could have a different Attack() method implementation than a Dragon class.

When we move towards a data-driven Type Object system, defining complex, unique behaviors purely through data becomes more challenging. Unless we integrate a scripting system (like Lua, which we discussed earlier in this chapter) that our type objects can reference or embed, we can't directly write arbitrary "code" in our data files.

However, a practical middle-ground approach is to add data fields to our type objects that influence behavior, which is then interpreted by the C++ Entity class. The behavior itself is still implemented in C++, but it can be parameterized or selected based on data from the type object.

For instance, we could add a FightingStyle property to our MonsterType data:

{
  "Goblin": {
    "name": "Goblin",
    "health": 100,
    "art": "goblin.png",
    "fightingStyle": "Swarm" 
  },
  "Ogre": {
    "name": "Ogre",
    "health": 500,
    "art": "ogre.png",
    "fightingStyle": "BruteForce" 
  }
}

Our MonsterType struct in C++ would then include this new field:

// MonsterType.h
#pragma once
#include <string>
class Entity;

enum class FightingStyle {
  Swarm, BruteForce, RangedSnipe
};

struct MonsterType {
  std::string Name;
  int Level;
  int MaxHealth;
  int Damage;
  std::string ArtFile;
  FightingStyle Style;

  Entity CreateInstance(int x, int y) const;
};

And our Entity class can have a method, say PerformCombatAction(), that uses this FightingStyle property to decide what to do:

// Entity.h
#pragma once
#include <iostream>
#include "MonsterType.h"

class Entity {
public:
  Entity(const MonsterType& Type, int x, int y)
    : Type{Type}, X_Pos{x}, Y_Pos{y},
      CurrentHealth{Type.MaxHealth} {}

  const MonsterType& Type;
  int X_Pos;
  int Y_Pos;
  int CurrentHealth;

  void PerformCombatAction() const {
    std::cout << Type.Name << " considers its "
      "action...\n";

    using enum FightingStyle;
    if (Type.Style == Swarm) {
      std::cout << "  It attempts to overwhelm "
        "with numbers!\n";
    } else if (Type.Style == BruteForce) {
      std::cout << "  It charges with brute "
        "force!\n";
    } else if (Type.Style == RangedSnipe) {
      std::cout << "  It looks for a sniping "
        "opportunity!\n";
    } else {
      std::cout << "  It attacks normally.\n";
    }
  }
};

In our main function:

// main.cpp
#include "MonsterType.h"
#include "Entity.h"

int main(int argc, char** argv) {
  using enum FightingStyle;
  MonsterType GoblinType{
    "Goblin", 3, 100, 15, "goblin.png", Swarm
  };

  MonsterType OgreType{
    "Ogre", 5, 300, 25, "ogre.png", BruteForce
  };

  Entity Goblin{
    GoblinType.CreateInstance(5, 10)};
  Entity Ogre{OgreType.CreateInstance(10, 6)};

  Goblin.PerformCombatAction();
  Ogre.PerformCombatAction();
  
  return 0;
}
Goblin considers its action...
  It attempts to overwhelm with numbers!
Ogre considers its action...
  It charges with brute force!

This way, designers can change the FightingStyle in the data to alter how entities behave, without touching C++ code. The range of behaviors is still defined in C++, but their selection and parameterization can be data-driven.

This approach provides a good balance between flexibility and the complexity of implementing a full scripting system. We could extend this further with more data fields, like a Spellbook array that lists the combat abilities the type has access to, or MovementPattern to represent how the type moves.

Changing Types

One of the defining capabilities of type objects is that they give us the ability to change an object's type at runtime.

C++ doesn't let us change the class of an object at run time, but it does let us change what is stored in a member variable, and a type object is just another variable. In the following example, we've added a SetType() function to our Entity class.

We've also changed the Type member from being a MonsterType reference to a MonsterType pointer, as references cannot be updated to point to other objects. The logic we write may also want to consider the the possibility the object having no type at all - that is, Type being a nullptr:

// Entity.h
#pragma once
#include <iostream>
#include "MonsterType.h"

class Entity {
public:
  // Constructor now takes a pointer to MonsterType
  Entity(const MonsterType* Type, int x, int y) 
    : Type{Type}, X_Pos{x}, Y_Pos{y},
      // Added null check and switched from
      // the . to -> operator
      CurrentHealth{Type ? Type->MaxHealth : 0} 
  {}

  // Changed type to pointer
  const MonsterType* Type; 

  void ChangeType(const MonsterType* NewType) {
    if (!NewType) return; // Or handle error

    std::cout << X_Pos << "," << Y_Pos 
      << ": " << Type->Name
      << " is transforming into a "
      << NewType->Name << "!\n";
    Type = NewType;

    // Adjust health based on the new type.
    // For example, cap current health at new
    // max health, or fully heal to new max health.

    // Let's set it to the new type's max health
    // for this example.
    CurrentHealth = Type->MaxHealth;
  }

  int X_Pos;
  int Y_Pos;
  int CurrentHealth;
};
// MonsterType.cpp
#include "MonsterType.h"
#include "Entity.h"

Entity MonsterType::CreateInstance(
  int x, int y
) const {
  // Change argument from reference to pointer
  return Entity{*this, x, y};
  return Entity{this, x, y};
}

We can now dynamically change our object's type at run time:

// main.cpp
#include "MonsterType.h"
#include "Entity.h"

int main(int argc, char** argv) {
  using enum FightingStyle;
  MonsterType GoblinType{
    "Goblin", 3, 100, 15, "goblin.png", Swarm
  };

  MonsterType DragonType{
    "Dragon", 7, 500, 45, "dragon.png", RangedSnipe
  };

  Entity Goblin{
    GoblinType.CreateInstance(5, 10)};

  Goblin.ChangeType(&DragonType);
  
  return 0;
}
5,10: Goblin is transforming into a Dragon!

In type object systems, it's generally the case that changing this value will have signicant impacts on the behaviour of the object. Naturally, the extent of this will depend on how we've designed our program.

In the following example, our object initially renders as a goblin (which we've simulated through some logging) and, after a type change, it renders itself as a dragon:

// Entity.h
#pragma once
#include <iostream>
#include "MonsterType.h"

class Entity {
public:
void Render() { if (!Type) { return; // Or handle error } std::cout << X_Pos << "," << Y_Pos << ": Rendering " << Type->Name << " using art file: " << Type->ArtFile << "\n"; } };

Let's see this in action:

// main.cpp
#include "MonsterType.h"
#include "Entity.h"

int main(int argc, char** argv) {
  using enum FightingStyle;
  MonsterType GoblinType{
    "Goblin", 3, 100, 15, "goblin.png", Swarm
  };

  MonsterType DragonType{
    "Dragon", 7, 500, 45, "dragon.png",
    RangedSnipe
  };

  Entity Enemy{
    GoblinType.CreateInstance(5, 10)};

  Enemy.Render();
  Enemy.ChangeType(&DragonType);
  Enemy.Render();
  
  return 0;
}
5,10: Rendering Goblin using art file: goblin.png
5,10: Goblin is transforming into a Dragon!
5,10: Rendering Dragon using art file: dragon.png

Object Pools

Even if the program we're making has no user-facing requirement for objects to change type, having this capability can still be useful for our behind-the-scenes implementation.

In more complex programs, we tend to avoid excessive creation and deletion of objects, as creating a complex object like a typical game entity can be quite expensive. Additionally, the constant churn of object deletion and creation over the lifetime of our program's execution can cause the system's memory to get fragmented, further degrading performance.

Instead of deleting objects, we often prefer to hide and disable them in some way, thereby allowing us to reuse those objects again later. In some older games, this was literally implemented as moving the object behind a wall, or under the floor, so the player can't see it.

Later in our game, when we need an object of the same class, we just pick one of those disabled objects, bring it back to life, and move it to wherever it's needed. This technique is commonly called object pooling. We implement it by having a fixed-size collection (or "pool") of available game objects neatly organized in memory. When we need a "new" game object somewhere in our world, we can repurpose an object from that pool rather than create a new one.

A flexible type object systems naturally lends itself to this technique. If all our game objects have the exact same class - like Entity - then object pooling and memory optimization in general is much easier.

For example, a player might kill a goblin early in the dungeon, open a treasure chest in the middle, then fight a dragon at the end. But, behind the scenes, the goblin, treasure chest, and dragon may have been the exact same object. It was simply recycled and given a different type object between each encounter.

Later in the course, we'll introduce the entity-component-system (ECS) design pattern. This gives us yet more options for significantly changing the behaviour of our objects at run time.

Complete Code

A complete version of the files we created in this lesson is available below:

Files

main.cpp
Entity.h
MonsterType.h
MonsterType.cpp
Select a file to view its content

Summary

This lesson introduced the Type Object design pattern as a way to define an object's characteristics using external data rather than hardcoding them into C++ classes. We explored how this pattern separates an entity's fixed class from its variable, data-defined traits, leading to more flexible and maintainable game designs.

By using type objects, we can empower designers and even players to modify game content without recompiling code.

Key Takeaways:

  • Type Objects allow shared characteristics of entities to be defined by data.
  • This pattern decouples an entity's C++ class from its specific "kind" or "breed."
  • Changes to entity types can be made externally, speeding up iteration.
  • Type Objects facilitate data-driven design, making games more moddable.
  • Behaviors can be influenced by data in type objects, offering a balance with C++ implemented logic.
  • Entities can potentially change their type object at runtime, enabling dynamic transformations.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

View Course
Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

View Full CourseGet Started for Free
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 120 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

View Course
Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved