Log In  


eggs.p8

Hi Pico-8 community.

I'm releasing a library that I have been working on for a little while. It's called eggs. It tries to do what other ECS libraries do in a slightly different way - it replaces Components (the C in ECS) with Tags. And Entities become simple tables.

The library is on github, there is a README with API descriptions and examples and stuff:

https://github.com/kikito/eggs.p8

I am attaching a demo cardridge below.

Cart #test_eggs-0 | 2025-10-04 | Code ▽ | Embed ▽ | No License
4

On it we can see chickens showing different behaviors:

  • Chicks hatching from eggs
  • Growing into puberty and then into adulthood
  • Adult hens laying more eggs
  • Eventually all poultry dies of old age
  • Several stats like the population and CPU usage are shown, they can be hidden with 🅾️
  • You can help the population grow with ❎. Don't worry, population growth is capped in order to avoid maxing out the CPU
  • When the population reaches 100, something happens

I spent a significant amount of effort in making this library usable, and now the only excuse I have to not continue working on libraries and going back to making an actual game is to receive feedback. So, please help me :).

4


Moved the github link to the top of the post since some people missed it.


Wait… The kikito? Awesome finding you on the pico8 community after using your lua libraries for years ;)
Right now I’m working on two Picotron retro game ports using middleclass, bump and stateful.
This eggs ecs implementation looks very promising and I wonder how it could evolve (if needed) in a Picotron project with no token limitations. I mean, your other lua libraries could also be ported for Picotron leveraging the userdata paradigm. Curious to hear/read your opinion on this.


@kc001 yep, that is me! I am glad that you are finding my libraries useful!

> I’m working on two Picotron retro game ports using middleclass, bump and stateful.

I would love to see those!

At some point I need to try to make a smaller Finite State Machine library. For collision detection, you could try using hit + locus:

This should give you 80% of the bump.lua features for 30% of the token cost (and more speed).

> a Picotron project with no token limitations

I think userdata is a missing piece for Lua game development. I definetly missed it when implementing in Pico-8 as well as in LÖVE. Lua is simply not great at handling arrays of bytes with structure. Tables don't cut it, performance-wise.

The thing is that I am extremely comfortable with the constrained environment that Pico-8 provides. I like the token limitation. Perhaps that makes me a weirdo. I think the limitations are one of the reasons I moved away from LÖVE - it even has shaders now. (But seriously the big change for me was becoming a parent. With my limited time, I can hack something up in PICO-8 quickly because of the limitations. With LÖVE, I would have to limit myself)

So that is why in general I am a bit less motivated to make the jump to Picotron. I don't know how much effort I want to dedicate to making libraries that would be restricted to a platform that I find a bit less enjoyable than PICO-8.

If @lexaloffle released the userdata library as a general opensource Luarocks library, or at least backported it to pico-8, I would be more on board with the idea. I could then publish libraries that anyone could use, including the LÖVE people. Right now I'm ... ambivalent.

That said, my libraries are all MIT-licensed so if you want to fork/enhance them, please feel free to do so. I can even give your forks a review if you want (I might not be able to give them a review fast, though)


1

Using hit, locus and eggs I managed to cut the logic of my current project (I may put it in the picotron bbs when I’m ready…) to a minimum while keeping the logic intact. Your code is very elegant and easy to use/extend!
About the parenting and having less time for hobbies/passions, don’t worry, they grow fast and you’ll have plenty of free time sooner than you think (I have three kids…).
A compact and usable state machine for pico-8/picotron would be a great addition to the toolset. Maybe a tiny animation library similar to anim-8 (I have used that as well, stripping out the love2d stuff).
BTW, the PR in the eggs repo is me ;)


> I managed to cut the logic of my current project

Nice! Ping me when you share them for sure!

> I have three kids…

Wow! Huge admiration

> A compact and usable state machine for pico-8/picotron

That will almost certainly happen. I know myself.

> Maybe a tiny animation library similar to anim-8

That makes sense for Picotron, but pico-8 spritesheets are so limited that most animations are 2-4 frames. I don't know if it's worth it.

> the PR in the eggs repo is me

Much appreciated, nice to meet you there!

> BTW, the PR in the eggs repo is me ;)


@kc001 I could not remove it from my head so I just went and sketched the fsm.

https://gist.github.com/kikito/2f17263cfc8745a32a905dbb0ee40937


I put my two current picotron projects in my profile. I'll open up the two github repos if you want to have a look at the source, but the .p64 cartridges are not really obfuscated (I just copied my src and lib folders into the cartridge itself...).
thanks for the fsm, you're the best!
You really deserve more praise for your code, it's very clean and usable.
I struggled a bit with eggs in the beginning, but now I can just easily add a new entity and write some dedicated systems. This is taken from my Necromancer picotron port project:

