Flyweights, Prototypes and Type Objects

Inheritance FTW

There must be monsters

Quick! Write me a MMORPG with hundreds of monsters, spawning from various caves, pits, nests and whatnot!

Sure thing boss, I've got a degree in DAE:*

class Monster
{
    int _health;
    float _speed;

    //... lots of other data

public:
    Monster(int health, float speed);
    virtual ~Monster() = default;

    virtual const char* GetWeapon() = 0;

    //... lots of other methods
};

*or a 3rd year student in their internship

Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be monsters

class Troll : public Monster
{
public:
    Troll() : Monster(50, 2) {}

    virtual const char* GetWeapon() override {
        return "Club";
    }
};
class Dragon : public Monster
{
public:
    Dragon() : Monster(200, 20) {}

    virtual const char* GetWeapon() override {
        return "FireBreathing";
    }
};
class Goblin : public Monster
{
public:
    Goblin() : Monster(30, 3) {}

    virtual const char* GetWeapon() override {
        return "Dagger";
    }
};
class Orc : public Monster
{
public:
    Orc() : Monster(100, 5) {}

    virtual const char* GetWeapon() override {
        return "Axe";
    }
};
Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be monsters

class Vampire : public Monster
{
public:
    Vampire() : Monster(80, 4) {}

    virtual const char* GetWeapon() override {
        return "Bite";
    }
};
class Zombie : public Monster
{
public:
    Zombie() : Monster(60, 1) {}

    virtual const char* GetWeapon() override {
        return "Claws";
    }
};
class Wizard : public Monster
{
public:
    Wizard() : Monster(50, 8) {}

    virtual const char* GetWeapon() override {
        return "Magic";
    }
};
class Giant : public Monster
{
public:
    Giant() : Monster(300, 2) {}

    virtual const char* GetWeapon() override {
        return "Rock Throwing";
    }
};
Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be monsters

class Phoenix : public Monster
{
public:
    Phoenix() : Monster(150, 6) {}

    virtual const char* GetWeapon() override {
        return "Fireball";
    }
};
class Werewolf : public Monster
{
public:
    Werewolf() : Monster(90, 7) {}

    virtual const char* GetWeapon() override {
        return "Claws and Fangs";
    }
};
class Mummy : public Monster
{
public:
    Mummy() : Monster(120, 2) {}

    virtual const char* GetWeapon() override {
        return "Wrapped Curse";
    }
};
class Centaur : public Monster
{
public:
    Centaur() : Monster(80, 6) {}

    virtual const char* GetWeapon() override {
        return "Bow and Arrow";
    }
};

Etcetera, etcetera,... (hundreds!)

Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be monsters

I asked ChatGPT for an UML diagram but...

center

Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be classes

You catch my drift: lots of lots of lots of classes.

And then the game designers start to yap: "The dragons are too strong can you half their health", or "The vampires weapon should be Fangs not Bite", ...

So your life is now:

  • Get email from the designer asking for a change
  • Check out the file and make the change
  • Recompile the game, run tests
  • Submit the change
  • Reply the email
  • Repeat.

Very contrived example of course, but what is the main issue and how do we solve it?

  • (hint: the designers stay alive)
Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There must be spawning

We're not done yet, we needed to spawn them too!

Easy peasy lemon sqeezy:

class Spawner
{
public:
    virtual ~Spawner() = default;
    virtual Monster* SpawnMonster() = 0;

    //... some additional code calling the SpawnMonster function in due time
};
Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be spawners

class TrollSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Troll();
    }
};
class DragonSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Dragon();
    }
};
class GoblinSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Goblin();
    }
};
class OrcSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Orc();
    }
};
Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be spawners

class VampireSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Vampire();
    }
};
class ZombieSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Zombie();
    }
};
class WizardSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Wizard();
    }
};
class GiantSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Giant();
    }
};
Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

There be spawners

class PhoenixSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Phoenix();
    }
};
class WerewolfSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Werewolf();
    }
};
class MummySpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Mummy();
    }
};
class CentaurSpawner : public Spawner
{
public:
    virtual Monster* SpawnMonster() override
    {
        return new Centaur();
    }
};

Hundreds! - You're laughing but this is students' code

Programming 4 - Flyweights, Prototypes and Type Objects
Inheritance FTW

Easy peasy lemon sqeezy

Just use templates! Obviously!

template <typename T>
class Spawner final
{
public:
    static_assert(std::is_base_of<Monster,T>::value, 
        "T must derive from Monster");

    Monster* SpawnMonster()
    {
        return new T();
    }
};
using TrollSpawner = Spawner<Troll>;
using DragonSpawner = Spawner<Dragon>;
using GoblinSpawner = Spawner<Goblin>;
using OrcSpawner = Spawner<Orc>;
using VampireSpawner = Spawner<Vampire>;
using ZombieSpawner = Spawner<Zombie>;
using WizardSpawner = Spawner<Wizard>;
using GiantSpawner = Spawner<Giant>;
using PhoenixSpawner = Spawner<Phoenix>;
using WerewolfSpawner = Spawner<Werewolf>;
using MummySpawner = Spawner<Mummy>;
using CentaurSpawner = Spawner<Centaur>;
//...

Yes, but in what issue do we run again?

Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Enter composition

Add components?

Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Type Object

Allow the flexible creation of new "classes" by creating a class, each instance of which represents a different type of object.

Instead of having a class for each new breed of monster, we say a monster has a breed.

center

  • No inheritance
  • 2 classes

Define a type object class and a typed object class. Each type object instance represents a different logical type. Each typed object stores a reference to the type object that describes its type.

Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Type Object

