Sandbox
Define behavior in a subclass using a set of operations provided by its base class.
Let’s say we want to create a class template that implements all superpowers. Here are some cons:
We can create a base class called Superpower
that has
protected non-virtual methods to communicate that they
are meant to be called by subclasses.
This is the Subclass Sandbox Pattern, which has a wide and shallow class hierarchy.
A base class defines an abstract sandbox method and several provided operations. Marking them protected makes it clear that they are for use by derived classes. Each derived sandboxed subclass implements the sandbox method using the provided operations.
Inheritance isn’t thought of kindly because base classes tend to accumulate lots of code.
With this pattern, since every subclass goes through its base class to reach the rest of the game, you can end up with the brittle base class problem.
If you get spaghetti code from this pattern, try turning to the component pattern.
Let’s make the Superpower base class:
class Superpower {
public:
virtual ~Superpower() {}
protected:
virtual void activate() = 0;
void move(double x, double y, double z) {
// Code here...
}
void playSound(SoundId sound, double volume) {
// Code here...
}
void spawnParticles(ParticleType type, int count) {
// Code here...
}
};
The active method is protected and abstract, which signals that subclasses must override it.
The other protected methods are the provided operations. These
methods will be called in activate
.
Making a superpower goes like this:
class SkyLaunch : public Superpower {
protected:
virtual void activate() {
(SOUND_SPROING, 1.0f);
playSound(PARTICLE_DUST, 10);
spawnParticles(0, 0, 20);
move}
};
Let’s say we want to add some control flow based on the hero’s location. We can add that to the Superpower:
class Superpower {
protected:
double getHeroX() {
// Code here...
}
double getHeroY() {
// Code here...
}
double getHeroZ() {
// Code here...
}
// Existing stuff...
};
Now that we can access state, we can make some interesting stuff happen:
class SkyLaunch : public Superpower {
protected:
virtual void activate() {
if (getHeroZ() == 0) {
// On the ground, so spring into the air.
(SOUND_SPROING, 1.0f);
playSound(PARTICLE_DUST, 10);
spawnParticles(0, 0, 20);
move}
else if (getHeroZ() < 10.0f) {
// Near the ground, so do a double jump.
(SOUND_SWOOP, 1.0f);
playSound(0, 0, getHeroZ() + 20);
move}
else {
// Way up in the air, so do a dive attack.
(SOUND_DIVE, 0.7f);
playSound(PARTICLE_SPARKLES, 1);
spawnParticles(0, 0, -getHeroZ());
move}
}
};
This is a fairly small pattern that has a lot of flexibility.
Should we add methods to the base class?
Should calls modify state?
Let’s say that we want to add more methods to the base class to play music:
class Superpower {
protected:
void playSound(SoundId sound, double volume) {
// Code here...
}
void stopSound(SoundId sound) {
// Code here...
}
void setVolume(SoundId sound) {
// Code here...
}
// Sandbox method and other operations...
};
Instead of adding it directly to the Superpower class, why don’t we create a new class that encapsulates it:
class SoundPlayer {
void playSound(SoundId sound, double volume) {
// Code here...
}
void stopSound(SoundId sound) {
// Code here...
}
void setVolume(SoundId sound) {
// Code here...
}
};
And have the Superpower manage access to it.
class Superpower {
protected:
& getSoundPlayer() {
SoundPlayerreturn soundPlayer_;
}
// Sandbox method and other operations...
private:
soundPlayer_;
SoundPlayer };
This is the simplest way:
class Superpower {
public:
(ParticleSystem* particles)
Superpower: particles_(particles)
{}
// Sandbox method and other operations...
private:
* particles_;
ParticleSystem};
But now all derived classes will need a constructor that calls the base class one and passes along that argument. This is a maintenance headache.
class SkyLaunch : public Superpower {
public:
(ParticleSystem* particles)
SkyLaunch: Superpower(particles)
{}
};
To avoid passing everything through the constructor, we can split initialization into two steps:
* power = new SkyLaunch();
Superpower->init(particles); power
This lets us initialize the superpower base class and the derived class at different times. Let’s create a helper function so we don’t forget to initialize both at the same time.
* createSkyLaunch(ParticleSystem* particles) {
Superpower* power = new SkyLaunch();
Superpower->init(particles);
powerreturn power;
}
If some systems required are singletons, we can make initialization static:
class Superpower {
public:
static void init(ParticleSystem* particles) {
particles_ = particles;
}
// Sandbox method and other operations...
private:
static ParticleSystem* particles_;
};
This makes it so every instance of Superpower doesn’t have to store
its own instance of particles, so this uses up less memory. Of course,
we’ll need to call Superpower::init()
before we can use
it.
class Superpower {
protected:
void spawnParticles(ParticleType type, int count) {
& particles = Locator::getParticles();
ParticleSystem.spawn(type, count);
particles}
// Sandbox method and other operations...
};
Prev: bytecode Next: type-object