I prefer composition to convoluted OOP hierarchies, so whenever I’m doing something a little larger than your average demo, I rely on a component based architecture.
I would have an Entity, or more generally a GameObject class, which would basically serve as a component container, running each active component on every tick. The Component class is just a template for a simple state machine, running currently active states.
It’s not a magic bullet, but I think it leads to far more manageable code.
You still have to think about the interplay between Entity and Component, and how to write generic components that work well together.
Generally, it helps to keep your components completely unaware of anything but the data and methods available in the Entity base class. So, if you have a PlayerMovement component, and the ability to jump when on ground, you don’t want to encode your algorithm for detecting “ground” in the component itself. Instead, you should define the onGround method on the Entity subclass, and pass that to the component.
That way, if you want to control some other object, where the method for detecting “on ground” is slightly different, you don’t have to change anything - just pass in a new function.
Simple example, I know, but even small things like that can translate into a big win.
Whatever you decide to do, don’t over-engineer. Do what makes sense for your current needs, with some thought given to how your needs might evolve, but know that you can’t make a perfect plan from the very start.
As you work on the system, you’ll hit moments of clarity, and whenever you do, you should basically rewrite the system to fit your new insights.
I know that people are reluctant to do that, especially when they have a whole heap of code written up already (and they spent so much time planning it out), but really, “doing it right”, even if it takes a long time, is usually much faster than struggling with a system that is clearly not fit for your new circumstances (that’s a struggle that can easily stretch out into years - which ultimately turn out to be years wasted).
On this page, Michael Abrash writes about his experiences working with John Carmack, on Quake, where Carmack basically followed this “scorched earth” policy: If something doesn’t seem to fit, it’s just thrown out, no matter how long and hard it took to initially develop.
BTW: If you didn’t read “Masters Of Doom”, by David Kushner - I highly recommend it.