Entity Component System (ECS) for PICO-8 & Picotron in 567 tokens.
(Based on KatrinaKitten's excellent Tiny ECS Framework v1.1)
Everything is part of a World. Create one with pecs()
:
local world = pecs()
Components describe data containers that can be instantiated:
local Position = world.component()
local Velocity = world.component()
An Entity is a collection of instantiated Components.
local player = world.entity()
player += Position({ x=10, y=0 })
player += Velocity({ x=0, y=1 })
All data within an Entity can be accessed as long as you know the Component it belongs to:
print(player[Position].x, 10, 10, 7)
Systems allow specifying game logic (as a function) which applies to Entities that have a certain set of Components (ie; a filter).
The game logic function of a System is executed once per matched Entity, ensuring performance is maintained when there are many entities. The function receives any arguments passed when calling the method. Useful for passing in elapsed time, etc.
local move = world.system({ Position, Velocity }, function(entity, ticks)
entity[Position].x += entity[Velocity].x * ticks
entity[Position].y += entity[Velocity].y * ticks
end)
-- Run the system method against all matched entities
-- Any args passed will be available in the system callback function
local ticks = 1
move(ticks)
local world = pecs()
local Position = world.component()
local Velocity = world.component()
local player = world.entity({ name="Jess" })
player += Position({ x=10, y=0 })
player += Velocity({ x=0, y=1 })
local move = world.system({ Position, Velocity }, function(entity, ticks)
entity[Position].x += entity[Velocity].x * ticks
entity[Position].y += entity[Velocity].y * ticks
end)
local lastTime = time()
function _update()
move(time() - lastTime)
lastTime = time()
end
function _draw()
cls()
print(player[Position].x.." "..player[Position].y, 10, 10, 7)
end
For more complete & practical examples, see the example/
folder:
example/particles.p8
: A Particle Emitter showing how to spawn entities and adding/removing components (the type of Emitter) on a button press.example/camera-follow.p8
: A camera follow/window technique built using Components & Systems. This example has a visual representation of the "camera" to see the effect.
Everything in PECS happens within a world.
Can be called multiple times to create multiple worlds:
local world1 = pecs()
local world2 = pecs()
Each world has its own Components and Entities.
Must be called at the start of each _update()
before anything else.
local player = world.entity()
local trap = world.entity({ type="spikes" })
local enemy = world.entity({}, Position({ x=10, y=10 }), Rotation({ angle=45 })
player += Position({ x=100, y=20 })
player -= Position
local Position = world.component()
local Drawable = world.component({ color: 8 })
Where filter
is a table of Components, and callback
is a function that's
passed the entity to operate on.
Returns a function that when called will execute the callback
once per Entity
that contains all the specified Components.
When executing the function, any parameters are passed through to the
callback
.
local move = world.system({ Position, Velocity }, function(entity, ticks)
entity[Position].x += entity[Velocity].x * ticks
entity[Position].y += entity[Velocity].y * ticks
end)
-- Run the system method against all matched entities
-- Any args passed will be available in the system callback function
local ticks = 1
move(ticks)
Systems efficiently maintain a list of filtered entities that is only updated when needed. It is safe to create many systems that operate over large lists of Entities (within PICO-8's limits).
World#system
(which calls query
internally).
Where filter
is a table of Components, eg; { Position, Velocity }
.
Return a reference to a filtered table of Entity
s. The table is automatically
updated when new entities match or old entities no longer match. Modifying this
table can cause Systems to misbehave; treat it as read-only.
local entities = world.query({ Position })
for _, ent in pairs(entities) do
printh(ent[Position].x .. "," ..ent[Position].y)
end
Queries efficiently maintain a list of filtered entities that is only updated when needed. It is safe to create many queries that operate over large lists of Entities (within PICO-8's limits).
Remove the given entity from the world.
Any Systems or Queries which previously matched this entity will no longer operate on it.
Useful for delaying actions until the next turn of the update loop. Particularly when the action would modify a list that's currently being iterated on such as removing an item due to collision, or spawning new items.