Log In  

ToriEngine - ECS Metroidvania Engine

Cart #toriengine-0 | 2023-08-07 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
3

I'm building an Entity-Component-System engine-thing to make platforming metroidvania games like Cave Story on Pico-8. I was initially building it for a Jam game, Tori Tower, but the engine fell out of scope x.x

I have run into some issues as I was building the code, and I'd really appreciate if anyone would like to help me here and there ^^

I'll start by explaining how the engine works, so it all becomes easier to digest later:

Architecture

An ECS (Entity-Component-System) is an architecture for games in which the World State is populated by Entities, which are simply containers (tables) who store values. These values are called Components, and each entity has its own set of components, which its own values assigned.

Entities hold no functions. The logic is handled by Systems, which are functions that make changes in the World State every frame. A system does so by filtering out the entities in the world who have components relevant to the system, and executes the function upon each entity selected.

Example: Moving system
A moving system moves every entity that is movable each frame.

So, the system _move will filter out the world table, so that it only selects the entities who have the move and pos component. Those components look a bit like this:

entity_that_moves={

pos = {x=30,y=64} --position component

move = {vel_x=1,vel_y=0,max_vel_x=3,max_vel_y=2,acc_x=1, acc_y=3, friction=0.8} --movement component
}

Filtering out these systems, it'll run the logic for movement:

function(ent)
    ent.move.vel_x=mid(-ent.move.max_vel_x,ent.move.vel_x+ent.move.acc_x,ent.move.max_vel_x)
    ent.move.vel_y= --yadda yadda yadda you get the point
end

With that said, I'll explain how my engine is working.

The Core

The core is simple and compact, and credit goes to @selfsame for creating the system, and @alexr for building upon it:
https://www.lexaloffle.com/bbs/?tid=30039

-- basic
function _has(e, ks)
  for n in all(ks) do
    if e[n]==nil then
      return false
    end
  end
  return true
end

function system(ks, f)
  return function(es)
    for e in all(es) do
      if _has(e, ks) then
        f(e)
      end
    end
  end
end

--extras

function tag(e,k,v)
    if (not v) v=true
    e[k]=v
end

function untag(e,k)
    e[k]=nil
end

function filter(es,k,v)
    local res={}
    for e in all(es) do
        if e[k]==v then
            add(res,e)
        end
    end
    return res
end

