Aspect - an extremely minimal ECS module

Showcase your libraries, tools and other projects that help your fellow love users.
Post Reply
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Aspect - an extremely minimal ECS module

Post by airstruck »

Aspect is a minimal ECS with a focus on simplicity through small API and intuitive operation.

Code: Select all

local Aspect = require 'aspect'
The Aspect API consists of two functions, Aspect.system and Aspect.each.

Aspect.system (aspects, process)

Creates a new system. The system will process entities with components matching each member of the aspects array.

Code: Select all

local drawRectangle = Aspect.system(
    { 'position', 'size', 'color' },
    function (pos, size, color)
        love.graphics.setColor(color)
        love.graphics.rectangle('fill', pos.x, pos.y, size.width, size.height)
    end
)
param aspects

An array of keys which must be present in an entity in order to process it.

param process

A function to run on the components of each matching entity. The function will receive one argument for each member of the aspects array, containing a component.

return function (entities, [context])

The newly created system is returned in the form of a function taking a list of entities to process. The user may optionally pass in a context value which will be made available via the _context meta-aspect (see below).

Aspect.each (entities, aspects, [context], [process])

Process entities without creating a system. May be used from within a system to process entity interactions.

Code: Select all

for pos, vel in Aspect.each(entities, { 'position', 'velocity' }) do
    pos.x = pos.x + vel.x
    pos.y = pos.y + vel.y
end
param entities

An array of entities to process.

param aspects

An array of keys which must be present in the entity in order to process it.

param context

Optional user-defined context value, available via the _context meta-aspect (see below).

param process

Optional function to process matching entities. If omitted, Aspect.each will act as an iterator as in the example above.

Meta-aspects

Meta-aspects work like regular aspects: supply their names in an aspects list and get a matching process argument or iterator result.

_context

User-defined data passed in as the second argument when invoking a system or the third argument to Aspect.each.

_entities

The list of entities being processed.

_entity

The entity being processed. Avoid using this; use components directly whenever possible.

_index

The index of the entity being processed in the entities list.

aspect.lua
Just cut and paste into a file for now. If there's enough interest in this I'll github it later.

Code: Select all

local Aspect = {}

local unpack = table.unpack or _G.unpack

function Aspect.each (entities, aspects, context, process)
    function iterate (process) 
        local meta = { _context = context, _entities = entities }
        for index, entity in pairs(entities) do
            local components = {}
            local isMatchingEntity = true
            meta._entity = entity
            meta._index = index
            for aspectIndex, aspect in pairs(aspects) do
                local component = meta[aspect] or entity[aspect]
                if not component then 
                    isMatchingEntity = false
                    break
                end
                components[aspectIndex] = component
            end
            if isMatchingEntity then
                process(unpack(components))
            end
        end
    end
    if process then
        iterate(process) 
    else 
        return coroutine.wrap(function ()
            iterate(coroutine.yield)
        end)
    end
end

function Aspect.system (aspects, process)
    return function (entities, context)
        Aspect.each(entities, aspects, context, process)
    end
end

return Aspect
I'm interested in hearing thoughts on this. How can it be improved? What's missing? I'll post a simple example project soon.
User avatar
bakpakin
Party member
Posts: 114
Joined: Sun Mar 15, 2015 9:29 am
Location: Boston

Re: Aspect - an extremely minimal ECS module

Post by bakpakin »

