5
\$\begingroup\$

I'm creating my first c++ game engine project (for learning purposes) and in it I've attempted to implement an entity/component system utilizing some data oriented design principles while also not fully giving up my object oriented way of thinking. I have data only component structs:

(Example)

Position.h

struct Position
{
    Vector3D position{ 0.0f, 0.0f, 0.0f };
};

which are stored in arrays within my SceneManager class:

SceneManager.h

class SceneManager
{
    //Some other code....

private:
    //So only systems have access to component arrays
    friend class BGraphics::RenderSystem;
    friend class BInput::InputSystem;
    friend class BPhysics::MovementSystem;
    friend class BPhysics::CollisionSystem;
    friend class BAudio::AudioSystem;

    //Component arrays. Each index of the arrays represents an entity.
    //So positionComponents.at(1) represents entity 1's position. 
    Array<Position, 10> positionComponents;
    Array<Velocity, 10> velocityComponents;
};

As you can see, my engine's system classes are the only ones who have access to the component arrays and so every system can process components. There are some other things involved in this setup that I don't want to get too deep into for simplicity's sake. My issues are as follows:

Each system accepts a SceneManager reference in an Update function like so:

RenderSystem::Update(SceneManager& scene)
{ 
   //Use scene component arrays necessary for processing...
}

What I'm struggling with is right now my components seem like global variables in that every system has access to them and can change their state. Though it seems like a lot of data oriented design involves structs like these with public data for which other functions can operate on, changing their state. With that being said, is there something I'm missing with my implementation that would make my component data more encapsulated and safer? How am I suppose to think about handling data in a safe way when utilizing data oriented design? Any clarity is appreciated.

\$\endgroup\$
  • \$\begingroup\$ The idea of components being arrays and entities being indexes into arrays isn't "a more data oriented approach to ECS", it's the entire point of ECS and always has been. \$\endgroup\$ – Miles Rout Mar 14 '17 at 21:11
9
\$\begingroup\$

What's an object?

What we call an object or an entity depending on the model is, fundamentally, made of two parts: data, and behaviour. Game objects have properties and do things.

Let's take a simple, free-falling ball as an example. A ball's data is:

  • Its current position
  • Its current speed

Our ball can do a single thing: fall. When a ball falls, it does the following (let's assume simple Euler integration here):

  • Integrate the global gravity field vector into its speed (accelerate)
  • Integrate the resulting speed into its position (actually move)

Now let's model this ball, first with OOP, then with an ECS.

OOP

In object-oriented programming, instances contain their data as members:

struct Ball {
    vec2 _position, _speed;
};

Then comes the principle of encapsulation. Encapsulation is about simplifying the interface of an object: the single behaviour of our ball will be modeled by a member function, which contains the code described above. Users of the class will then be able to make the ball fall (enact its behaviour) without meddling with its data. The fall function knows what it's doing, and all is fine.

struct Ball {

    void fall() {
        _speed += global.gravity * global.deltaTime;
        _position += _speed * global.deltaTime;
    }

    vec2 position() const { return _position; }

private:
    vec2 _position, _speed;
};

So this is our (contrived) API: users can query a ball's position, and make it fall. Note:

  • There is no getter for _speed. Not all of an object's data (what it needs to be able to function) is part of its API (what its users need to know about it).
  • There are no setters. Teleporting or receiving impulses are not behaviours of our ball according to our spec, only falling is.

Yes, that makes it quite a useless ball, especially since I omitted the constructors. But it's fine for an example :)

ECS

Let's move on to the Entity Component System model of our ball. This will be fuzzier, since unlike OOP, C++ does not provide native syntax for ECS concepts. But we'll make do with pseudocode.

Our ball, again, is made of two components, position and speed:

entity Ball {
    comp::Position;
    comp::Speed;
};

As you know, the components are only modifiable through systems. So let's add a system.

system Fall {
    updateEntity(comp::Position &pos, comp::Speed &spd) {
        spd += global.gravity * global.deltaTime;
        pos += spd * global.deltaTime;
    }
};

Voilà! once you have transcoded that into your ECS's syntax, you can create a ball and make it fall. This time the speed of the ball is readable from the outside -- I've not seen any ECS framework include restrictions on that so far. But only a system::Fall can modify the ball's state.

Did you notice? This is encapsulation. The system looks very much like a member function: it knows what to do, and it does it with a simplified API. The user of the system who calls it upon the entity still does not modify the data himself.

The difference between the function and the system is:

  • The Ball::fall function says "I know how to make a ball fall";
  • The system::Fall system says "I know how to make anything with a position and a speed fall".

ECS modeling essentially enables creating duck-typed objects dynamically. An object's data is inside its components, and its behaviour is inside the systems that you call upon it. You can add and remove components and systems as if you could add and remove member variables and member functions, at runtime.

Wrap-up

So the answer to your question is no: the ability for systems to access the components is not a breach of encapsulation.
While instances of different C++ classes are accessing each other's states, none of them are separate "objects": the actual, conceptual game object (a.k.a entity) we're caring about is made of the union of its data (components) and behaviour (systems), which interact much like member data and member functions in a traditional OOP class.

\$\endgroup\$
  • \$\begingroup\$ Perfect. This gets me to think about encapsulation in a whole different way. Let me ask you though, in your example you're passing the data needed by a specific system through parameters of the function which helps to limit the number of components each system can use. In my implementation currently, all systems have access to all components which means there could be systems that change the state of many different components. While I would try and avoid this, nothing yet is really inhibiting me from doing so. Do you think this is enough of a concern to merit changing my ECS implementation? \$\endgroup\$ – Jason Mar 10 '17 at 23:47
  • 1
    \$\begingroup\$ @Jason there's no real way to put a limit on that -- how would it look like? No more than X components for a system? This is the same self-discipline as always: don't put a ton of unrelated data into a single object, don't make huge functions that do too much. Also, if you want to actually have that explicit function signature inside your systems, C++ can do this, too! I have made something similar here. That one does more than what you need, but the part that detects the function's parameters and provides suitable arguments should be useful. \$\endgroup\$ – Quentin Mar 11 '17 at 11:45

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.