📜 ⬆️ ⬇️

What is the Entity System Framework and why is it needed in game devs?

On Habré there are already a lot of interesting articles about creating a game from scratch, but there are few articles describing what to do, so that your favorite hack will not turn into Frankenstein and reach sane state. The author of the original article describes how the game engine code evolved from a pile of trash in the main loop to a well-designed, extensible architecture using the Entity Component System. The article contains a lot of code, which almost all often repeats - but I didn’t throw it away, because with it it will be clearer what changes the author makes and how it affects the whole structure. The article is intended for newbies, like me, who have already set up some “ingenious” prototypes, but now they don’t know how to figure out their code.

translator's note:
I am not a translator. I am an ordinary tongue-tied programmer who was looking for ways to refactor his game and came across this pattern, was surprised to find that there was almost no mention of it in Habré. I never translated large texts and it was a revelation for me to learn that translating easy and understandable English text into normal Russian is such a difficulty. In short, I apologize if the article seems clumsy and crookedly written. I honestly tried :)


Last week I released the Ash - Entity System framework for developing games on Actionscript and many people asked me the question "What is the Entity System Framework?". Here is my long answer.

Entity systems are becoming popular with well-known things like Unity and less well-known ActionScript frameworks like Ember2, Xember, and my own Ash. There are good reasons for this: a simplified game architecture that encourages sharing of responsibility in the code is easy to use.
')
In this post, I will consistently show how Entity-based architecture comes from an old-fashioned game cycle. It will take some time. The examples given will be on Actionscript, since this is exactly what I am currently using, but the architecture itself is suitable for any programming language.

Examples


In this article I will use the simple game Asteroids as an example. I use Asteroids as an example, because it includes many of the things needed in big games - the render system, physics, AI, player control of the object and non-controlled objects.

Game cycle


To really understand why we use entity systems, you must clearly understand how the good old game cycle works. For Asteroids, it might look something like this:
function update( time:Number ):void { game.update( time ); spaceship.updateInputs( time ); for each( var flyingSaucer:FlyingSaucer in flyingSaucers ) { flyingSaucer.updateAI( time ); } spaceship.update( time ); for each( var flyingSaucer:FlyingSaucer in flyingSaucers ) { flyingSaucer.update( time ); } for each( var asteroid:Asteroid in asteroids ) { asteroid.update( time ); } for each( var bullet:Bullet in bullets ) { bullet.update( time ); } collisionManager.update( time ); spaceship.render(); for each( var flyingSaucer:FlyingSaucer in flyingSaucers ) { flyingSaucer.render(); } for each( var asteroid:Asteroid in asteroids ) { asteroid.render(); } for each( var bullet:Bullet in bullets ) { bullet.render(); } } 

This game cycle is called at regular intervals, usually 60 or 30 times per second, to update the game. The order of operations in the cycle is important because we update various game objects, check for collisions between them, and then draw them all. Every frame.
This is a very simple game loop, because:
1. The game is simple in itself.
2. The game has only one state.
In the past, I worked on console games, where the game loop, the only function, consisted of more than 3000 lines of code. It was not beautiful and it was stupid. But it was the way that games were created and we had to live with it.
The architecture of the entity systems came from trying to solve the problems of the game cycle. It makes the game cycle the core of the game and assumes that the simplification of the game cycle is more important than everything else in the architecture of the modern game. This is more important than separating the view from the controller, for example.

Processes


The first step in this evolution is objects called processes. These are objects that can be initialized, updated, and destroyed. The process interface looks like this:
 interface IProcess { function start():Boolean; function update( time:Number ):void; function end():void; } 

We can simplify the game cycle if we divide it into several processes that will be responsible, for example, for rendering, moving objects or handling collisions. To manage these processes, we will create a process manager.
 class ProcessManager { private var processes:PrioritisedList; public function addProcess( process:IProcess, priority:int ):Boolean { if( process.start() ) { processes.add( process, priority ); return true; } return false; } public function update( time:Number ):void { for each( var process:IProcess in processes ) { process.update( time ); } } public function removeProcess( process:IProcess ):void { process.end(); processes.remove( process ); } } 

This is a kind of simplified version of the process manager. In particular, we must make sure that we update the processes in the correct order (which is determined by the priority parameter in the add method) and we must handle the situation when the process is deleted during the game cycle. But this simplified version conveys the idea itself. If our game cycle is divided into several processes, then the update method of our process manager is our new game cycle and the processes are already becoming the core of the game.