This looks pretty cool! Calling it an ECS module is almost misleading (it's almost too simple), but I really like the concise code and the functional approach to ECS.

I really like ECS and think it is one of the best way to do game programming. I made an ECS module trying to not have too complicated an api, but this really takes the cake for simplicity. Here it is viewtopic.php?f=5&t=79937.

I especially like the context passing idea for passing and maintaining state.

One simple (improvement?) could be adding the option to use a function as an aspect. The function could take 1 or 2 parameters, an entity and optionally the context, and return a boolean value if the entity matches. Just an idea, might not match your idea for the module.
((_((_CRAYOLA_((_((_> GitHub <_((_((_CRAYOLA_((_(()
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Aspect - an extremely minimal ECS module

Post by airstruck »

bakpakin wrote:Calling it an ECS module is almost misleading (it's almost too simple), but I really like the concise code and the functional approach to ECS.
Thanks, I'm really trying to deconstruct ECS as much as possible with this. I'm trying not to put anything in the API that can be fairly reasonably done without it. For example, you don't add systems into a world, or bind them to an entity list, because you can just do this:

Code: Select all

function love.update (dt)
    updateMomentum(entities, dt)
    updateGravity(entities, dt)
    updateCollision(entities, dt)
end
Now I want to explore what are the drawbacks of this; what benefits did we lose by scapping (for example) the "world" concept? How much trouble is it for users to recreate whatever functonality was lost with their own "world?"
bakpakin wrote:One simple (improvement?) could be adding the option to use a function as an aspect. The function could take 1 or 2 parameters, an entity and optionally the context, and return a boolean value if the entity matches. Just an idea, might not match your idea for the module.
I think I like this idea. I'm looking at your module now; I think what you're proposing would cover the require/reject all/any part of your API. Under this module an "aspect" is just a key in the entity corresponding to a component value, and those components are passed into the process function, so I'm not sure how an aspect dealing with multiple components would fit in. The function could return multiple values, but it might be difficult to keep track of when creating systems. I'll think about this more.
User avatar
bakpakin
Party member
Posts: 114
Joined: Sun Mar 15, 2015 9:29 am
Location: Boston

Re: Aspect - an extremely minimal ECS module

Post by bakpakin »

Now I want to explore what are the drawbacks of this; what benefits did we lose by scapping (for example) the "world" concept? How much trouble is it for users to recreate whatever functionality was lost with their own "world?"
We lose a couple things, but mainly the ability to easily cache which entities get processed by which systems. Caching which entities get processed by which systems can supposedly increase performance, but often it's not too much of an issue. Also, ordered traversal of entities would be difficult with your approach, when different systems had to traverse entities in different orders.

Still, there are plenty of other problems with a more heavy-weight ecs implementation, like slightly more memory use, harder to integrate api, and a lot more state everywhere.
((_((_CRAYOLA_((_((_> GitHub <_((_((_CRAYOLA_((_(()
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Aspect - an extremely minimal ECS module

Post by airstruck »

bakpakin wrote:We lose a couple things, but mainly the ability to easily cache which entities get processed by which systems.
Thanks for your input on this. I've been trying to decide whether caching is worth it. In this case it might be nice, because instead of caching entities, we could cache component lists (since process receives components instead of entities). I don't see any reason the cache needs to be stored in the world, though... we should be able to store it in the system instead. Of course we'd have to use "functables" for that, but none of that complexity needs to be exposed to the user, except maybe a way to manually clear the cache.

The problem with caching is that we'd need to invalidate it when the entities list changes, or if componenents are being added and removed on the fly. So that means all that stuff would need to be managed by the API, or the user would have to manually clear the cache, and know when to do it. Overall, none of this seems worth it unless there is a significant boost in performance. Also, the user could maintain multiple entity lists to squeeze more performance out.
bakpakin wrote: Also, ordered traversal of entities would be difficult with your approach, when different systems had to traverse entities in different orders.
Ordered traversal is something I didn't consider. I can see where you might want to reverse-iterate if you needed to remove entities on the fly, or do sorted iteration for a drawing routine where there's some kind of z-index component to sort by. I suppose this could just be another optional argument passed to each in the form of an iterator function. I'll think about it more.

Thanks for bringing these issues up, I'll have a closer look at your project and see what else I might be missing.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Aspect - an extremely minimal ECS module

Post by airstruck »

Added support for entity ids, caching and ordered traversal.

To set up caching for an entity list, call Aspect.cache(entities). To invalidate the cache, call Aspect.cache(entities) again. If you want to stop using the cache at runtime for some reason, call Aspect.uncache(entities).

This does not modify the entities list in any way, it just lets Aspect know it should use a cache when applying systems to that entity list.

Ordered traveral is handled through a new optional iterator parameter for Aspect.each and Aspect.system. This defaults to pairs. Aspect.reverse may be passed to iterate in reverse. Custom iterators should have a function signature compatible with pairs.

Context arguments work differently now; _context has gone away and any extra arguments passed into a system are appended to the process arguments after all components.

Also added basic support for entity ids. Get an id for an entity with Aspect.ids[entity], and find an entity by id with Aspect.entities[id].

I'll update the top post with new API docs soon.

https://gist.github.com/airstruck/5cbfa9af10950fe57852
User avatar
bakpakin
Party member
Posts: 114
Joined: Sun Mar 15, 2015 9:29 am
Location: Boston

Re: Aspect - an extremely minimal ECS module

Post by bakpakin »

Nice! This actually looks pretty useful, elegant, and easy to use now.
((_((_CRAYOLA_((_((_> GitHub <_((_((_CRAYOLA_((_(()
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Aspect - an extremely minimal ECS module

Post by airstruck »

Thanks, I may tweak it a little more though. Might have cache enabled by default and user has to explicity disable it per entities list. Would encourage clearing the cache at appropriate times, so people don't add caching after they write all their systems and break stuff.

Maybe also a user-configurable way to resolve "aspects" (component keys) other than just indexing the entity with them.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Aspect - an extremely minimal ECS module

Post by airstruck »

To anyone who starred the gist, the code is now here.
Post Reply

Who is online

Users browsing this forum: No registered users and 18 guests