├── .busted ├── spec ├── engine_spec.lua └── entity_spec.lua ├── readme.md └── adorbs.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | apiUnit = { 3 | ROOT = {"spec"} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/engine_spec.lua: -------------------------------------------------------------------------------- 1 | describe('Imports, Creates and updates engine', function() 2 | it('imports the engine table', function() 3 | local engine, _, _ = require 'adorbs' () 4 | assert(engine ~= nil) 5 | end) 6 | 7 | 8 | it('has the right initial state', function() 9 | local engine, _, _ = require 'adorbs' () 10 | 11 | assert.are.same(engine.state(), { 12 | entities = {}, 13 | systems = {} 14 | }) 15 | end) 16 | 17 | it('runs process once on empty engine', function() 18 | local engine, _, _ = require 'adorbs' () 19 | local fakeDelta = 0.016 20 | engine.process(fakeDelta) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /spec/entity_spec.lua: -------------------------------------------------------------------------------- 1 | describe('entities', function() 2 | it('imports entity api', function() 3 | local _, _, entity = require 'adorbs' () 4 | assert(entity ~= nil) 5 | end) 6 | 7 | it('creates entity with no components', function() 8 | local _, _, entity = require 'adorbs' () 9 | local entity = entity.create('faker') 10 | 11 | assert(entity ~= nil) 12 | assert.are.same(entity.components, {}) 13 | end) 14 | 15 | it('creates entity with blank components', function() 16 | local _, _, entity = require 'adorbs' () 17 | 18 | local entity = entity.create('faker', { 19 | 'transform' 20 | }) 21 | 22 | assert.are.same(entity.components.transform, {}) 23 | end) 24 | 25 | it('creates entity with populated components', function() 26 | local _, _, entity = require 'adorbs' () 27 | 28 | local entity = entity.create('faker', { 29 | transform = { x = 0, y = 0 } 30 | }) 31 | 32 | assert.are.same(entity.components.transform, { x = 0, y = 0 }) 33 | end) 34 | end) 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # (Totes) Adorbs 2 | 3 | Adorbs is a functional entity framework for LÖVE. The goal was to provide a 4 | minimal entity framework sdk with a centralized game state. 5 | 6 | 7 | ## Getting Started 8 | 9 | Below is an example of a minimal ECS setup in adorbs. 10 | ```lua 11 | local engine, system, entity, component = require 'adorbs' () 12 | 13 | function love.load() 14 | entity.create('player', { 15 | 'characterController', -- components are defined inline, and can be empty, as long as they are a string 16 | transform = { x = 15, y = 0 } 17 | }) 18 | 19 | system.create( 20 | 'systemName', 21 | {'characterController', 'transform'}, 22 | function(delta, characterController, transform) -- called on each entity that matches components 23 | print(transform.x) -- should print out 15 24 | end 25 | ) 26 | end 27 | 28 | function love.draw() 29 | engine.process() 30 | end 31 | ``` 32 | 33 | If you're looking to move past the pare minimum I generally lay my projects out like so: 34 | 35 | ``` 36 | folders 37 | -> components 38 | -> entities 39 | -> systems 40 | ``` 41 | 42 | I populate those folders with my a respective ECS lua module that return a function, here is an example. 43 | 44 | ##### Component 45 | ```lua 46 | -- newTransform.lua 47 | return function(x, y, scale, rotation) 48 | return { x = x, y = y, scale = scale, rotation = rotation} 49 | end 50 | ``` 51 | 52 | ##### Entity 53 | ```lua 54 | return function(spriteSheetLoc, x, y, speed) 55 | entity.create('player', { 56 | characterController = newCharacterController(speed), 57 | transform = newTransform(x, y) 58 | }) 59 | end 60 | ``` 61 | 62 | ##### System 63 | ```lua 64 | return function() 65 | system.create( 66 | 'animator', 67 | {'animation', 'transform'}, 68 | function(dt, animation, transform) 69 | local currentAnimation = animation.animations[animation.current] 70 | 71 | if currentAnimation == nil then 72 | error('You called an animation (' .. animation.current .. ') that does\'nt exist!') 73 | return 74 | end 75 | 76 | currentAnimation:update(dt) 77 | currentAnimation:draw(animation.image, transform.x, transform.y) 78 | end 79 | ) 80 | end 81 | ``` 82 | 83 | Then in your main.lua or scene file you just `require` what you need and they get added to the state automatically. 84 | 85 | ```lua 86 | -- overworld.lua 87 | 88 | local scene = {} 89 | require 'systems/map' () 90 | require 'systems/inputController' () 91 | require 'systems/animator' () 92 | 93 | function scene:enter() 94 | require 'entities/map' ('maps/test') --you can make the functions accept arguments 95 | require 'entities/player' ('assets/animplayers.png', 20, 20, 40) 96 | end 97 | 98 | function scene:draw(dt) 99 | engine.process() 100 | end 101 | 102 | return scene 103 | ``` 104 | -------------------------------------------------------------------------------- /adorbs.lua: -------------------------------------------------------------------------------- 1 | local engine = {} 2 | local system = {} 3 | local entity = {} 4 | 5 | local state = { 6 | entities = {}, 7 | systems = {} 8 | } 9 | 10 | -- System 11 | function system.create(name, components, processFunc, status) 12 | state.systems[name] = { 13 | status = status ~= nil and status or 'running', 14 | components = components, 15 | process = processFunc 16 | } 17 | end 18 | 19 | function system.pause(name) 20 | state.systems[name].status = 'paused' 21 | end 22 | -- end System 23 | 24 | -- Engine 25 | function engine.state() 26 | return state 27 | end 28 | 29 | function engine.process() 30 | for _, system in pairs(state.systems) do 31 | for entityName, entity in pairs(state.entities) do 32 | local pluckedEntityComponents = {} 33 | local matchRequirements = true 34 | for _, requiredSystemComponentName in ipairs(system.components) do 35 | if entity.components[requiredSystemComponentName] ~= nil then 36 | pluckedEntityComponents[#pluckedEntityComponents +1] = entity.components[requiredSystemComponentName] 37 | else 38 | matchRequirements = false 39 | break 40 | end 41 | end 42 | 43 | if matchRequirements and #pluckedEntityComponents > 0 and system.status == 'running' then 44 | system.process(love.timer.getDelta(), unpack(pluckedEntityComponents)) 45 | end 46 | end 47 | end 48 | end 49 | 50 | function engine.draw() 51 | for _, system in pairs(state.systems) do 52 | for entityName, entity in pairs(state.entities) do 53 | local pluckedEntityComponents = {} 54 | for _, requiredSystemComponentName in ipairs(system.components) do 55 | if entity.components[requiredSystemComponentName] ~= nil then 56 | pluckedEntityComponents[#pluckedEntityComponents +1] = entity.components[requiredSystemComponentName] 57 | end 58 | end 59 | 60 | if #pluckedEntityComponents > 0 and system.status == 'running' and system.draw ~= nil then 61 | system.draw(love.timer.getDelta(), unpack(pluckedEntityComponents)) 62 | end 63 | end 64 | end 65 | end 66 | 67 | -- Entity 68 | function entity.create(name, components, isActive) 69 | local newEntity = { 70 | status = 'init', 71 | active = isActive, 72 | components = {} 73 | } 74 | 75 | if components ~= nil then 76 | -- Allows the passing of either a string or another table 77 | for componentName, component in pairs(components) do 78 | if type(componentName) == "string" and type(component) == "table" then 79 | newEntity.components[componentName] = component 80 | else 81 | newEntity.components[component] = {} 82 | end 83 | end 84 | end 85 | 86 | state.entities[name] = newEntity 87 | return state.entities[name] 88 | end 89 | -- end Entity 90 | 91 | return function() 92 | return engine, system, entity 93 | end 94 | --------------------------------------------------------------------------------