Render process


Let's look, for example, at the render process. We can simply pull the render code out of the game loop and place it in the process, getting something like this:
 class RenderProcess implements IProcess { public function start() : Boolean { //    return true; } public function update( time:Number ):void { spaceship.render(); for each( var flyingSaucer:FlyingSaucer in flyingSaucers ) { flyingSaucer.render(); } for each( var asteroid:Asteroid in asteroids ) { asteroid.render(); } for each( var bullet:Bullet in bullets ) { bullet.render(); } } public function end() : void { //    } } 


We use interfaces


But it is not very effective. We still have to manually draw all possible kinds of game objects. If we had a common interface for all visible objects, we could simplify a lot
 interface IRenderable { function render(); } class RenderProcess implements IProcess { private var targets:Vector.<IRenderable>; public function start() : Boolean { //    return true; } public function update( time:Number ):void { for each( var target:IRenderable in targets ) { target.render(); } } public function end() : void { //    } } 

Then the class of our spacecraft will contain a similar code:
 class Spaceship implements IRenderable { public var view:DisplayObject; public var position:Point; public var rotation:Number; public function render():void { view.x = position.x; view.y = position.y; view.rotation = rotation; } } 

This code is based on Flash display lists. If we used buffers or stage3d, it would be different, but the principles would be the same. We need a picture to draw, position and rotation to draw it. And the render function that performs output to the screen.

We use base classes and inheritance


In fact, there is nothing unique in the ship code. All its code could be used by all visible objects. The only thing that distinguishes them is this display object, tied by the view property, as well as the position and angle of rotation. Let's wrap this into a base class and use inheritance.
 class Renderable implements IRenderable { public var view:DisplayObject; public var position:Point; public var rotation:Number; public function render():void { view.x = position.x; view.y = position.y; view.rotation = rotation; } } class Spaceship extends Renderable { } 

Of course, all the objects being drawn will expand the base class and we will get a hierarchy like this:
image

Movement process


To understand the next step, first we need to look at another process and the class with which it works. Let us try to present a motion process that updates information about the position of objects
 interface IMoveable { function move( time:Number ); } class MoveProcess implements IProcess { private var targets:Vector.<IMoveable>; public function start():Boolean { return true; } public function update( time:Number ):void { for each( var target:IMoveable in targets ) { target.move( time ); } } public function end():void { } } class Moveable implements IMoveable { public var position:Point; public var rotation:Number; public var velocity:Point; public var angularVelocity:Number; public function move( time:Number ):void { position.x += velocity.x * time; position.y += velocity.y * time; rotation += angularVelocity * time; } } class Spaceship extends Moveable { } 


Multiple inheritance


It all looks good, but, unfortunately, we want our ship to move and draw, but many programming languages ​​do not allow multiple inheritance. And even in those languages ​​that support it, we will encounter a problem when the position and rotation in the Movable class should be the same as in the Renderable class.
The solution may be to create a chain of inheritance when the Movable will extend the Renderable.
 class Moveable extends Renderable implements IMoveable { public var velocity:Point; public var angularVelocity:Number; public function move( time:Number ):void { position.x += velocity.x * time; position.y += velocity.y * time; rotation += angularVelocity * time; } } class Spaceship extends Moveable { } 

Now our spaceship is able to move and draw. We can apply the same principles to other game objects and get this class hierarchy.
image

We can even get static objects that simply extend the Renderable.
image

Moveable but not renderable


But what if we want to create moving objects that should not be drawn? For example, invisible game objects? Here our class hierarchy breaks down and we need alternative implementations of the Movable interface, which is not inherited from Renderable.
 class InvisibleMoveable implements IMoveable { public var position:Point; public var rotation:Number; public var velocity:Point; public var angularVelocity:Number; public function move( time:Number ):void { position.x += velocity.x * time; position.y += velocity.y * time; rotation += angularVelocity * time; } } 

image
In a simple game, this is messy, but controllable, and in complex games, using inheritance to bind processes to objects quickly makes the code unmanaged, and soon you will find things in the game that don’t invest in a simple inheritance tree, like the ones above.

Prefer composition to inheritance.


There is an old OOP principle: prefer composition over inheritance . The application of this principle can save from potential confusion in inheritance.
We still need the Renderable and Movable classes, but instead of inheriting them to create a spacecraft class, we will create a ship class that will contain instances of each of these classes.
 class Renderable implements IRenderable { public var view:DisplayObject; public var position:Point; public var rotation:Number; public function render():void { view.x = position.x; view.y = position.y; view.rotation = rotation; } } class Moveable implements IMoveable { public var position:Point; public var rotation:Number; public var velocity:Point; public var angularVelocity:Number; public function move( time:Number ):void { position.x += velocity.x * time; position.y += velocity.y * time; rotation += angularVelocity * time; } } class Spaceship { public var renderData:IRenderable; public var moveData:IMoveable; } 

In this way, we can combine different behaviors in any way without getting any problems with inheritance.
image
These objects, made with this composition: Static Object, Spaceship, Flying Saucer, Asteroid, and Force Field - are collectively called Entities.
Our processes do not change.
 interface IRenderable { function render(); } class RenderProcess implements IProcess { private var targets:Vector.<IRenderable>; public function update(time:Number):void { for each(var target:IRenderable in targets) { target.render(); } } } interface IMoveable { function move(); } class MoveProcess implements IProcess { private var targets:Vector.<IMoveable>; public function update(time:Number):void { for each(var target:IMoveable in targets) { target.move( time ); } } } 

But we will not add a ship object to each process; instead, we will add its components. And, so we get something like this:
 public function createSpaceship():Spaceship { var spaceship:Spaceship = new Spaceship(); ... renderProcess.addItem( spaceship.renderData ); moveProcess.addItem( spaceship.moveData ); ... return spaceship; } 

This approach looks good. It gives the freedom to mix and match support processes in various game objects without porridge chains of inheritance or self-repetitions. But there is one problem.

What to do with general information?


The position and rotation properties in the Renderable class object must have the same values ​​as the rotational position in the Movable object, since the Move process must change their values, and the Render process needs them to be drawn.
 class Renderable implements IRenderable { public var view:DisplayObject; public var position:Point; public var rotation:Number; public function render():void { view.x = position.x; view.y = position.y; view.rotation = rotation; } } class Moveable implements IMoveable { public var position:Point; public var rotation:Number; public var velocity:Point; public var angularVelocity:Number; public function move( time:Number ):void { position.x += velocity.x * time; position.y += velocity.y * time; rotation += angularVelocity * time; } } class Spaceship { public var renderData:IRenderable; public var moveData:IMoveable; } 

To solve this problem, we need to be sure that both objects refer to identical instances of these properties. In ActionScript, this means that these properties must be objects, since objects can be passed by reference, and primitive types are passed by value.
So, we present another set of classes that we call components. These components are a wrapper over the property values ​​to be shared between processes.
 class PositionComponent { public var x:Number; public var y:Number; public var rotation:Number; } class VelocityComponent { public var velocityX:Number; public var velocityY:Number; public var angularVelocity:Number; } class DisplayComponent { public var view:DisplayObject; } class Renderable implements IRenderable { public var display:DisplayComponent; public var position:PositionComponent; public function render():void { display.view.x = position.x; display.view.y = position.y; display.view.rotation = position.rotation; } } class Moveable implements IMoveable { public var position:PositionComponent; public var velocity:VelocityComponent; public function move( time:Number ):void { position.x += velocity.velocityX * time; position.y += velocity.velocityY * time; position.rotation += velocity.angularVelocity * time; } } 

When we create our spacecraft class, we must be sure that Movable and Renderable objects refer to the same PositionComponent instance.
 class Spaceship { public function Spaceship() { moveData = new Moveable(); renderData = new Renderable(); moveData.position = new PositionComponent(); moveData.velocity = new VelocityComponent(); renderData.position = moveData.position; renderData.display = new DisplayComponent(); } } 

This change still does not affect the processes.

And this is a good place to take a break.


Now we have a clear division of tasks. The game loop "twists" the processes, calling the update method for each. Each process consists of a collection of objects that implement an interface by which they can interact with and (process) calls the necessary methods for these objects. Such objects perform a single task with their information. With the help of components, these objects have common information and a combination of different processes can create complex interactions between game objects, while keeping each process relatively simple.
This architecture is similar to many entity systems in game development. It implements the principles of OOP well and it works. But there is something else that can drive you crazy.

Avoiding good object oriented practice


The current architecture uses object-oriented programming principles such as encapsulation and sharing of responsibility — IRenderable and iMovable cover the values ​​and logic by responsibility, updating game objects every frame. And composition - the spacecraft object is created by combining the implementations of the IRenderable and IMovable interfaces. Using the system of components, we are sure that, where necessary, the values ​​are equally available for various data-class objects.

The next step in the evolution of object systems may seem intuitively incomprehensible and destroying the tendencies of the very essence of object-oriented programming. We break the encapsulation of information and logic in the Renderable and Movable implementations. In particular, we will move the logic from these classes to processes.

So this:
 interface IRenderable { function render(); } class Renderable implements IRenderable { public var display:DisplayComponent; public var position:PositionComponent; public function render():void { display.view.x = position.x; display.view.y = position.y; display.view.rotation = position.rotation; } } class RenderProcess implements IProcess { private var targets:Vector.<IRenderable>; public function update( time:Number ):void { for each( var target:IRenderable in targets ) { target.render(); } } } 

It will be this:
 class RenderData { public var display:DisplayComponent; public var position:PositionComponent; } class RenderProcess implements IProcess { private var targets:Vector.<RenderData>; public function update( time:Number ):void { for each( var target:RenderData in targets ) { target.display.view.x = target.position.x; target.display.view.y = target.position.y; target.display.view.rotation = target.position.rotation; } } } 


And this:
 interface IMoveable { function move( time:Number ); } class Moveable implements IMoveable { public var position:PositionComponent; public var velocity:VelocityComponent; public function move( time:Number ):void { position.x += velocity.velocityX * time; position.y += velocity.velocityY * time; position.rotation += velocity.angularVelocity * time; } } class MoveProcess implements IProcess { private var targets:Vector.<IMoveable>; public function move( time:Number ):void { for each( var target:Moveable in targets ) { target.move( time ); } } } 

It will be this:
 class MoveData { public var position:PositionComponent; public var velocity:VelocityComponent; } class MoveProcess implements IProcess { private var targets:Vector.<MoveData>; public function move( time:Number ):void { for each( var target:MoveData in targets ) { target.position.x += target.velocity.velocityX * time; target.position.y += target.velocity.velocityY * time; target.position.rotation += target.velocity.angularVelocity * time; } } } 


It may not be immediately obvious why we did this, but trust me. We got rid of the need for interfaces and the processes are now doing something more important - instead of simply delegating our work in implementing IRenderable or iMovable, it does the work itself.

The first obvious conclusion is that all entities must have the same rendering method, since the render code is now in the RenderProcess. But not only that. We can, for example, create two processes, RenderMovieClip and RenderBitmap, and they can operate on different sets of entities. Thus, we will not lose in the flexibility of the code.

What we get is the ability to substantially refactor our entities in order to get an architecture with a clearer separation and simple configuration. Refactoring starts with a question.

Do we need value classes?


At the moment our essence
 class Spaceship { public var moveData:MoveData; public var renderData:RenderData; } 


Contains two classes
 class MoveData { public var position:PositionComponent; public var velocity:VelocityComponent; } class RenderData { public var display:DisplayComponent; public var position:PositionComponent; } 


These data classes contain three components:
 class PositionComponent { public var x:Number; public var y:Number; public var rotation:Number; } class VelocityComponent { public var velocityX:Number; public var velocityY:Number; public var angularVelocity:Number; } class DisplayComponent { public var view:DisplayObject; } 


And these value classes are used in two processes:
 class MoveProcess implements IProcess { private var targets:Vector.<MoveData>; public function move( time:Number ):void { for each( var target:MoveData in targets ) { target.position.x += target.velocity.velocityX * time; target.position.y += target.velocity.velocityY * time; target.position.rotation += target.velocity.angularVelocity * time; } } } class RenderProcess implements IProcess { private var targets:Vector.<RenderData>; public function update( time:Number ):void { for each( var target:RenderData in targets ) { target.display.view.x = target.position.x; target.display.view.y = target.position.y; target.display.view.rotation = target.position.rotation; } } } 


But the entity should not care about the data classes. All components contain the state of the entity itself. Data classes exist for convenience of processes. We will refactor the code so that the Spaceship class contains the components themselves instead of data classes.
 class Spaceship { public var position:PositionComponent; public var velocity:VelocityComponent; public var display:DisplayComponent; } class PositionComponent { public var x:Number; public var y:Number; public var rotation:Number; } class VelocityComponent { public var velocityX:Number; public var velocityY:Number; public var angularVelocity:Number; } class DisplayComponent { public var view:DisplayObject; } 


By getting rid of information classes and using composite components to define a spacecraft instead, we have eliminated any need for the entity to know which processes can affect it. The ship now contains components that determine its state. Any need to combine these components into other value classes is now transferred to the responsibility of other classes.

Systems and nodes.


A certain part of the core of the Entity System framework (to which we will move in a minute) will dynamically create these objects as needed for the processes. In this simplified context, classes of values ​​will be nothing more than nodes, or leaves, in collections (arrays, linked lists, or any other) that use processes. So for clarity, we will rename them to nodes.
 class MoveNode { public var position:PositionComponent; public var velocity:VelocityComponent; } class RenderNode { public var display:DisplayComponent; public var position:PositionComponent; } 


The processes themselves will not change, but preserving the more general naming rules, we will rename them to systems.
 class MoveSystem implements ISystem { private var targets:Vector.<MoveNode>; public function update( time:Number ):void { for each( var target:MoveNode in targets ) { target.position.x += target.velocity.velocityX * time; target.position.y += target.velocity.velocityY * time; target.position.rotation += target.velocity.angularVelocity * time; } } } class RenderSystem implements ISystem { private var targets:Vector.<RenderNode>; public function update( time:Number ):void { for each( var target:RenderNode in targets ) { target.display.view.x = target.position.x; target.display.view.y = target.position.y; target.display.view.rotation = target.position.rotation; } } } interface ISystem { function update( time:Number ):void; } 


And what is the essence?


One last change - there is nothing special about the Spaceship class. It is just a container for components. So just call it Entity and give it an array of components. We can access these components through their class type (and now show me on the pros - comment of the translator).
 class Entity { private var components : Dictionary; public function add( component:Object ):void { var componentClass : Class = component.constructor; components[ componentClass ] = component' } public function remove( componentClass:Class ):void { delete components[ componentClass ]; } public function get( componentClass:Class ):Object { return components[ componentClass ]; } } 


And this is how we will create our spaceships:
 public function createSpaceship():void { var spaceship:Entity = new Entity(); var position:PositionComponent = new PositionComponent(); position.x = Stage.stageWidth / 2; position.y = Stage.stageHeight / 2; position.rotation = 0; spaceship.add( position ); var display:DisplayComponent = new DisplayComponent(); display.view = new SpaceshipImage(); spaceship.add( display ); engine.add( spaceship ); } 


Engine core class


We should not forget about the system manager, previously known as the process manager:
 class SystemManager { private var systems:PrioritisedList; public function addSystem( system:ISystem, priority:int ):void { systems.add( system, priority ); system.start(); } public function update( time:Number ):void { for each( var system:ISystem in systemes ) { system.update( time ); } } public function removeSystem( system:ISystem ):void { system.end(); systems.remove( system ); } } 


This class will be expanded and will become the heart of our framework. We will add to its functionality the above ability to dynamically create nodes for systems.

Entities deal only with components, and systems deal only with nodes. , ( , , — . ), , . , , . .

. , . , .
 public class Engine { private var entities:EntityList; private var systems:SystemList; private var nodeLists:Dictionary; public function addEntity( entity:Entity ):void { entities.add( entity ); //            //         //        } public function removeEntity( entity:Entity ):void { //  ,    //       entities.remove( entity ); } public function addSystem( system:System, priority:int ):void { systems.add( system, priority ); system.start(); } public function removeSystem( system:System ):void { system.end(); systems.remove( system ); } public function getNodeList( nodeClass:Class ):NodeList { var nodes:NodeList = new NodeList(); nodeLists[ nodeClass ] = nodes; // create the nodes from the current set of entities // and populate the node list return nodes; } public function update( time:Number ):void { for each( var system:ISystem in systemes ) { system.update( time ); } } } 


image

, , Ash Entity System Framework Asteroids .

Conclusion


, Entity Systems - . , , , . — . , () . , , .

, , . , , , .

Entity System Framework , - . , .

:
- - Entity Component System C++? , . , - Artemis Entity System Framework, , .
:
, :)


Original

Source: https://habr.com/ru/post/197920/


All Articles