Components:
- Managed by systems, self-contained as much as possible
- May have references to components in other systems that they're dependent on (eg a sprite might want a position, though it may accept any vector (shared or unshared) for that position)
- Generally take those references in their constructor
- Generally designed to have a nice oop-y method interface, rather than be "just" data - eg
sprite:set_frame(fx, fy)
,animation:reset()
etc
Systems:
- Do stuff with their set of components
- Have "final say" when it comes to those components, including jettisoning them into space (most important for stuff like sorting lists of sprites on z, keeping invisible in a separate list, etc)
- Are where component addition/removal happens - no entity required
- There's a base system with support for deferred removal (uses
push
/pop
/with(f)
style and removes everything once we hit the base level) to prevent anything funny happening when removing components mid-iteration
Entities:
- used for managing the setup of components
entity:new()
takes a list of systems so you can have multiple worlds going at onceentity:add_component(system, name, args)
does what you'd hopefully expect, creating a new component in system, naming it name as far as this entity is concerned, and passing args through to the system component constructor (should probably use varargs but all of them take a table anyway)entity:c(n)
andentity:get_component(name)
are aliases- also have an event system built in, as it's handy to be able to say
player:e("hurt")
rather than have a dedicated component to bother with
Kernel:
- have systems and tasks
- can have "other global stuff" attached as part of its table, as it's visible to all systems
- tasks are single functions in an ordered collection by name
- systems have an ordering (implicitly the order they were added if not defined)
- when a system is added, it gets a
register
callback with the kernel - systems generally add tasks for update/draw here as well as listening to any global pubsub stuff
- base system also has a default "register" implementation that adds update/draw tasks to the kernel if those functions exist in the system
Example:
Deep Sky's game state kernel setup looks like this (m is the state machine for the game state)
m.k = kernel:new({
camera = world.cam,
})
:add_system("sprites", sprite_system:new(render_to_world), 3)
:add_system("tilemap_sprites", sprite_system:new(render_to_world_shader), 3)
:add_system("sprites_overlay", sprite_system:new(render_to_world), 5)
:add_system("tilemap_sprites_overlay", sprite_system:new(render_to_world_shader), 5)
:add_system("hud_sprites", sprite_system:new(render_to_screen), 20)
:add_system("physics", physics_system:new())
:add_system("beat", beat_system:new(0.75))
:add_system("behaviour", behaviour_system:new())
:add_system("behaviour_timeless", behaviour_system:new())
:add_system("animation", animation_system:new())
Update for the entire thing:
m.k:update(dt)
Parallax foreground entity constructor, has an example of the behaviour system being a cheeky catch-all
return function(systems, args)
local asset = args.asset
if not asset then
return nil
end
local e = entity:new(systems)
--set up the sprite
local s = e:add_component("tilemap_sprites_overlay", "sprite", asset)
local f = args.frame or love.math.random(6)
s.pos:vset(args.pos)
s.frame:sset(
math.floor(f % 2),
math.floor(f / 2)
)
s.framesize:sset(192/512, 128/512)
s.size:sset(6, 4)
if args.flip then
s.x_flipped = true
end
s.z = 10 + args.parallax_factor * 10
--add a behaviour component
--this just gets its update method called each frame
--we use it to hold a reference to the sprite created above,
--and modify its offset based on distance from the camera
e:add_component("behaviour", "parallax_thing", {
sprite = s,
cam = systems.sprites_overlay.camera,
_tv = vec2:zero(),
mw = args.world.mw,
fac = args.parallax_factor,
update = function(self, dt)
self._tv:vset(self.sprite.pos):vsubi(self.cam.pos)
self._tv.x = math.wrap(self._tv.x, -self.mw * 0.5, self.mw * 0.5)
self.sprite.offset:vset(self._tv):smuli(self.fac)
end
})
return e
end
Benefits:
- can have more than one of a single component type per entity
- can have free-floating components without need for an entity (eg a static sprite, and maybe an animation managing the sprite, or a static collider)
- systems can manage their own data for performance
- 99% of systems can do a nice dumb loop over their components to update
- those that need it can do threading or store components as ffi objects or be implemented in c++ under the hood or whatever
- all your throwaway game code can go in the behaviour system while being very easy to port to a dedicated system - animations began their lives here for example