-- ogres.lua
local function makeOgre(y)
   local x = rnd({Game.Ogre.starting_x_position, Game.screen_size.x})
   local ogre_speed_x = (Game.Ogre.move_speed_x * rnd(1.2)) + 0.5
   local ogre_direction_x = -sgn(x)
   local ogre_direction_y = 0
   local ogre = world.ent("animatable,drawable,movable,ogre", {
      x = x,
      y = y,
      speed = vec(ogre_speed_x, 0),
      box_w = 12,
      box_h = 14,
      sprite_index_offset = Game.Ogre.sprite_index_offset,
      animation_start = 1,
      animation_end = 0,
      animation_frame_rate = 30,
      sprite = 1,
      direction = vec(ogre_direction_x, ogre_direction_y),
      score = ogre_speed_x * 10
   })
   loc.add(ogre, ogre.x, ogre.y, ogre.box_w, ogre.box_h)
end

-- game scene
function Stage1:_setupSystems()
   self.animate_system = require("animate_system")
   self.clear_ogres_system = require("clear_ogres_system")
   self.control_system = require("control_system")
   self.draw_entities_system = require("draw_entities_system")
   self.move_ogres_system = require("move_ogres_system")
   self.ogres_collision_system = {}
   -- and more
end

return world.sys("animatable", function(entity)
   entity.animation_timer = (entity.animation_timer or 0) + 1
   if entity.animation_timer < entity.animation_frame_rate then return end
   entity.animation_timer = 0
   entity.sprite += 1
   if entity.sprite > entity.animation_end then
      entity.sprite = entity.animation_start
   end
end)

-- move_ogres_system.lua
local function is_entity_type(entity, entity_type)
   return world.msk(entity)[entity_type]
end

local function handleCollision(ogre, entity_type, tag_ogre, tag_entity, ...)
   local left, top, width, height = ...
   for entity in pairs(loc.query(left, top, width, height,
      function(e) return is_entity_type(e, entity_type) end)
   ) do
      if Rect2RectOverlap(
            ogre.x, ogre.y, ogre.box_w, ogre.box_h,
            entity.x, entity.y, entity.box_w, entity.box_h
         ) then
         if tag_entity then
            world.tag(entity, tag_entity)
         end
         if tag_ogre then
            world.tag(ogre, tag_ogre)
         end
      end
   end
end

local function collisionWithTree(ogre, ...)
   handleCollision(ogre, "tree", "hittree", "hitogre", ...)
end

local function collisionWithAdultTree(ogre, ...)
   handleCollision(ogre, "adult_tree", "hittree", nil, ...)
end

local function collisionWithPlayer(ogre, ...)
   handleCollision(ogre, "player", "hitplayer", nil, ...)
end

return world.sys("movable,ogre", function(ogre)
   local next_x = ogre.x + ogre.direction.x * ogre.speed.x
   local next_y = ogre.y + ogre.direction.y * ogre.speed.y

   collisionWithPlayer(ogre, GetNextBox(ogre, next_x, next_y))
   collisionWithTree(ogre, GetNextBox(ogre, next_x, next_y))
   collisionWithAdultTree(ogre, GetNextBox(ogre, next_x, next_y))

   ogre.x = next_x
   ogre.y = next_y
   loc.update(ogre, ogre.x, ogre.y, ogre.box_w, ogre.box_h)
end)

I admit I never managed to fully adopt the E(C)S system for one of my projects. I usually get stuck between wanting to write systems that want to manage a whole lot of different entities and mini systems for specific entity sets.

eggs.p8 tagging feature is really the best of both worlds and it's very similar to the pure ECS architecture of the old ash system from Richard Lord, especially the Node system which is very similar to the tagging system.


Cool!

> In my profile

I was getting a bit crazy because I thought they were on your github profile, but they are on your lexaloffle profile! So, here: https://www.lexaloffle.com/bbs/?uid=35587 . I will check them out.

I hope it is ok if I give some feedback about your code.

Is_entity_type is somewhat expensive:

local function is_entity_type(entity, entity_type)
   return world.msk(entity)[entity_type]
end

That is calling 2 function calls + 3 table accesses. You could replace all of that by a single table access by adding type = "ogre" on the entity creation. And then doing:

if entity.type == "ogre"

