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