Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

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.

share|improve this question

migrated from stackoverflow.com Jun 2 at 20:04

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

    
Good reading : stackoverflow.com/questions/49002/… –  Chnossos Jun 2 at 8:40
1  
Can you post your initial design in code so we can experiment with suggestion before we post the answer? –  NirMH Jun 2 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 at 11:07
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 at 13:00
1  
Isn't this basically the Facade pattern? –  o_weisman Jun 2 at 13:31

1 Answer 1

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.

share|improve this answer
    
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 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 at 16:10
    
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 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 at 22:14

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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