This is less flexible (you can't be a player and an ogre at the first time, or an ogre and a tree) but the extra flexibility might not be needed.

Looking at your ogre system, it looks like your system does 3 things:

  • For each enemy that could intersect with the ogre if they are on their box:
    • Collision detection using rectangle intersection between an ogre and 3 kinds of entities (tree, adultree, player)
    • When a collission with those three happens, add a tag to the ogre, the collided object, or both

I propose these changes:

  1. You are iterating over locus multiple times, one for the player, one for the trees, one for the adult trees. Unless it is important to do the player first, then the trees, and then the old trees, you could do a single locus query instead, using a slightly more complex filter.

  2. Given that all your system does is colliding and then add tags to each colliding part, you could have a "table that says what tags are set".

Extremely rough sketch:

-- ogre.lua

local function ogre_collidable_filter=function(other)
  return is_entity_type(other,"player")
      or is_entity_type(other,"tree")
      or is_entity_type(other,"old_tree")
end

local ogre_collidable_tags = {
  player = { me = "hitplayer" },
  tree = { me = "hittree", other = "hitogre" },
  old_tree = { other = "hittree" },
}

local function makeOgre(y)
  ...
  local ogre = world.ent("...,collidable", {
    ...
    collidable_filter = ogre_collidable_filter,
    collidable_tags = ogre_collidable_tags,
    ...
  }
end

-- (new file) collidable.lua
...

return world.sys("movable,collidable", function(me)
   local next_x = me.x + me.direction.x * me.speed.x
   local next_y = me.y + me.direction.y * me.speed.y

   for other in pairs(loc.query(left, top, width, height, me.collidable_filter) do
     if Rect2RectOverlap(
       me.x, me.y, me.box_w, me.box_h,
       other.x, other.y, other.box_w, other.box_h
     ) then
       for ctag,tags in me.collidable_tags do
         if is_entity_type(other,ctag) then
           if tags.me then
             world.tag(me, tags.me)
           end
           if tags.other then
             world.tag(other, tags.other)
           end
         end
       end
     end
   end
end)

Now the collidable system now knows nothing about ogres and can be used in a similar way for other entities.


Thanks a ton for the suggestions, I noticed the 30% CPU usage and I wondered if I was doing multiple unnecessary loops.

I'm also using the entity type tag (ogre, tree, etc.) for collision resolution based on those "hit<entity>" tags.
Since I should move away from tagging ogre entities with "ogre", I could completely avoid using the intermediate step of tagging colliding entities (locus query => tag entity => resolve collision) and just fire a specific function callback from a table of callbacks assigned to the entity:

-- functions.lua
--- Checks if an entity is of a specific type
--- @param entity any
--- @param entity_types string
---  @Return boolean
function IsEntityType(entity, entity_types)
   local entity_type_table = split(entity_types)
   local result = false
   for entity_type in all(entity_type_table) do
      if entity.type == entity_type then
         result = true
         break
      end
   end
   return result
end

-- ogres.lua
local ogre_collidable_filter = function(other)
   return IsEntityType(other, "player,tree,adult_tree,poisoned_tree,dead_tree")
end

local ogre_collidable_callbacks = {
   player = {fn = <collision_with_player_callback>, fired = false}
   tree = {fn = <collision_with_tree_callback>, fired = false}
   ...
}

local ogre = world.ent("..., movable,collidable", {
   ...
   type = "ogre"
   collidable_filter = ogre_collidable_filter
   collidable_callbacks = ogre_collidable_callbacks
})

-- collidable.lua
world.sys("collidable,movable",
   function(me)
      local left, top, width, height = GetNextBox(me)
      for other in all(loc.query(left, top, width, height, me.collidable_filter)) do
         if Rect2RectOverlap(
            me.x, me.y, me.width, me.height,
            other.x, other.y, other.width, other.height
         ) then
            for tag, callback in pairs(me.collidable_callbacks) do
               if IsEntityType(other, tag) and not callback.fired then
                  callback.fn()
                  callback.fired = true
               end
            end
         end
      end

      local next_x = me.x + me.direction.x * me.speed.x
      local next_y = me.y + me.direction.y * me.speed.y
      me.x, me.y = next_x, next_y
      loc.update(me, me.x, me.y, me.width, me.height)
   end)

I think this is less cpu intensive than using a separate system to resolve collisions because I couldn't avoid doing another locus query to fire the appropriate callback.

What do you think?

EDIT:
I managed to convert all the systems to this new logic and regained 20% CPU, but I'm struggling with the resetting of callback.fired to false to avoid either double firing or firing the callback only once and never again.
Right now I'm managing by resetting the callback.fired when clearing entities or through other systems with constant polling. Maybe I should use a dedicated system to reset the callbacks at the start or at the very end of the update function...



[Please log in to post a comment]