A real taste of object oriented Design (Battlebeyz 4)
The class declarations below are lifted from the repo on the dates given. Last updated 2026-09-17.
Well, things are moving quite well along with my spinning top game project. The physics came out quite nicely, now that all the units, time, and constants are scaled correctly. And this basic implementation only took a hundred hours of debugging! Woo, go figure.
Now that brings me to today’s topic of everyone’s favorite: OOP. I’m now at the stage where I am starting to need to reevaluate my design choices when developing new features. Here’s the scenario.
”Just add a name to the menu”
This all started from me wanting to add names to beyblades and display them on a menu before the beyblade launch. Should be simple enough. I had a GameControl struct for passing variables around with the GLFW window, and I could just add a name field to Beyblade. Here is what Beyblade looked like at the time, trimmed to the parts that matter:
// September 10, 2024
class GameObject {
public:
virtual void initializeMesh() = 0;
virtual void render(ShaderProgram& shader, const glm::vec3& lightColor, const glm::vec3& lightPos) = 0;
protected:
std::vector<glm::vec3> vertices, normals, tangents, colors;
std::vector<glm::vec2> texCoords;
std::vector<unsigned int> indices;
unsigned int VAO, VBO, EBO;
};
class Beyblade : public GameObject {
// ...
std::unordered_map<std::string, glm::vec3> materialColors;
BeybladeBody* rigidBody;
std::string name;
};
class PhysicsWorld {
std::vector<BeybladeBody*> beybladeBodies;
std::vector<StadiumBody*> stadiumBodies;
};
The name went in fine. The problem was getting it back out. The launch menu needed to list every bey in the match, and the only thing that knew about every bey was PhysicsWorld, which by design stored rigid bodies and nothing else. I could add name at the rigid body level, but that would couple the physics elements with non-physics elements, and at least that part was handled well. I could create a separate ObjectWorld to hold all the objects, but the objects already contain their rigid bodies, so that’s the same list twice.

There’s a fine balance between the amount and complexity of changes caused by an adjustment in structure, and the long-term benefits it would have in organization. I’m lucky to both know my code extremely well and have a project that is constantly shifting and not an extremely complex legacy. In this case, I evaluated that it would be best to bite the small amount of extra space and tweaking involved in changing PhysicsWorld to hold objects instead of just their rigid bodies.
Cascading stupid syndrome
Ok, let’s do that. But waaait. Most of the fields in Beyblade are for the mesh, and name doesn’t seem to fit in anywhere as a more general variable. If I’m already separating the physics into BeybladeBody and StadiumBody, why am I not doing the same for the mesh? I almost forgot, but… it’s inheriting from GameObject for mesh-related functionality? That doesn’t make sense. Why is it called GameObject and not MeshObject???
This is a phenomenon I dub the cascading stupid syndrome (CSS).
It’s not just normal to spot another flaw or inconsistency after inspecting an existing one. It’s ubiquitous. The uninformed approach I took to managing game objects previously was now back to bite, once again. I guess I might as well get into that one too.
Look at GameObject above. It is a bag of vertex arrays and three GL handles with two pure virtual functions on top. Nothing in it is about being a game object. Beyblade and Stadium each overrode the two virtuals, and the overrides differed by very little: Beyblade’s render walked a materialColors map so that a multi-material model could be drawn with the right colors per part, and Stadium’s didn’t. That is the whole reason the hierarchy existed. Two subclasses, one small difference, and a base class whose name was a lie.
I have since watched enough tech content and read enough articles to know that inheritance fricking sucks for this. What was I thinking? The rendering pieces belong together in one thing that a Beyblade has, not something a Beyblade is. So on September 14 the mesh data moved out into its own class, the same way the physics already had:
// October 19, 2024
class Beyblade {
friend class BeybladeMesh;
friend class BeybladeBody;
public:
void render(ShaderProgram& shader);
BeybladeBody* getRigidBody() { return rigidBody; }
BeybladeMesh* getMesh() { return mesh; }
std::string getName() const { return name; }
private:
BeybladeBody* rigidBody;
BeybladeMesh* mesh;
std::string name;
};
class PhysicsWorld {
std::vector<Beyblade*> beyblades;
std::vector<Stadium*> stadiums;
};
A Beyblade is now a name, a body, and a mesh. PhysicsWorld holds Beyblades and reaches through to the body when it needs physics, and the launch menu reaches through the same list for the names. The materialColors map lives in BeybladeMesh, where it only has to exist for the one class that uses it. Stadium got the identical split into StadiumBody and StadiumMesh. I did briefly worry about performance from the extra indirection, but realistically nothing measurable was lost, and the code got a lot easier to move around.
Two things I would still change, in fairness. Those raw pointers became std::unique_ptr in December 2024 once I trusted myself with ownership. The two friend declarations are still there as of 2026, which tells you the mesh and body still poke at Beyblade’s insides more than they should.
The state machine, three times
The same month taught the same lesson from a different direction. I think it’s quite poetic how the game’s notion of “what screen are we on” went through three designs in about six weeks.
The first was booleans in GameControl, the struct that got passed around with the window:
// September 10, 2024
struct GameControl {
bool showHomeScreen;
bool showInfoScreen;
bool showCustomizeScreen;
bool showAboutScreen;
// ...
};
Four flags, so sixteen combinations, of which four are meaningful. Every screen transition had to remember to clear the other three, and the render loop was a chain of ifs. Pause landed on September 14 as one more field bolted onto this struct, a ProgramState enum sitting next to the booleans that already said the same thing. A pause screen is a screen on top of another screen, and neither a flag nor an enum can express “on top of”.
The second, from October 13, was a StateManager holding a single polymorphic state:
class StateManager {
void ChangeState(std::unique_ptr<GameState> newState) {
if (currentState) currentState->cleanup();
currentState = std::move(newState);
if (currentState) currentState->init();
}
std::unique_ptr<GameState> currentState;
};
Better: the impossible combinations are gone and each state owns its own init and cleanup. But it still can’t pause, because changing state destroys the match.
The third, by October 19, was a stack in GameEngine:
class GameEngine {
void changeState(GameStateType stateType);
void pushState(GameStateType stateType);
void popState();
std::vector<std::unique_ptr<GameState>> stateStack;
};
Push the pause state, the match underneath keeps its memory and stops updating. Pop it, the match resumes. The states are created from an enum through a factory so nothing outside the engine ever names a concrete state class. This is the standard answer, and I could have read it in any game programming book, but I would not have understood why it’s shaped that way if I hadn’t first tried to pause a game held together with four booleans.
What I actually learned
I guess the lesson here is that OOD sucks, but the real lesson is more specific than that. Inheritance was the wrong tool because the things I was modeling didn’t share behavior, they shared parts, and parts want composition. The feature request was the thing that found the seam: nothing about “add a name” is architectural, and yet it was impossible without moving the boundary between physics and everything else.
The second lesson is that making a large, significant project is one of the best ways for you to feel the effects of your architectural decisions. Trying to take your implementation and running it against the wall of feasible complexity teaches you more about why your ideas don’t work than any lecture ever could.