I am designing a class Master
that is composed from multiple other classes, A
, Base
, C
and D
. These four classes have absolutely no use outside of Master
and are meant to split up its functionality into manageable and logically divided packages. They also provide extensible functionality as in the case of Base
, which can be inherited from by clients.
But, how do I maintain encapsulation of Master
with this design?
So far, I’ve got two approaches, which are both far from perfect:
1. Replicate all accessors:
Just write accessor-methods for all accessor-methods of all classes that Master
is composed of.
This leads to perfect encapsulation, because no implementation detail of Master
is visible, but is extremely tedious and makes the class definition monstrous, which is exactly what the composition should prevent. Also, adding functionality to one of the composees (is that even a word?) would require to re-write all those methods in Master
. An additional problem is that inheritors of Base
could only alter, but not add functionality.
2. Use non-assignable, non-copyable member-accessors:
Having a class accessor<T>
that can not be copied, moved or assigned to, but overrides the operator->
to access an underlying shared_ptr
, so that calls like
Master->A()->niceFunction();
are made possible. My problem with this is that it kind of breaks encapsulation as I would now be unable to change my implementation of Master
to use a different class for the functionality of niceFunction()
. Still, it is the closest I’ve gotten without using the ugly first approach. It also fixes the inheritance issue quite nicely. A small side question would be if such a class already existed in std
or boost
.
EDIT: Wall of code
I will now post the code of the header files of the classes discussed. It may be a bit hard to understand, but I’ll give my best in explaining all of it.
1. GameTree.h
The foundation of it all. This basically is a doubly-linked tree, holding GameObject
-instances, which we’ll later get to. It also has it’s own custom iterator GTIterator
, but I left that out for brevity. WResult
is an enum with the values SUCCESS
and FAILED
, but it’s not really important.
class GameTree
{
public:
//Static methods for the root. Only one root is allowed to exist at a time!
static void ConstructRoot(seed_type seed, unsigned int depth);
inline static bool rootExists(){ return static_cast<bool>(rootObject_); }
inline static weak_ptr<GameTree> root(){ return rootObject_; }
//delta is in ms, this is used for velocity, collision and such
void tick(unsigned int delta);
//Interaction with the tree
inline weak_ptr<GameTree> parent() const { return parent_; }
inline unsigned int numChildren() const{ return static_cast<unsigned int>(children_.size()); }
weak_ptr<GameTree> getChild(unsigned int index) const;
template<typename GOType>
weak_ptr<GameTree> addChild(seed_type seed, unsigned int depth = 9001){
GOType object{ new GOType(seed) };
return addChildObject(unique_ptr<GameTree>(new GameTree(std::move(object), depth)));
}
WResult moveTo(weak_ptr<GameTree> newParent);
WResult erase();
//Iterators for for( : ) loop
GTIterator& begin(){
return *(beginIter_ = std::move(make_unique<GTIterator>(children_.begin())));
}
GTIterator& end(){
return *(endIter_ = std::move(make_unique<GTIterator>(children_.end())));
}
//unloading should be used when objects are far away
WResult unloadChildren(unsigned int newDepth = 0);
WResult loadChildren(unsigned int newDepth = 1);
inline const RenderObject& renderObject() const{ return gameObject_->renderObject(); }
//Getter for the underlying GameObject (I have not tested the template version)
weak_ptr<GameObject> gameObject(){
return gameObject_;
}
template<typename GOType>
weak_ptr<GOType> gameObject(){
return dynamic_cast<weak_ptr<GOType>>(gameObject_);
}
weak_ptr<PhysicsObject> physicsObject() {
return gameObject_->physicsObject();
}
private:
GameTree(const GameTree&); //copying is only allowed internally
GameTree(shared_ptr<GameObject> object, unsigned int depth = 9001);
//pointer to root
static shared_ptr<GameTree> rootObject_;
//internal management of a child
weak_ptr<GameTree> addChildObject(shared_ptr<GameTree>);
WResult removeChild(unsigned int index);
//private members
shared_ptr<GameObject> gameObject_;
shared_ptr<GTIterator> beginIter_;
shared_ptr<GTIterator> endIter_;
//tree stuff
vector<shared_ptr<GameTree>> children_;
weak_ptr<GameTree> parent_;
unsigned int selfIndex_; //used for deletion, this isn't necessary
void initChildren(unsigned int depth); //constructs children
};
2. GameObject.h
This is a bit hard to grasp, but GameObject
basically works like this:
When constructing a GameObject
, you construct its basic attributes and a CResult
-instance, which contains a vector<unique_ptr<Construction>>
. The Construction
-struct contains all information that is needed to construct a GameObject
, which is a seed and a function-object that is applied at construction by a factory. This enables dynamic loading and unloading of GameObject
s as done by GameTree
. It also means that you have to define that factory if you inherit GameObject
. This inheritance is also the reason why GameTree
has a template-function gameObject<GOType>
.
GameObject
can contain a RenderObject
and a PhysicsObject
, which we’ll later get to.
Anyway, here’s the code.
class GameObject;
typedef unsigned long seed_type;
//this declaration magic means that all GameObjectFactorys inherit from GameObjectFactory<GameObject>
template<typename GOType>
struct GameObjectFactory;
template<>
struct GameObjectFactory<GameObject>{
virtual unique_ptr<GameObject> construct(seed_type seed) const = 0;
};
template<typename GOType>
struct GameObjectFactory : GameObjectFactory<GameObject>{
GameObjectFactory() : GameObjectFactory<GameObject>(){}
unique_ptr<GameObject> construct(seed_type seed) const{
return unique_ptr<GOType>(new GOType(seed));
}
};
//same as with the factories. this is important for storing them in vectors
template<typename GOType>
struct Construction;
template<>
struct Construction<GameObject>{
virtual unique_ptr<GameObject> construct() const = 0;
};
template<typename GOType>
struct Construction : Construction<GameObject>{
Construction(seed_type seed, function<void(GOType*)> func = [](GOType* null){}) :
Construction<GameObject>(),
seed_(seed),
func_(func)
{}
unique_ptr<GameObject> construct() const{
unique_ptr<GameObject> gameObject{ GOType::factory.construct(seed_) };
func_(dynamic_cast<GOType*>(gameObject.get()));
return std::move(gameObject);
}
seed_type seed_;
function<void(GOType*)> func_;
};
typedef struct CResult
{
CResult() :
constructions{}
{}
CResult(CResult && o) :
constructions(std::move(o.constructions))
{}
CResult& operator= (CResult& other){
if (this != &other){
for (unique_ptr<Construction<GameObject>>& child : other.constructions){
constructions.push_back(std::move(child));
}
}
return *this;
}
template<typename GOType>
void push_back(seed_type seed, function<void(GOType*)> func = [](GOType* null){}){
constructions.push_back(make_unique<Construction<GOType>>(seed, func));
}
vector<unique_ptr<Construction<GameObject>>> constructions;
} CResult;
//finally, the GameObject
class GameObject
{
public:
GameObject(seed_type seed);
GameObject(const GameObject&);
virtual void tick(unsigned int delta);
inline Matrix4f trafoMatrix(){ return physicsObject_->transformationMatrix(); }
//getter
inline seed_type seed() const{ return seed_; }
inline CResult& properties(){ return properties_; }
inline const RenderObject& renderObject() const{ return *renderObject_; }
inline weak_ptr<PhysicsObject> physicsObject() { return physicsObject_; }
protected:
virtual CResult construct_(seed_type seed) = 0;
CResult properties_;
shared_ptr<RenderObject> renderObject_;
shared_ptr<PhysicsObject> physicsObject_;
seed_type seed_;
};
3. PhysicsObject
That’s a bit easier. It is responsible for position, velocity and acceleration. It will also handle collisions in the future. It contains three Transformation objects, two of which are optional. I’m not going to include the accessors on the PhysicsObject
class because I tried my first approach on it and it’s just pure madness (way over 30 functions). Also missing: the named constructors that construct PhysicsObject
s with different behaviour.
class Transformation{
Vector3f translation_;
Vector3f rotation_;
Vector3f scaling_;
public:
Transformation() :
translation_{ 0, 0, 0 },
rotation_{ 0, 0, 0 },
scaling_{ 1, 1, 1 }
{};
Transformation(Vector3f translation, Vector3f rotation, Vector3f scaling);
inline Vector3f translation(){ return translation_; }
inline void translation(float x, float y, float z){ translation(Vector3f(x, y, z)); }
inline void translation(Vector3f newTranslation){
translation_ = newTranslation;
}
inline void translate(float x, float y, float z){ translate(Vector3f(x, y, z)); }
inline void translate(Vector3f summand){
translation_ += summand;
}
inline Vector3f rotation(){ return rotation_; }
inline void rotation(float pitch, float yaw, float roll){ rotation(Vector3f(pitch, yaw, roll)); }
inline void rotation(Vector3f newRotation){
rotation_ = newRotation;
}
inline void rotate(float pitch, float yaw, float roll){ rotate(Vector3f(pitch, yaw, roll)); }
inline void rotate(Vector3f summand){
rotation_ += summand;
}
inline Vector3f scaling(){ return scaling_; }
inline void scaling(float x, float y, float z){ scaling(Vector3f(x, y, z)); }
inline void scaling(Vector3f newScaling){
scaling_ = newScaling;
}
inline void scale(float x, float y, float z){ scale(Vector3f(x, y, z)); }
void scale(Vector3f factor){
scaling_(0) *= factor(0);
scaling_(1) *= factor(1);
scaling_(2) *= factor(2);
}
Matrix4f matrix(){
return WMatrix::Translation(translation_) * WMatrix::Rotation(rotation_) * WMatrix::Scale(scaling_);
}
};
class PhysicsObject;
typedef void tickFunction(PhysicsObject& self, unsigned int delta);
class PhysicsObject{
PhysicsObject(const Transformation& trafo) :
transformation_(trafo),
transformationVelocity_(nullptr),
transformationAcceleration_(nullptr),
tick_(nullptr)
{}
PhysicsObject(PhysicsObject&& other) :
transformation_(other.transformation_),
transformationVelocity_(std::move(other.transformationVelocity_)),
transformationAcceleration_(std::move(other.transformationAcceleration_)),
tick_(other.tick_)
{}
Transformation transformation_;
unique_ptr<Transformation> transformationVelocity_;
unique_ptr<Transformation> transformationAcceleration_;
tickFunction* tick_;
public:
void tick(unsigned int delta){ tick_ ? tick_(*this, delta) : 0; }
inline Matrix4f transformationMatrix(){ return transformation_.matrix(); }
}
4. RenderObject
RenderObject
is a base class for different types of things that could be rendered, i.e. Meshes, Light Sources or Sprites. DISCLAIMER: I did not write this code, I’m working on this project with someone else.
class RenderObject
{
public:
RenderObject(float renderDistance);
virtual ~RenderObject();
float renderDistance() const { return renderDistance_; }
void setRenderDistance(float rD) { renderDistance_ = rD; }
protected:
float renderDistance_;
};
struct NullRenderObject : public RenderObject{
NullRenderObject() : RenderObject(0.f){};
};
class Light : public RenderObject{
public:
Light() : RenderObject(30.f){};
};
class Mesh : public RenderObject{
public:
Mesh(unsigned int seed) :
RenderObject(20.f)
{
meshID_ = 0;
textureID_ = 0;
if (seed == 1)
meshID_ = Model::getMeshID("EM-208_heavy");
else
meshID_ = Model::getMeshID("cube");
};
unsigned int getMeshID() const { return meshID_; }
unsigned int getTextureID() const { return textureID_; }
private:
unsigned int meshID_;
unsigned int textureID_;
};
I guess this shows my issue quite nicely: You see a few accessors in GameObject
which return weak_ptr
s to access members of members, but that is not really what I want.
Also please keep in mind that this is NOT, by any means, finished or production code! It is merely a prototype and there may be inconsistencies, unnecessary public
parts of classes and such.
18
The whole game industry switched to Component-based Design engines (for re-usable engines only) for the reason of composability with performance constraints (and allow also fully data-driven engines). You should look into it, it’s the best way we know to have very heterogeneous kind of entities, defined by a set of different components.
There is a lot of different ways to setup a component system. I should point to the book Game Engine Architecture which have a whole chapter on the different kind of setup you can have to solve the problem you have, including different kind of component-based systems. Also there are a lot of articles about the subject like:
- http://gameprogrammingpatterns.com/component.html
(there are also a lot of experimentation on github about how to implement a component system in modern C++. I did one recently with no inheritance at all, using concept-based type-erasure)
The issues you have are related to the use of inheritance where it makes the architecture less flexible than it could. Remove inheritance and organize things in batches of components and you will have maximum flexibility.
Note that, of course, that flexibility is not always what you want in the end, but it should solve the problem you are presenting here.
4
I wanted to try to complement Klaim’s fine answer with some alternative details.
But, how do I maintain encapsulation of Master with this design?
You don’t if I understood the context correctly. That’s a slightly dogmatic-sounding statement but unless someone figures out a brand new way of designing and implementing objects here in the face of the flexible and programmable and open architecture requirements of game engines, it is not practical to expect to model a concept like a World
, or Universe
, or most commonly used term, Scene
, and expect its public interface to service every possible thing you can ever want to do with the “universe” without exposing every little thing that resides inside of it.
The common approach here is to favor what might be called a “leaky abstraction” and think of your Scene
, or Universe
, as a container of sorts, with the primary responsibility of storing things and allowing the users of its interface to access whatever exists inside of it. Trying to make it hide away such details in favor of the strongest level of encapsulation and information hiding while maintaining invariants tends to grow into an impossibly monolithic responsibility that grows and grows and grows with ever-changing requirements.
Just write accessor-methods for all accessor-methods of all classes that Master is composed of. This leads to perfect encapsulation, because no implementation detail of Master is visible […]
I don’t consider that “perfect encapsulation”. That might be a bit subjective but what I consider the strongest level of encapsulation is a class whose state doesn’t even need to be exposed in the first place (through accessor functions or anything else). In my mind it’s a class that exposes the absolutely minimal information about itself, like a container that doesn’t even need to expose what’s stored inside for anything other than read-only access. That is the strongest way to maintain invariants over its state is to not expose it in any mutable form whatsoever. If we want to maintain Universe
-level invariants in our Universe
class/interface, then we cannot expose anything inside of it for tampering. Of course, as mentioned above, that tends to not be very practical when we’re modeling things at the level of a “Universe” with the types of shifting requirements imposed on game engines which often want to model a “Universe” of sorts (however simplified, gamedevs are creating their own miniature universe, and I’d dare say that is one of the most challenging things to do from a programming design standpoint as they are literally trying to be “gods” breathing life into some universe whether it’s Super Mario or Unreal 4).
Disparate Types of Things
And this is where I think you found a problem (with the Master
, or GameTree
, as I gathered you call it). Even if we make our Universe
, or Scene
, into a container, it doesn’t want to contain very homogeneous types of things.
We might have stars which emit light, we might have plants which depend on such light to grow, and wither and die in its absence, we might have people, animals which feed on each other and plants, vehicles, machine guns, artificial lights, cameras, buildings, etc etc etc.
So there’s not a very homogeneous concept or design or interface here to unify all these things. So games usually solve this problem by making the individual things inside of our Scene
into something akin to “containers” themselves, called an Entity
(which is like your GameObject
).
And we might have a sentry entity which has a camera component so that you can look through it from a security station, it might have a directional light component so that it shines infrared light in the direction it’s looking at, it might have a weapon component to fire when intruders get in its line of sight, and just for fun it might also be an organic plant with a plant component (it’s like a cybernetic plant thing) that requires sunlight to survive and its weapon component is designed to shoot spiked needles at intruders.
Houston: We Still Have a Problem
Uh oh, we just transferred the non-homogeneous collection problem to these game Entities
. How are we supposed to fetch things as disparate as plant components from camera components from an Entity
? And there often the interface of said Entity
will tend to involve some down casts in its implementation (but checked centrally at runtime for safety), like:
// Fetch a plant component from the entity, or nullptr if
// if the entity does not provide one.
PlanetComponent* plant = some_entity.get<PlantComponent>();
If (plant)
{
// Do something with the plant.
}
And how you implement that can vary wildly, but often you’ll find some central downcast somewhere in the implementation of the analogical get
method above (might be a dynamic_cast
; I have some answer somewhere on here where I suggested one possible implementation). The implementation usually combines a polymorphic base pointer container of sorts with a downcast (which is frowned upon, but at least we can centralize it to one place in the system).
Systems
If we get this far, then we might want to organize our codebase into “Systems”. You might have a NatureSystem
whose sole responsibility is to loop through all entities in our universe which provide plant components, and make sure they have available sunlight to grow. If they don’t, they might begin to wither and die.
Given how frequently such systems might want to do such things, we might add a public method to our Scene
to fetch all plant components in our universe:
for (PlantComponent& plant: scene.query<PlantComponent>())
{
// Make sure plant component has had sufficient available
// sunlight or else make it start withering and dying.
}
And this way we don’t even have to bother looping through our entities and checking if they provide a plant component. We just ask the scene what plant components are available to process in some loop.
Efficiency
If we start profiling code like the above, we might find a lot of hotspots in scene.query<SomeComponentType>()
which might have to loop through all entities in our “universe” and check to see if they provide some component of a given type, like a plant component.
So that might raise the question if components should really be implemented like containers. For that I tried to consult with the high elders of game programming and, if I remember the holy scripture correctly, their answer went something like this:
No.
Instead it might be a lot more efficient for these systems performing loops to store each component of a particular type contiguously in our Scene
(or universe), and have entities become associated/mapped to the components they conceptually “have”.
And with that, when a system wants all plant components in our “universe”, or Scene
, you no longer have to go through the entities at all. You can just directly access the collection of plant components in a scene and iterate through it (with a nice, contiguous representation that preserves locality of reference). We don’t store components inside of entities. We associate entities to their components.
Pure Interfaces
Just a brief mention, but you might wonder why we don’t use pure interfaces, like IPlant
. In that case we could see if an entity provides “plant capabilities” like so:
IPlant* plant = entity.get<IPlant>();
Or fetch everything that provides plant interfaces in a scene like so:
for (IPlant& plant: scene.query<IPlant>())
{
...
}
While this gives lots of flexibility to the user of such interfaces, the problem with that approach is that your entities will generally want to implement these pure interfaces directly or directly store components that do. The nature of such an architectural design might also tempt developers to redundantly implement such an interface with little benefit in terms of Liskov substitution (just mostly code duplication), or introduce some abstract base class to eliminate the redundancy which largely defeats the whole point and substitutability of pure interfaces.
Here such flexibility comes about from the separation of components and the logic (systems) which processes them rather than the substitutability of the implementation of a given component. We end up getting the sort of flexibility of duck typing as found in C++ templates, where if something has wings, it can flap them and begin flying. We don’t care about what it is beyond that except for the fact that it has wings, and the wings might even just be data instead of some abstract interface.
Conclusion
So with all that, we arrive at the entity-component system architecture, as popular among game engines today (and I’m the only oddball to my knowledge using it in VFX software now). And I don’t know if the “evolution story” above is true or not. I just made it up. But I like telling it and it makes sense to me as a former gamedev starting in Borland Turbo C tackling the exact same design problems all along of how to model a “universe”, but finding the ECS the nicest fit I’ve found so far to these same design problems I’ve faced all these years. I like telling stories like these. I’m not sure if my sense of humor translates in text so well. I’m much better at it in person. Drunk girls seem to dig it (well, I mean not programming stories in those cases; usually you don’t want to tell girls in night clubs that you’re a programmer until they’ve already started to like you).