Modern game programming with the Entity Component System and the engine Bevy
The Bevy game engine uses the Rust programming language and the Entity Component System, which is becoming increasingly common in game development.
In many professional game projects, the Entity Component System (ECS) design pattern replaces the classic game loop because it gets more performance out of current hardware. The well-known game engine Unity also propagates ECS in its DOTS initiative. The new game engine Bevy in the programming language Rust is an ideal implementation of ECS. No prior knowledge of Rust is required to understand the essential concepts.
From the game loop to the ECS design pattern
The game loop in pseudo code for a space game like Asteroid, in which a spaceship laser destroys the approaching asteroids, could look like this:
While true:
time_diff := <Zeit, seit letztem Schleifendurchlauf>
ship.read_input()
ship.update(time_diff)
for every asteroid:
asteroid.update(time_diff)
for every laser:
laser.update(time_diff)
collisions()
ship.draw()
for every asteroid: asteroid.draw()
for every laser: laser.draw()
the GameObject
s represent the objects (ship
, asteroid
, laser
) that the game is about. The central loop of the game engine calls the method as often as possible Update
the GameObject
s up. The object reacts to its surroundings, calculates its new position or processes inputs. In some frameworks the GameObject
s an additional Draw method that takes care of drawing on each iteration of the loop. The more often this happens, the more frames appear on the screen per second, making the game run smoother.
As the complexity of the game loop and the Update
-Methods of objects do more work. As a result, the program becomes more confusing over time.
approach with inheritance
The classic approach in object-oriented programming for cleaner code is to build an inheritance hierarchy, where an object like ship
is derived from generally valid classes. Many GameObject
s have similar attributes and methods.
Anything that moves can be ruled out by the class Moveable
derive, which takes care of the calculation of the movement. Everything that is drawn inherits from the class Renderable
.
The spaceship that moves on the one hand and that is drawn on the other hand would have to be derived from both classes. This is not possible in programming languages without multiple inheritance. However, one way out would be interfaces in programming languages such as Java, which offer the concept.
The general way out is a hierarchy with the class at the top Renderable
stands, from the Moveable
is derived, that in turn Ship
as a child class. If a spaceship with camouflage is to be added later, it will be difficult: It moves (Moveable), but is not visible (Renderable). The inheritance hierarchy therefore no longer fits.
Continue with components
Many newcomers to object-oriented programming have stumbled across too much inheritance and have learned to prefer assembling classes from individual components.
The class Ship
would have the components Moveable
and Renderable
:
Class Ship:
move = new Moveable()
render = new Renderable()
...
For a starship with camouflage, the moveable component is sufficient.
The game engine Unity is dedicated to this component-based architecture. With many objects, however, it is difficult to optimize performance, for example by parallelizing processes or better memory access. One reason for this is the distribution of data and functionality across different classes that may call each other.
The data-oriented programming (DOP) approach attempts to strictly separate data and program from one another in order to create more options for automatic optimization. One way is the ECS design pattern.
The Entity Component System
A component in the ECS design pattern contains only data. In games, it is the attributes that a game object can have. That could look like this:
- Position(x,y) – PositionComponent
- Speed (x,y) – SpeedComponent
- Wireframe(mesh) – MeshComponent
A component does not contain any processing logic.
An entity is a game object like the spaceship. It consists of any number of components and contains no data or program logic. A unique ID linked to the components via an internal data structure is sufficient for the implementation of an entity.
The third building block of the ECS is the system. It contains the complete processing logic, but no own data. A game consists of any number of systems that can run in parallel in different threads.
In the ECS pattern, a mechanism that runs the required systems in parallel as often as possible replaces the classic game loop. To put it more generally: A system reads components and transforms their current state into another.
For example, the Movement system fetches all velocity components (SpeedComponent
s) and positional components (PositionComponent
s) to calculate the new position.
The advantages of the Entity Component System (ECS) pattern over the game loop are:
- the data-centric approach, where data drives operations,
- the clean architecture with loose coupling instead of nested inheritance as well
- high performance thanks to parallel processing and caching.