11
\$\begingroup\$

I'm working on a framework that will be used to create simple 2D grid based games. It is using an Entity-Component-System pattern with corresponding manager classes used to control the lifespan of objects and construct them polymorphically based on an ID.

In addition to general improvements to my code, I'm asking for feedback on the implementation of the ECS model specifically. Things I would like to change are:

  • Remove Manager classes (if possible)
  • Decouple Systems and Components keys from base classes (related post).

I don't like the use of my manager classes, I created them out of necessity to ensure the proper constructors are called on my abstract base classes, and would like to redesign the framework without them if possible.

I would appreciate any advice on decoupling the Systems and Components id's from their base classes. (Possibly create a generic key class that can be used for both base classes?) Related post.

Using the ECS:

SystemsManager* sysMan = new ConcreteSystemsManager();
EntitiesManager* entMan = new ConcreteEntitiesManager();

sysMan->createSystem("ExampleSystem");
entMan->createEntity("ExampleEntity");

sysMan->getSystemPtr("ExampleSystem")->registerEntity(
    entMan->getEntityPtr("ExampleEntity");

GameState* gameState = new ConcreteGameState(entMan, sysMan);
gameState->run();

I've only included the declarations of the classes used in my framework, I didn't feel it was necessary since my request for feedback is about my overall design, however I'll post the implementation files upon request.

State.hpp

#pragma once
#include "EntitiesManager.hpp"
#include "SystemsManager.hpp"

namespace drj
{
namespace gfw
{
namespace core
{
    // Forward Declarations
    class GameSystem;

    // Base class for concrete GameState classes to
    // inherit from.
    class State
    {
    public:
        State();
        State(EntitiesManager*, SystemsManager*);

        ~State();

        void setEntitiesManager(EntitiesManager*);
        void setSystemsManager(SystemsManager*);

        virtual int run() = 0;

    protected:
    private:
        std::shared_ptr<EntitiesManager> entitiesManager;
        std::shared_ptr<SystemsManager> systemsManager;
    };
};
};
};

IManager.hpp

#pragma once

namespace drj
{
namespace gfw
{
namespace core
{
    class IManager
    {
    public:
        virtual ~IManager();

        virtual int create(std::string const&) = 0;
        virtual int destroy(std::string const&) = 0;

    protected:
        IManager();

    private:
    };
};
};
};

EntitiesManager.hpp

#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include "Entity.hpp"
#include "IManager.hpp"

namespace drj
{
namespace gfw
{
namespace core
{
    class EntitiesManager : public IManager
    {
    public:
        EntitiesManager();
        virtual ~EntitiesManager();

        // Returns nullptr if the entity doesn't exist.
        // This EntitiesManager still owns the pointer.
        Entity* getEntityPtr(std::string const&) const;

        // Returns -1 if the entity already exists.
        int create(std::string const&);
        // Returns -1 if the entity didn't exist.
        int destroy(std::string const&);

    protected:      
    private:
        std::unordered_map<
            std::string,
            std::unique_ptr<Entity>> entities;

    };
};
};
};

SystemsManager.hpp

#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include "System.hpp"
#include "IManager.hpp"

namespace drj
{
namespace gfw
{
namespace core
{
    class SystemsManager : public IManager
    {
    public:
        virtual ~SystemsManager();

        // Returns nullptr if the system doesn't exist.
        System* getSystemPtr(std::string const&) const;

        // Returns -1 if the system already exists.
        virtual int create(std::string const&) = 0;
        // Returns -1 if the system didn't exist.
        virtual int destroy(std::string const&) = 0;

    protected:
        SystemsManager();

    private:
        std::unordered_map<
            std::string,
            std::unique_ptr<System>> systems;
    };
};
};
};

ComponentsManager.hpp

#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include "Component.hpp"
#include "IManager.hpp"

namespace drj
{
namespace gfw
{
namespace core
{
    class ComponentsManager : public IManager
    {
    public:
        virtual ~ComponentsManager();

        // Returns nullptr if the component doesn't exist.
        Component* getComponentPtr(std::string const&) const;

        // Returns -1 if the component already exists.
        virtual int create(std::string const&) = 0;
        // Returns -1 if the component didn't exist.
        virtual int destroy(std::string const&) = 0;

    protected:
        ComponentsManager();

        std::unordered_map<
            std::string,
            std::unique_ptr<Component>> components;

    private:

    };
};
};
};

Entity.hpp

#pragma once
#include <list>
#include <memory>
#include <string>
#include "ComponentsManager.hpp"
//#include "GameComponent.hpp"
#include "System.hpp"

namespace drj
{
namespace gfw
{
namespace core
{
    // GameObjects are used as Entities in the ECS model
    // of the core GameEngine module.
    class Entity
    {
        friend class System;

    public:
        Entity();
        ~Entity();

        // Returns true if the provided key is in this
        // objects systemsRegistry.
        bool hasSystemKey(std::string const&) const;

        // Adding a key is not the same as registering
        // an entity, however it is a process in that.
        void addSystemKey(std::string const&);
        void removeSystemKey(std::string const&);

        ComponentsManager* getComponentsManager() const;
        void setComponentsManager(ComponentsManager*);

    protected:
    private:
        std::list<std::string> systemsRegistry;
        std::unique_ptr<ComponentsManager> componentsManager;
    };
};
};
};

Component.hpp

#pragma once
#include <string>
#include <list>

namespace drj
{
namespace gfw
{
namespace core
{
    // Base class for specialized GameComponent
    // classes to inherit from.
    class Component
    {
        friend class System;

    public:
        std::string const& getKey() const;

        virtual ~Component();

    protected:
        Component(std::string const&);

    private:
        Component();

        std::list<std::string> systemsUsing;

        const std::string key;

        size_t getSystemsUsingSize() const;
        bool hasSystemKey(std::string const&) const;

        // Used by GameSystem when registering
        // entities and components.
        void addSystemKey(std::string const&);
        void removeSystemKey(std::string const&);
    };
};
};
};

System.hpp

#pragma once
#include <string>
#include "Component.hpp"

namespace drj
{
namespace gfw
{
namespace core
{
    // Forward Declarations
    class Entity;

    // Base class used to create specialized Systems
    // that use registered GameObjects as their
    // input and output data.
    class System
    {
    public:
        virtual ~System();

        std::string const& getKey() const;

        // Returns true if the provided entity
        // has this systems key in its systemsRegistry.
        //
        // The same as calling GameObject::hasKey(GameSystem::key)
        bool isRegistered(Entity const&) const;

        // Adds this systems key to the provided
        // entities systemsRegistry and adds the
        // necessary components to its ComponentsManager.
        void registerEntity(Entity&) const;
        void unregisterEntity(Entity&) const;

        virtual int run(Entity&) = 0;

    protected:
        // Prevents users from creating non-specialized
        // GameSystem objects.
        //
        // @arg[0] The key used to identify this
        // system apart from other systems.
        //
        // @arg[1] The list of component keys that
        // are required for concrete systems.
        System(std::string const&,
            std::list<std::string> const&);

    private:
        System();

        const std::list<std::string> componentsRegistry;
        const std::string registryKey;
    };
};
};
};
\$\endgroup\$
  • 1
    \$\begingroup\$ This is surprisingly very pointer heavy for no apparent reason. \$\endgroup\$ – Rapptz Jul 30 '14 at 4:08
5
\$\begingroup\$

There is nothing to redesign. This is (rudely speaking) a lot of scaffolding, with no concrete usage to validate whether this is a good design.

I suggest that you first try to implement a real System (like the video System for instance), which will unequivocally tell you how exactly it expects to use Entities and Components. Then there will be something to review and make progress on.

\$\endgroup\$
  • \$\begingroup\$ This is in short the best answer to me. I added some more in depth thoughts but I think this can be accepted as well :) \$\endgroup\$ – GameDeveloper May 5 '18 at 8:40
