Skip to content

Instantly share code, notes, and snippets.

@filiptibell
Last active September 13, 2025 21:46
Show Gist options
  • Select an option

  • Save filiptibell/8610b94a5b56d40fd93c032632f052be to your computer and use it in GitHub Desktop.

Select an option

Save filiptibell/8610b94a5b56d40fd93c032632f052be to your computer and use it in GitHub Desktop.
Connector to create a source for many entities & their component values using jecs + vide
--!nonstrict
--!native
--!optimize 2
local jecs = require(path.to.jecs)
type Description<T...> = jecs.Query<T...>
type ObserverArm = (<a>(
PatchedWorld,
{
query: jecs.Query<a>,
callback: ((jecs.Entity) -> ())?,
}
) -> () -> () -> jecs.Entity) & (<a, b>(
PatchedWorld,
{
query: jecs.Query<a, b>,
callback: ((jecs.Entity) -> ())?,
}
) -> () -> () -> jecs.Entity) & (<a, b, c>(
PatchedWorld,
{
query: jecs.Query<a, b, c>,
callback: ((jecs.Entity) -> ())?,
}
) -> () -> () -> jecs.Entity)
export type PatchedWorld = jecs.World & {
added: <T>(
PatchedWorld,
jecs.Id<T>,
<e>(e: jecs.Entity<e>, id: jecs.Id<T>, value: T?) -> ()
) -> () -> (),
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (),
changed: <T>(
PatchedWorld,
jecs.Id<T>,
<e>(e: jecs.Entity<e>, id: jecs.Id<T>, value: T) -> ()
) -> () -> (),
observer: ObserverArm & any,
monitor: ObserverArm & any,
}
local function observers_new(
world: PatchedWorld,
query: any,
onAdded: (<T>(jecs.Entity<T>) -> ())?,
onChanged: (<T, a>(jecs.Entity<T>, jecs.Id<a>, value: a?) -> ())?,
onRemoved: (<T>(jecs.Entity<T>) -> ())?
)
query = query:cached()
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
observers_on_create[#observers_on_create].callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
observers_on_delete[#observers_on_delete].callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local ids = {}
local function emplaced<T, a>(entity: jecs.Entity<T>, id: jecs.Id<a>, value: a?)
local record = jecs.entity_index_try_get_fast(entity_index, entity :: any) :: jecs.Record
local arch = if record then record.archetype else nil
if arch and archetypes[arch.id] then
if ids[entity] ~= true then
ids[entity] = true
-- 1. Run the onAdded callback for newly matching entities
if onAdded then
onAdded(entity)
end
-- 2. Run the onChanged callback for all query terms, for this entity
if onChanged then
for _, term in terms do
local tvalue = world:get(entity, term)
onChanged(entity, term, tvalue)
end
end
else
-- 3. This entity was already matched, call the onChanged callback
-- only for this component / tag which was just changed
if onChanged then
onChanged(entity, id, value)
end
end
end
end
local function removed(entity: jecs.Entity, id: jecs.Id<any>)
if ids[entity] ~= nil then
ids[entity] = nil
-- 4. The entity no longer matches the query, call the onRemoved callback
if onRemoved then
onRemoved(entity)
end
end
end
for _, term in terms do
world:added(term, emplaced :: any)
world:changed(term, emplaced :: any)
world:removed(term, removed :: any)
end
end
local function monitors_new(
world,
query: any,
onAdded: (<T>(jecs.Entity<T>) -> ())?,
onRemoved: (<T>(jecs.Entity<T>) -> ())?
)
query = query:cached()
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
observers_on_create[#observers_on_create].callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
observers_on_delete[#observers_on_delete].callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local ids = {}
local function emplaced<T, a>(entity: jecs.Entity<T>, id: jecs.Id<a>, value: a?)
local record = jecs.entity_index_try_get_fast(entity_index, entity :: any) :: jecs.Record
local arch = if record then record.archetype else nil
if arch and archetypes[arch.id] then
if ids[entity] ~= true then
ids[entity] = true
-- 1. Run the onAdded callback for newly matching entities
if onAdded then
onAdded(entity)
end
end
end
end
local function removed(entity: jecs.Entity, id: jecs.Id<any>)
if ids[entity] ~= nil then
ids[entity] = nil
-- 2. The entity no longer matches the query, call the onRemoved callback
if onRemoved then
onRemoved(entity)
end
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
type Signal = { [jecs.Entity]: { (...any) -> () } }
local world_mut = world :: jecs.World & { [string]: any }
local signals = {
added = {} :: Signal,
emplaced = {} :: Signal,
removed = {} :: Signal,
}
world_mut.added = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local function on_add(entity, id, value)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnAdd)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.on_add = on_add
else
world:set(component, jecs.OnAdd, on_add)
end
end
table.insert(listeners, fn)
return function()
local i = table.find(listeners, fn)
if i == nil then
return
end
local n = #listeners
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.changed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local function on_change(entity, id, value: any)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnChange)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.on_change = on_change
else
world:set(component, jecs.OnChange, on_change)
end
end
table.insert(listeners, fn)
return function()
local i = table.find(listeners, fn)
if i == nil then
return
end
local n = #listeners
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.removed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id) -> ()
)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local function on_remove(entity, id)
for _, listener in listeners :: any do
listener(entity, id)
end
end
local existing_hook = world:get(component, jecs.OnRemove)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.on_remove = on_remove
else
world:set(component, jecs.OnRemove, on_remove)
end
end
table.insert(listeners, fn)
return function()
local i = table.find(listeners, fn)
if i == nil then
return
end
local n = #listeners
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world_mut.signals = signals
world_mut.observer = observers_new
world_mut.monitor = monitors_new
return world_mut :: PatchedWorld
end
return observers_add
local vide = require(path.to.vide)
local world = nil -- path to world
type Entity = any -- your entity type (i use { __T: "Entity" })
type Component<T> = T -- your component type (i use { __T: "Component", __V: T })
type Source<T> = vide.Source<T>
type Readable<T> = () -> T
type IdMap<T = true> = { [Entity]: T }
type AnyMap = { [string]: any }
type ComponentMap = { [string]: Component<any> }
export type EcsSourceMapMulti = Readable<IdMap<Readable<AnyMap>>>
return function(components: ComponentMap, additionalTerms: { Component<any> }?): EcsSourceMapMulti
local componentsArray: { Component<any> } = {}
local componentsToKeys: { [Component<any>]: { string } } = {}
for key, component in components do
local keys = componentsToKeys[component]
if keys then
table.insert(keys, key)
else
componentsToKeys[component] = { key }
table.insert(componentsArray, component)
end
end
local function makeEntityMap(id: Entity): AnyMap
local map: AnyMap = {}
for component, keys in componentsToKeys do
local val = world:Get(id, component)
if val ~= nil then
for _, key in keys do
map[key] = val
end
end
end
return map
end
-- Construct the query and sources with initial values
local query = if additionalTerms ~= nil
then world:query(table.unpack(componentsArray)):with(table.unpack(additionalTerms))
else world:query(table.unpack(componentsArray))
local sources: IdMap<Source<AnyMap>?> = {}
local values: IdMap<AnyMap> = {}
for id in query :: any do
local map = makeEntityMap(id)
sources[id] = vide.source(map)
values[id] = map
end
local entitiesSource = vide.source(table.clone(sources))
-- Create callbacks for observer
local function onAdded(id: Entity)
if sources[id] == nil then
local map = makeEntityMap(id)
sources[id] = vide.source(map)
values[id] = map
entitiesSource(table.clone(sources))
end
end
local function onChanged(id: Entity, component: Component<any>, value: any)
local valsSource = sources[id]
assert(valsSource ~= nil, "changed callback was called before added or after removed")
local vals = values[id]
assert(vals ~= nil, "changed callback was called before added or after removed")
local keys = componentsToKeys[component :: any]
assert(keys ~= nil, "changed callback was called for unknown component / tag / relation")
for _, key in keys do
vals[key] = value
end
valsSource(table.clone(vals))
end
local function onRemoved(id: Entity)
if sources[id] ~= nil then
sources[id] = nil
entitiesSource(table.clone(sources))
end
end
-- Finally, create the observer
world:observer(query, onAdded, onChanged, onRemoved)
return entitiesSource :: any
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment