The Entity Component System (ECS)
I set myself the task to develop such a system in AGK-Basic (Tier 1). In an object oriented environment like C# or C++ this is no problem.
The possibility to create classes with inheritance and overrides are very helpful. But AppGameKit does not offer this comfort.
ECS - what is that?
Some of you may have already heard or read something about it. I'll try to define it in my own words.
Entity
An entity, roughly speaking, can be anything in a game environment. Every object that is displayed. But also what is not visible. For example also any triggers. It does not have any functions of its own. It is rather a collection of data (components) that describe a function or behavior. So the entity is a description about the appearance, behavior and function of an object.
Component
The component contains only low-level data. No functionality or logic. Only data. Typical components would be for example position, acceleration and appearance. The position would have x,y and z values. Acceleration would have corresponding acceleration values and Appearance would have, for example, sprite data. The components are then used to describe the entities.
System
The systems execute the logic, behavior and functions of the entities. They are the control centers of all entities. Usually a system is developed for each behavior. For example For motion, the motion system would go through components that have an acceleration and position and calculate and write back the new position data. While the representation system would then go through all the sprite components and represent them. Thus, a system would have to be developed for each behavior.
Here would be a link for a better description.
I think the concept can be implemented in several ways.
As an example of an implementation of ECS, I have developed a Space Invaders clone.
The player, for example, is an entity. This entity has certain properties. It must be represented. It has to move. It is controllable. So we have three systems that have to be implemented.
- Display system
- Motion system
- Control system
In addition, there is an entity system. This system is responsible for the creation and deletion of entire entities. It also generates unique ID's that are used to identify the entities. With it then also the individual components are connected with one another. Thus can be determined which component to which Entity belongs.
This system is not a system in the sense of ECS. A better designation, in order to avoid confusion, would be Entity server. Because it serves us with new entities or deletes them.
So how do all these parts work together?
The entity server creates the entities, these call the system functions to create the components which are important for the respective system. The individual components are stored in arrays. In order for them to be found again, they need the entity name, or number in my case. The system then goes through the components and determines the behavior according to their components.
Here is an example of a component from my project.
// speed component for setting a speed for an entity
type TComponent_Velocity
entity as integer
speed as float
endtype
For example, the player entity is created like this.
Function SysEntity_CreatePlayer(posx as float, posy as float)
typCmp As TComponent_EntityType
spr_id as integer
ent_id as integer
// create entity id and sprite id
ent_id = SysEntity_GetFreeID()
spr_id = CloneSprite(SPRID_PLAYER)
// create entity type
typCmp.entity = ent_id
typCmp.typ = SYSENT_TYPE_PLAYER
gComponents.types.insertsorted(typCmp)
// create players property
SysRenderer_AddProperty(ent_id,spr_id,posx,posy,3)
SysMovement_AddProperty(ent_id,128,0,0)
SetSpriteVisible(spr_id,0)
// move left (left arrow key, joystick x-axis)
SysAction_AddProperty(ent_id,CMP_ACTION_MOVELEFT,37,-1)
SysAction_AddProperty(ent_id,CMP_ACTION_MOVELEFT,CMP_INPUT_JOYAXISX,0)
// move right (right arrow key, joystick x-axis)
SysAction_AddProperty(ent_id,CMP_ACTION_MOVERIGHT,39,1)
SysAction_AddProperty(ent_id,CMP_ACTION_MOVERIGHT,CMP_INPUT_JOYAXISX,0)
// shoot (space key, joystick button 1)
SysAction_AddProperty(ent_id,CMP_ACTION_FIRE,32,1)
SysAction_AddProperty(ent_id,CMP_ACTION_FIRE,CMP_INPUT_JOYBUTTON1,0)
// add screen-bound collision
SySCollision_AddProperty(ent_id,32,0,GetVirtualWidth()-32,GetVirtualHeight())
// add collision group
SysCollision_AddPropertyGroupCollision(ent_id,CMP_GROUP_PLAYER,0)
// add players weapon
SysWeapon_AddProperty(ent_id, CMP_WEAPONTYPE_PLAYER)
// add players statistics
SysHUD_AddPropertyStatistic(ent_id)
EndFunction ent_id
In the last code snippet you can already see which system I use.
A ..
_AddProperty finally adds all the components of the system.
For the movement, it looks like this.
/*
All the components necessary for the 'property movement' are created here.
PARAMETER
ent - the entity to whom the property should be attributed.
vel - sets the speed of the entity
xdir,
ydir - the direction in which the entity is moved. It is a normalised vector.
*/
Function SysMovement_AddProperty(ent as integer, vel as float, xdir as float, ydir as float)
dirCmp as TComponent_Direction
velCmp as TComponent_Velocity
// setup move component
dirCmp.entity = ent
dirCmp.dx = xdir
dirCmp.dy = ydir
gComponents.direction.insertsorted(dirCmp)
// setup velocity component
velCmp.entity = ent
velCmp.speed = vel
gComponents.velocity.insertsorted(velCmp)
EndFunction
For the update of this system it goes through the array velocity.
/*
Calculates the new position of all entities that have a position comoponent,
velocity component and direction component.
PARAMETER
ft - The movement is time-based, so the time between the last frame and
this frame is passed here. (frame time)
*/
Function SysMovement_Update(ft as float)
vel as TComponent_Velocity
dir as TComponent_Direction
pos as TComponent_Position
i as integer
index as integer
typ As Integer
For i = 0 To gComponents.velocity.length
vel = gComponents.velocity[i]
// if direction component not available, continue loop, otherwise get component
index = GetComponentDirection(vel.entity)
If index < 0 then continue else dir = gComponents.direction[index]
typ = SysEntity_GetEntityType(vel.entity)
// if current entity a player entity? then ...
If typ = SYSENT_TYPE_PLAYER
// ... reset movement, movement must be set for each new frame
gComponents.direction[index].dx = 0
gComponents.direction[index].dy = 0
//ElseIf typ = SYSENT_TYPE_SWARM
//Inc vel.speed,(55-gComponents.swarm[0].rel_pos.length)*3
EndIf
// if direction component not available, continue loop
// we then need the index of the position component to write back the values.
index = GetComponentPosition(vel.entity)
if index < 0 then continue
// calculate the new position
Inc gComponents.position[index].px, dir.dx*(vel.speed*ft)
Inc gComponents.position[index].py, dir.dy*(vel.speed*ft)
Next
EndFunction
This is of course only a rough outline. A finished project is available for download. First the DOCUMENTED source code without media. The second is the compiled game. Packed with Vishnu Studio. Great Program for media protection
.
I have omitted the sound for now, because I wanted to try it out first how everything could work under BASIC.
I really enjoyed this experiment. Because thinking in the ECS sense is different than the typical approach I usually followed in game development. That's how I feel about it.
What about you?
Are you already familiar with ECS?
Have you already used it?
What does your implementation look like?