Previous slide Next slide Toggle fullscreen Open presenter view
Flyweights, Prototypes and Type Objects
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;
public :
Monster (int health, float speed);
virtual ~Monster () = default ;
virtual const char * GetWeapon () = 0 ;
};
*or a 3rd year student in their internship
Programming 4 - Flyweights, Prototypes and Type Objects
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
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
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
There be monsters
I asked ChatGPT for an UML diagram but...
Programming 4 - Flyweights, Prototypes and Type Objects
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
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 ;
};
Programming 4 - Flyweights, Prototypes and Type Objects
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
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
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
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
Enter composition
Add components?
Programming 4 - Flyweights, Prototypes and Type Objects
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.
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
Type Object
class Monster
{
int _health;
Breed& _breed;
public :
Monster (Breed& breed) :
_health(breed.GetHealth ()), _breed(breed) {}
const char * GetWeapon () const {
return _breed.GetWeapon ();
}
};
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
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 ();
}
};
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
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
Still need that spawning
Template version no longer works
using TrollSpawner = Spawner<Troll>;
How then?
Programming 4 - Flyweights, Prototypes and Type Objects
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
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;
}
};
class Spawner final
{
Monster* _prototype;
public :
Spawner (Monster* prototype) :
_prototype(prototype) {}
Monster* SpawnMonster ()
{
return _prototype->Clone ();
}
};
Programming 4 - Flyweights, Prototypes and Type Objects
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
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;
public :
Monster (int health, float speed);
virtual ~Monster () = default ;
virtual const char * GetWeapon () = 0 ;
};
Programming 4 - Flyweights, Prototypes and Type Objects
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
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.