function find(es,k,v)
    local res=filter(es,k,v)
    if (#res>0) return res[1]
    return nil
end

function filter_tagged(es,t)
    local res={}
    for e in all(es) do
        if e[t]!=nil then
            add(res,e)
        end
    end
    return res
end

Basically, the function system(ks, f) returns a system function, that when called passing the world as the argument, executes function f upon the entities in the world that have all the components dictated in ks (a table of strings)

The other extra functions are pretty self explanatory. Tagging just means adding a component to an entity that didnt have it before.

Entities and Components

Entities are all stored in the world table, and are made in factory functions, such as mk_player(x,y), that returns a table with all the components of a player, to be added to the world.

Systems

Update systems are called in the _update60() function and rendering systems are called in the _draw function. These are the ones currently implemented:

(credit to @matthughson for the collision system, on this cart: https://www.lexaloffle.com/bbs/?tid=28793)

function _update60()
    -- these are the updater
    -- systems we need (for now)

    _animate(world)

    --_behaviour_system(world)
    _ai_behaviour(world)

    _move_input(world)
    _jump_input(world)
    _atk_input(world)

    _gravity(world)

    _grounded(world)

    _movement(world)

    _map_collisions(world)
    _ent_collisions(world)

    _damaged(world)
    _death(world)

end

function _draw()
    palt(11,true) 
    palt(0,false)
    cls(5)

    colorize(128,133,141,134)
    map()

    -- these are the render systems
    -- we need (for now)

    colorize(129,2,15,7)
    _animspr(world)
    _camera(world)
    pal()
    _ui(world)

    -- just debug stuff
    if debug then
        _debug_vectors(world)

    for p in all(debug_collision_pixels) do 
        pset(p[1],p[2],p[3])
        end
    end
end

Custom Handlers

Collisions are handled in separate functions, because their logic was too extensive to handle in a single system function.

They're separated into two types: colisions with the map tiles and collisions with other entities.

-- update systems

_map_collisions = system({"pos","size","mcoll","jump"},
 function(e)

  collide_side(e)

  collide_roof(e)

  if collide_floor(e) then
   tag(e,"grounded")
  else
   --self:set_anim("jump")
   untag(e,"grounded")
  end

 end)

_ent_collisions = system({"pos","move","ecoll"},
    function(e)
        for typ,tg in pairs(e.ecoll.all) do

        local es=filter(world,"type",typ)
            for e2 in all(es) do

                if e2!=e and overlaps_box_box(
                    e.pos,e.size,
                    e2.pos,e2.size) then

                    --entity collided will become
                    --e[tg]
                    if (not e[tg]) tag(e,tg,{by=e2})

                end
            end
        end
    end
)

-- custom handlers

function collide_side(e)

 local offset=e.size.x/3
 for i=-(e.size.x/3),(e.size.x/3),2 do
 --if e.move.vx>0 then
 if fget(mget((e.pos.x+(offset))/8,(e.pos.y+i)/8),0) then
   e.move.vx=0
   e.pos.x=(flr(((e.pos.x+(offset))/8))*8)-(offset)
   return true
  end
 --elseif e.move.vx<0 then
  if fget(mget((e.pos.x-(offset))/8,(e.pos.y+i)/8),0) then
   e.move.vx=0
   e.pos.x=(flr((e.pos.x-(offset))/8)*8)+8+(offset)
   return true
  end
-- end
 end
 --didn't hit a solid tile.
 return false
end

--check if pushing into floor tile and resolve.
--requires e.move.vx,e.pos.x,e.pos.y,self.grounded,self.airtime and 
--assumes tile flag 0 or 1 == solid
function collide_floor(e)
 --only check for ground when falling.
 if e.move.vy<0 then
  return false
 end
 local landed=false

 --check for collision at multiple points along the bottom
 --of the sprite: left, center, and right.
 for i=-(e.size.x/3),(e.size.x/3),2 do
  local tile=mget((e.pos.x+i)/8,(e.pos.y+(e.size.y/2))/8)
  if fget(tile,0) or (fget(tile,1) and e.move.vy>=0) then
   e.move.vy=0
   e.pos.y=(flr((e.pos.y+(e.size.y/2))/8)*8)-(e.size.y/2)
   landed=true
  end
 end
 return landed
end

--check if pushing into roof tile and resolve.
--requires e.move.vy,e.pos.x,e.pos.y, and 
--assumes tile flag 0 == solid
function collide_roof(e)
 --check for collision at multiple points along the top
 --of the sprite: left, center, and right.
 for i=-(e.size.x/3),(e.size.x/3),2 do
  if fget(mget((e.pos.x+i)/8,(e.pos.y-(e.size.y/2))/8),0) then
   e.move.vy=0
   e.pos.y=flr((e.pos.y-(e.size.y/2))/8)*8+8+(e.size.y/2)
   e.jump.jump_hold_time=0
  end
 end
end

Databases

These are for data sets that are repeated among many entity constructors, or those that are unique
to each entity. Things such as animations and enemy behaviours.
There's a better description of each in the code :3

Backlog

[X] ECS

Gamemodes
[X] Overworld
[ ] Cutscene
[ ] Boss Fight
[ ] Title Screen
[ ] Game Over Screen

Entities
[X] Player
[X] Enemy
[ ] Item
[ ] Moving Blocks/Platforms
[ ] Breakable Block
[ ] Boss

Update Systems
[X] Movement
[X] Solid Collisions
[X] One-Way Platform Collisions
[X] Entity Collisions
[X] Walk Input
[X] Jump Input
[ ] Get Item
[X] Take Damage (any entity with HP)
[ ] Knockback
[~] Die
[~] Enemy Behaviour
[ ] Gameover
[ ] Scriptable Overworld Cutscenes (Toriscript)

Render Systems
[X] Sprites
[X] Sprite Animations
[X] Camera
[X] HP UI
[ ] Current Item UI
[ ] GFX
[ ] Textbox

Custom Handlers
[ ] Change/Load Room
[ ] Story Event Handler
[ ] Save Progress

Current Progress [Aug 07 2023]

I'm currently having issues with implementing the unique entity behaviour. If you see the _ai_behaviour() system and the db_bhvr table, you'll see what I'm talking about.
There's been weird behaviour like this one - the enemy is only supposed to avoid walls and pits; I'm not sure what's causing it .w.

Any help will be sincerily appreciated~
Cheers!

P#132836 2023-08-07 16:15


[Please log in to post a comment]