Last active
September 13, 2025 21:46
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --!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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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