class Monster
{
    int _health;
    Breed& _breed;

public:
    Monster(Breed& breed) :
    _health(breed.GetHealth()), _breed(breed) {}

    const char* GetWeapon() const { 
        return _breed.GetWeapon(); 
    }

    //... lots of other methods
};
class Breed
{
    int _health;
    float _speed;
    const char* _weapon;
public:
    Breed(const char* weapon, int health, 
        float speed) : _weapon(weapon), 
        _health(health), _speed(speed) {}

    int GetHealth() const { return _health; }    
    float GetSpeed() const { return _speed; }
    const char* GetWeapon() const { 
        return _weapon; 
    }
};
Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Type Object

Making type objects more like types: "constructors"

class Monster
{
    friend class Breed;
    int _health;
    Breed& _breed;

    Monster(Breed& breed) :
    _health(breed.GetHealth()), _breed(breed) {}

public:
    const char* GetWeapon() const { 
        return _breed.GetWeapon(); 
    }

    //... lots of other methods
};
class Breed
{
    int _health;
    float _speed;
    const char* _weapon;
public:
    Breed(const char* weapon, int health, 
        float speed) : _weapon(weapon), 
        _health(health), _speed(speed) {}

    Monster* CreateMonster() { 
        return new Monster(*this); 
    }

    int GetHealth() const { return _health; }    
    float GetSpeed() const { return _speed; }
    const char* GetWeapon() const { 
        return _weapon; 
    }
};
Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Type Object considerations

Use it when:

  • At compile time it's impossible to know what types there are going to be.
  • You want to change types at runtime (add/edit/remove)

But:

  • You need to maintain your own type system now, creating them, loading them into memory, unloading them when they're no longer required, etc
  • How will you define behaviour for each type? Suggestions?

Can the type change at runtime?

  • Could be useful for an object pool of monsters

Do we support inheritance?

  • A breed could refer to a parent breed
    • To create Orc Zombies for example
Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Still need that spawning

Template version no longer works

using TrollSpawner = Spawner<Troll>;
//...

How then?

Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Still need that spawning

Template version no longer works

using TrollSpawner = Spawner<Troll>;
//...

Yes, composition, via the prototype pattern.

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Prototype

We add a clone function to the Monster and use that in the Spawner:

class Monster
{
    friend class Breed;
    int _health;
    Breed& _breed;

    Monster(Breed& breed) :
    _health(breed.GetHealth()), _breed(breed) {}

public:
    const char* GetWeapon() const { 
        return _breed.GetWeapon(); 
    }

    Monster* Clone()
    {
        auto result = new Monster(_breed)
        result->_health = _health;
        return result;
    }

    //... lots of other methods
};
class Spawner final
{
    Monster* _prototype;
public:
    Spawner(Monster* prototype) : 
        _prototype(prototype) {}

    Monster* SpawnMonster()
    {
        return _prototype->Clone();
    }
};
Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Cloneable

Often happens via an interface (exists by default in .Net and Java)

class ICloneable
{
public:
    virtual ~ICloneable() = default;
    virtual ICloneable* Clone() = 0;
}

But why a Clone function? Why not simply use the copy constructor?

Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

More data

Let's forget the Breed class for a moment and have a look at that "lots of other data"

class Monster
{
    int _health;
    float _speed;

    Mesh _mesh;
    Shader _shader;
    Texture2D _albedo;
    Texture2D _normal;
    Color _albedoColor;
    Color _teamColor;
    Transform _transform;
    
    //... still lots of other data, like animations, sound, and whatnot.

public:
    Monster(int health, float speed);
    virtual ~Monster() = default;

    virtual const char* GetWeapon() = 0;
};
Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Composition FTW

Components? Perhaps, or:

class Monster
{
    int _health;
    float _speed;

    MonsterModel* _model;
    Color _teamColor;
    Transform _transform;

public:
    Monster(int health, float speed);
    virtual ~Monster() = default;

    virtual const char* GetWeapon() = 0;
};
class MonsterModel
{
    Mesh _mesh;
    Shader _shader;
    Texture2D _albedo;
    Texture2D _normal;
    Color _albedoColor;
}

One set of model data can now be used by all Monsters of the same type.

Programming 4 - Flyweights, Prototypes and Type Objects
Composition FTW

Flyweight

Use sharing to support large numbers of fine-grained objects efficiently

The MonsterModel contains the shared data of the monsters that look the same.

  • This data is often immutable.
  • This data is called the intrinsic state of the instance.
  • The other data, unique to the instance, is the extrinsic state.

Considerations

  • What popular example do you know of this pattern?
  • If we put everything together, a Monster now has a Breed and a MonsterModel - can we combine these?
  • Both the Flyweight and Type Object pattern go well together with Object Pool.
Programming 4 - Flyweights, Prototypes and Type Objects

- Inheritance and composition is better. But out of what do we compose? Not components again... - Also: we should be data driven

an interface, start of the Factory Method pattern

this is all defined at compile time, we need it at run time, aka data driven

No, composition is not only components

Notice how these two classes cover all the previous code

Here we just return a new object, but we could add a lot of other initialization here => this is a factory method after all.

Behaviour can via id's to some sort behaviour/ai like "patrolling" or something. Better: byte code pattern -> lua Changing types enabled object pools, just swap the breed, but that has a lot of other implications Without inheritance we might be copy-pasting a lot of data.

The copy constructor is not virtual, we need to know the type of what we copy.

Having this data copied for every monster is obviously not the best idea - lots of mem - doesn't fit in cacheline anymore Components again?

In the book, it's called "context-free" instead of intrinsic.

Instanced rendering

Yes and no, it's a design choice. The intent between the two is different.