Skip to content

Entity Component System (ECS) for PICO-8 & Picotron

License

Notifications You must be signed in to change notification settings

jesstelford/pecs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation


PECS

Entity Component System (ECS) for PICO-8 & Picotron in 567 tokens.
(Based on KatrinaKitten's excellent Tiny ECS Framework v1.1)



Usage

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)

Example

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.

API

pecs() => World

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.

World#update()

Must be called at the start of each _update() before anything else.

World#entity([attr[, Component, ...]]) => Entity

local player = world.entity()

local trap = world.entity({ type="spikes" })

local enemy = world.entity({}, Position({ x=10, y=10 }), Rotation({ angle=45 })
Adding a Component to an Entity
player += Position({ x=100, y=20 })
Removing a Component from an Entity
player -= Position

World#component([defaults]) => Component

local Position = world.component()

local Drawable = world.component({ color: 8 })

World#system(filter, callback) => Function

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#query(filter) => Table<Entity>

⚠️ It is recommended to use World#system (which calls query internally).

Where filter is a table of Components, eg; { Position, Velocity }.

Return a reference to a filtered table of Entitys. 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).

World#remove(Entity)

Remove the given entity from the world.

Any Systems or Queries which previously matched this entity will no longer operate on it.

World#queue(Function)

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.