3
\$\begingroup\$
  • The flat namespaces seem a little weird to me, but the answers to this question and this question offer mixed suggestions on the preferred usage. They also seem to suggest that you may not need more than two namespaces, but I'm not familiar enough with this design to know for sure.

  • Some of your comments are unnecessary, such as this one:

    // Forward Declarations
    

    It's already pretty clear that this is a forward declaration, and you don't need to tell us anyway. Comments should best be used to document something unobvious for others.

  • In some places you do this:

    protected:      
    private:
        // code here...
    

    and in other places you do this:

    private:
        // no code here...
    

    If you don't currently have anything after such a keyword, simply leave it out. Keeping them there anyway doesn't really help with maintenance, and can still leave others confused about why it's left there anyway. It's especially unneeded for private, as classes are private by default. It is okay (usually preferred) to keep the keyword there anyway, but it should still have some code.

Side-note: as @Laurent has mentioned, there's not much to review here as all you've given us are headers. On the other hand, there's already much code here, so any additional code for review should be posted as a separate question.

\$\endgroup\$
3
\$\begingroup\$

Please, resist the temptation to write your own ECS system, I've been in you and it is a no-go. But if you have to do so, do it right!

  • You need to understand why you need ECS pattern (you entered dependency hell at least once)
  • You need to start from good examples of ECS in your language (and I fear, for C++ there are actually no good examples, nor usefull articles)

Let's me make a premise, I'm actually a consultant, helping a small indie team of quitting dependency hell, I'm basically re-writing a small game (30k lines of code). Turning it into a full-fledged ECS game. And I'm using an already existing ECS framework, not a mine framework. Explaining it is the most difficult part.

I'm actually learning new things with every-day problems I face, so I'm not speaking to you as "expert", but as person that is learning. Sometimes I ask questions to the author of the framework I use and he is very willingly to help me and find better solutions to certain problems.

Believe me, I come from a C++ programming background, so I always struggled to make my custom engine, dealing with allocation etc. I even started a 3D engine once, I would never do that again.

What you are trying to achieve already however:

Remove Manager classes (if possible)

Decouple Systems and Components keys from base classes

suggests me that you already spent a good amount of time digging into the topic. While your first point indicates you are on the right way, your second point gives me the impression that you have been hijacked by articles written by people that do not really use the ECS pattern or do not understand the pattern at all (Unity, Unreal and Lumberyard are NOT Using the ECS pattern, neither in its old conception).

