5

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 GameObjects 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 PhysicsObjects 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_ptrs 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.

migrated from stackoverflow.com Jun 2 '14 at 20:04

This question came from our site for professional and enthusiast programmers.

  • 1
    Can you post your initial design in code so we can experiment with suggestion before we post the answer? – NirMH Jun 2 '14 at 10:39
  • 2
    1.) The interfqace of your class master would be monstrous anyway which is bad design. 2.) If your master is " not be copied, moved or assigned" then whats the point of shared_ptr? – knivil Jun 2 '14 at 11:07
  • 1
    @Massa I'm not sure if this is really a good use for multiple inheritance. As your linked thread points out, you have to be careful not to violate Liskov's SP, which sounds to me could easily happen in this case. Besides that, inheritance is one of the most effective ways to break encapsulation in C++ (second only to friendship), so its use kind of contradicts the 'maintain encapsulation' requirement from the question title. Anyway, it is hard to give reasonable advice without knowing more details about how the different classes are supposed to interact. – ComicSansMS Jun 2 '14 at 11:51
  • 1
    @ComicSansMS if you can break the encapsulation via inheritance, then it was already broken... nice, encapsulated classes do protected as little as possible (never, if at all possible). In iFreiLicht case, specifically, the clients of his Master class want to treat it effectively as a A, Base, C, and D instance, and they want not to be changed (even if they must be recompiled) when some method's responsibility changes from C to D, for instance. – Massa Jun 2 '14 at 12:54
  • 1
    @Massa True, in that case you would simply use inheritance to avoid having to re-declare the member functions on Master (which has its drawbacks, but is valid). – ComicSansMS Jun 2 '14 at 13:00
5

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:

(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.

  • Could you elaborate a bit more on the "inheritance makes the architecture less flexible"-part? After reading the articles, I found they described something similar to what I'm doing, I separate functionality into GameObject, RenderObject and PhysicsObject. So my GameTree, which is nothing but a container of these Components, actually is a Component-based Design. My problem is now how to actually make the functionality these components provide available to the outside world. Additionally, how did that help you eliminate inheritance in any way? – iFreilicht Jun 3 '14 at 13:16
  • After looking into it a bit more, type erasure seems like a really cool idea and it could indeed help to improve encapsulation. I'm not marking this answer as the answer yet, but you already pointed me in the right direction, which was really helpful. – iFreilicht Jun 3 '14 at 16:10
  • 2
    About inheritance, it's a long subject but 1) having arrays of objects to udpate is faster than having to go through a tree of objects spread more or less randomly around in the memory, like you are currently doing; 2) inheritance is one of the strongest types of relationships between two types: the more you try to compose objects with different unrelated features (like in a lot of games) the more you will have inter-dependant code and changing one feature might have an impact on another feature. What you want is actual composition of feature which is the point of component design. – Klaim Jun 3 '14 at 22:13
  • I think it is best explained in details in Game Engine Architecture book, so again I recommend that you read the chapter about game object management, you might understand better. I will try to complete my answer when I have time though. – Klaim Jun 3 '14 at 22:14
1

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".

enter image description here

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).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.