To directly address your question, I think the best suggestion comes from @Laurent La RIZZA's answer:

I suggest that you first try to implement a real System (like the video System for instance), which will unequivocally tell you how exactly it expects to use Entities and Components. Then there will be something to review and make progress on

but let me explain why his suggestions arepure gold. ECS pattern is about decoupling all the game logic (yet seems most articles put focus on components, the point is deocupling the logic).

  • If your design is right then your logic is decoupled.
  • That means you can design "random" pieces of logic.
  • Design functionalities of your game first (Player shooting to enemies, player jumping, opening a door)
  • Each functionality becomes at least 1 System.

Once you have pieces of logic, working on Components, then you finally just need one further step to link that logic togheter, and what you need is actually a ECS framework.

  • Therefore the correct way to do ECS, is by designing Systems first,

  • In mine articles (I tried to condense these in one answer but if you want to understand details I suggest reading the original articles too) I do exactly that, I start with a overview of the ECS framework I use, but I do not spent too much time on defining Entities and components, rather I start immediatly implementing pieces of game logic (Systems/Engines). Those works on EntityViews.

The ECS paradigm emerges as a tool as long as you think your logic in a modular and decoupled way.

I think you should seriously take a tour into Svelto.ECS (beware, its author renamed "Systems" into "Engines", I believe to avoid confusion with c#'s namespace "System" from .NET), written in C#.

To do that you have to drive away from most of ECS articles you find on the web, I'm sad to say that, because I tried to use ECS "the old way" and it simply didn't worked well, it forced use of anti-patterns and made me wasting time.

Most ECS frameworks, don't scale. You start easy and when the project becomes big you enter dependency hell, but you don't realize that because the dependency is hidden by components.

With Svelto you just need to start, it seems hard (well it is a change of paradigm afterall), but after you get the first 10 engines done you realize how easy and flexible it is.

I would appreciate any advice on decoupling the Systems and Components id's from their base classes.

Simply use EntityViews like in Svelto. This concept was introduced with that framework. And I really love it.

Most ECS frameworks have this dull concept that Components are coupled with engines and every engine should loop all components of a certain type. They should not! Both for flexibility and performance issues! (there are even technical articles from AAA industries that use tricks like skipping updating Systems every X frames because they were basically looping too much).

Everything in Svelto is decoupled.

Most ECS frameworks have this concept:

  • If a entity has X,Y component, then it has to be processed by XY System, X system and Y System

Svelto actually do that:

  • Entity has X,Y component but to be processed by K Engine it actually need a KEntityView which maps X,Y components

This is deeply different. Basically you can select which engines process which entities without having to resort to specialized components or without resorting to usage of Tags or groups.

Basically when you spawn an entity, you have to select in advance which engines will see that entity. You can change that at any time by updating EntityViews in its descriptor, if there are missing components a nice error message will tell you that (I think you can implement that at compile time with C++, I done something similiar in past).

Basically you may have X entities in your game that have a Position component, but you don't want to process your position the same, so in example static objects should not even have Systems updating them, so they could just have a method that returns the position without allowing to change it (that method does not implement any logic it just returns data).

class IPositionGet
{
    public:
    virtual const Vector3 & GetPosition() const = 0;
};

class IPositionSet
{
    public:
    virtual void SetPosition( const Vector3 & pos) = 0;
};

This allows you to implement entities in C++ directly (prefer always this, even though direct memory addressing of C++ allows you to do dirty things)

struct Bush: public IPositionGet
{
    Vector3 BushPosition;

    public:

    Bush( const Vector3 & InitialBushPostion) { /*blah*/ }

    virtual const Vector3& GetPosition() const override
    {
         return BushPosition;
    }
};

When you instantiate a bush you also specify which entity views it will implement, so basically this allows it to be processed by right engines. In example if you want bushes to be avoidable by avoidance algorithm

//no return value. Correct
factory->CreateEntity< ObstacleAvoidanceView>( entityId, new Bush( pos));

the ObstacleAvoidanceView could be something similiar to (assuming the avoidance is done using a circle or a sphere):

class ObstacleAvoidanceView: Descriptor< IPositionGet, IRadiusGet>
{
  //...
}

This gives an overview simplified of the design process. You continuosly refine things, because you can't predict everything. I Assumed objects have a position ok, but then after I defined the first piece of logic It was obvious that the bush was missing the radius for collision avoidance. It was not so hard to add it later. This kind of continuos changes are a real pain without an ECS system. And I added a Radius, without warrying if that could interefer with other engines, just because the engines are selected by the entity view, so there is not risk that adding the Radius to bushes automatically makes the bush processed by Radius-realted systems. Bushes are only processed by Systems interested in their EntityViews.

If later I want bushes to be only slowing down player, I could altogheter remove the EntityView from the descriptor, and automatically I change bushes behaviour, without having to change engines, or without having to change the Bush.

Honestly I think the final syntax in C++ will be quite different from C#, but I believe it is actually possible implement the same of Svelto in C++.

\$\endgroup\$

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.