diff --git a/benches/visual/remove.bench.luau b/benches/visual/remove.bench.luau index a7082a8..5af2a17 100644 --- a/benches/visual/remove.bench.luau +++ b/benches/visual/remove.bench.luau @@ -1,49 +1,49 @@ ---!optimize 2 ---!native - -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local Matter = require(ReplicatedStorage.DevPackages.Matter) -local ecr = require(ReplicatedStorage.DevPackages.ecr) -local jecs = require(ReplicatedStorage.Lib) -local pair = jecs.pair -local ecs = jecs.World.new() -local mirror = require(ReplicatedStorage.mirror) -local mcs = mirror.World.new() - -local C1 = ecs:component() -local C2 = ecs:entity() -ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) -local C3 = ecs:entity() -ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) -local C4 = ecs:entity() -ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) -local E1 = mcs:component() -local E2 = mcs:entity() -mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) -local E3 = mcs:entity() -mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) -local E4 = mcs:entity() -mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete)) - -return { - ParameterGenerator = function() - end, - - Functions = { - Mirror = function() - local m = mcs:entity() - for i = 1, 100 do - mcs:add(m, E3) - mcs:remove(m, E3) - end - end, - - Jecs = function() - local j = ecs:entity() - for i = 1, 100 do - ecs:add(j, C3) - ecs:remove(j, C3) - end - end, - }, -} +--!optimize 2 +--!native + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Matter = require(ReplicatedStorage.DevPackages.Matter) +local ecr = require(ReplicatedStorage.DevPackages.ecr) +local jecs = require(ReplicatedStorage.Lib) +local pair = jecs.pair +local ecs = jecs.World.new() +local mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() + +local C1 = ecs:component() +local C2 = ecs:entity() +ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C3 = ecs:entity() +ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C4 = ecs:entity() +ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E1 = mcs:component() +local E2 = mcs:entity() +mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E3 = mcs:entity() +mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E4 = mcs:entity() +mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete)) + +return { + ParameterGenerator = function() + end, + + Functions = { + Mirror = function() + local m = mcs:entity() + for i = 1, 100 do + mcs:add(m, E3) + mcs:remove(m, E3) + end + end, + + Jecs = function() + local j = ecs:entity() + for i = 1, 100 do + ecs:add(j, C3) + ecs:remove(j, C3) + end + end, + }, +} diff --git a/demo/src/ReplicatedStorage/collect.luau b/demo/src/ReplicatedStorage/collect.luau index 9ec3bdd..d6df981 100644 --- a/demo/src/ReplicatedStorage/collect.luau +++ b/demo/src/ReplicatedStorage/collect.luau @@ -1,28 +1,28 @@ -local function collect( - signal: { - Connect: (RBXScriptSignal, fn: (T...) -> ()) -> RBXScriptConnection - } -): () -> (T...) - local enqueued = {} - - local i = 0 - - local connection = (signal :: any):Connect(function(...) - table.insert(enqueued, { ... }) - i += 1 - end) - - return function(): any - if i == 0 then - return - end - - i -= 1 - - local args: any = table.remove(enqueued, 1) - - return unpack(args) - end, connection -end - -return collect +local function collect( + signal: { + Connect: (RBXScriptSignal, fn: (T...) -> ()) -> RBXScriptConnection + } +): () -> (T...) + local enqueued = {} + + local i = 0 + + local connection = (signal :: any):Connect(function(...) + table.insert(enqueued, { ... }) + i += 1 + end) + + return function(): any + if i == 0 then + return + end + + i -= 1 + + local args: any = table.remove(enqueued, 1) + + return unpack(args) + end, connection +end + +return collect diff --git a/demo/src/ReplicatedStorage/components.luau b/demo/src/ReplicatedStorage/components.luau index f9fb0c4..5bb3225 100644 --- a/demo/src/ReplicatedStorage/components.luau +++ b/demo/src/ReplicatedStorage/components.luau @@ -1,36 +1,36 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jecs = require(ReplicatedStorage.ecs) -local types = require("./types") - -local Networked = jecs.tag() -local NetworkedPair = jecs.tag() - -local Renderable = jecs.component() :: jecs.Id -jecs.meta(Renderable, Networked) - - -local Poison = jecs.component() :: jecs.Id -jecs.meta(Poison, Networked) - -local Health = jecs.component() :: jecs.Id -jecs.meta(Health, Networked) - -local Player = jecs.component() :: jecs.Id -jecs.meta(Player, Networked) - - -local components = { - Renderable = Renderable, - Player = Player, - Poison = Poison, - Health = Health, - - Networked = Networked, - NetworkedPair = NetworkedPair, -} - -for name, component in components do - jecs.meta(component, jecs.Name, name) -end - -return components +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local jecs = require(ReplicatedStorage.ecs) +local types = require("./types") + +local Networked = jecs.tag() +local NetworkedPair = jecs.tag() + +local Renderable = jecs.component() :: jecs.Id +jecs.meta(Renderable, Networked) + + +local Poison = jecs.component() :: jecs.Id +jecs.meta(Poison, Networked) + +local Health = jecs.component() :: jecs.Id +jecs.meta(Health, Networked) + +local Player = jecs.component() :: jecs.Id +jecs.meta(Player, Networked) + + +local components = { + Renderable = Renderable, + Player = Player, + Poison = Poison, + Health = Health, + + Networked = Networked, + NetworkedPair = NetworkedPair, +} + +for name, component in components do + jecs.meta(component, jecs.Name, name) +end + +return components diff --git a/demo/src/ReplicatedStorage/observers_add.luau b/demo/src/ReplicatedStorage/observers_add.luau index b310ab1..c959b88 100644 --- a/demo/src/ReplicatedStorage/observers_add.luau +++ b/demo/src/ReplicatedStorage/observers_add.luau @@ -1,190 +1,190 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jecs = require(ReplicatedStorage.ecs) - -type Observer = { - callback: (jecs.Entity) -> (), - query: jecs.Query, -} - -export type PatchedWorld = jecs.World & { - added: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> (), - removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (), - changed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> (), - -- deleted: (PatchedWorld, () -> ()) -> () -> (), - observer: (PatchedWorld, Observer) -> (), - monitor: (PatchedWorld, Observer) -> (), -} - -local function observers_new(world, description) - local query = description.query - local callback = description.callback - local terms = query.filter_with :: { jecs.Id } - if not terms then - local ids = query.ids - query.filter_with = ids - terms = ids - end - - local entity_index = world.entity_index :: any - local function emplaced(entity: jecs.Entity) - local r = jecs.entity_index_try_get_fast( - entity_index, entity :: any) - - if not r then - return - end - - local archetype = r.archetype - - if jecs.query_match(query, archetype) then - callback(entity) - end - end - - for _, term in terms do - world:added(term, emplaced) - world:changed(term, emplaced) - end -end - -local function monitors_new(world, description) - local query = description.query - local callback = description.callback - local terms = query.filter_with :: { jecs.Id } - if not terms then - local ids = query.ids - query.filter_with = ids - terms = ids - end - - local entity_index = world.entity_index :: any - local function emplaced(entity: jecs.Entity) - local r = jecs.entity_index_try_get_fast( - entity_index, entity :: any) - - if not r then - return - end - - local archetype = r.archetype - - if jecs.query_match(query, archetype) then - callback(entity, jecs.OnAdd) - end - end - - local function removed(entity: jecs.Entity, component: jecs.Id) - local r = jecs.entity_index_try_get_fast( - entity_index, entity :: any) - - if not r then - return - end - - local archetype = r.archetype - - if jecs.query_match(query, archetype) then - callback(entity, jecs.OnRemove) - 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 - local signals = { - added = {}, - emplaced = {}, - removed = {}, - deleted = {} - } - - world = world :: jecs.World & {[string]: any} - - world.added = function(_, component, fn) - local listeners = signals.added[component] - if not listeners then - listeners = {} - signals.added[component] = listeners - - local idr = jecs.id_record_ensure(world :: any, component :: any) - local rw = jecs.pair(component, jecs.Wildcard) - local idr_r = jecs.id_record_ensure(world :: any, rw :: any) - local function on_add(entity: number, id: number, value: any) - for _, listener in listeners do - listener(entity, id, value) - end - end - world:set(component, jecs.OnAdd, on_add) - idr.hooks.on_add = on_add :: any - idr_r.hooks.on_add = on_add :: any - end - table.insert(listeners, fn) - end - - world.changed = function(_, component, fn) - local listeners = signals.emplaced[component] - if not listeners then - listeners = {} - signals.emplaced[component] = listeners - local idr = jecs.id_record_ensure(world :: any, component :: any) - local rw = jecs.pair(component, jecs.Wildcard) - local idr_r = jecs.id_record_ensure(world :: any, rw :: any) - local function on_change(entity: number, id: number, value: any) - for _, listener in listeners do - listener(entity, id, value) - end - end - world:set(component, jecs.OnChange, on_change) - idr.hooks.on_change = on_change :: any - idr_r.hooks.on_change = on_change :: any - end - table.insert(listeners, fn) - end - - world.removed = function(_, component, fn) - local listeners = signals.removed[component] - if not listeners then - listeners = {} - signals.removed[component] = listeners - local idr = jecs.id_record_ensure(world :: any, component :: any) - local rw = jecs.pair(component, jecs.Wildcard) - local idr_r = jecs.id_record_ensure(world :: any, rw :: any) - local function on_remove(entity: number, id: number, value: any) - for _, listener in listeners do - listener(entity, id, value) - end - end - world:set(component, jecs.OnRemove, on_remove) - idr.hooks.on_remove = on_remove :: any - idr_r.hooks.on_remove = on_remove :: any - end - table.insert(listeners, fn) - end - - world.signals = signals - - world.observer = observers_new - - world.monitor = monitors_new - - -- local world_delete = world.delete - - -- world.deleted = function(_, fn) - -- local listeners = signals.deleted - -- table.insert(listeners, fn) - -- end - -- world.delete = function(world, entity) - -- world_delete(world, entity) - -- for _, fn in signals.deleted do - -- fn(entity) - -- end - -- end - - return world :: PatchedWorld -end - -return observers_add +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local jecs = require(ReplicatedStorage.ecs) + +type Observer = { + callback: (jecs.Entity) -> (), + query: jecs.Query, +} + +export type PatchedWorld = jecs.World & { + added: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> (), + removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (), + changed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> (), + -- deleted: (PatchedWorld, () -> ()) -> () -> (), + observer: (PatchedWorld, Observer) -> (), + monitor: (PatchedWorld, Observer) -> (), +} + +local function observers_new(world, description) + local query = description.query + local callback = description.callback + local terms = query.filter_with :: { jecs.Id } + if not terms then + local ids = query.ids + query.filter_with = ids + terms = ids + end + + local entity_index = world.entity_index :: any + local function emplaced(entity: jecs.Entity) + local r = jecs.entity_index_try_get_fast( + entity_index, entity :: any) + + if not r then + return + end + + local archetype = r.archetype + + if jecs.query_match(query, archetype) then + callback(entity) + end + end + + for _, term in terms do + world:added(term, emplaced) + world:changed(term, emplaced) + end +end + +local function monitors_new(world, description) + local query = description.query + local callback = description.callback + local terms = query.filter_with :: { jecs.Id } + if not terms then + local ids = query.ids + query.filter_with = ids + terms = ids + end + + local entity_index = world.entity_index :: any + local function emplaced(entity: jecs.Entity) + local r = jecs.entity_index_try_get_fast( + entity_index, entity :: any) + + if not r then + return + end + + local archetype = r.archetype + + if jecs.query_match(query, archetype) then + callback(entity, jecs.OnAdd) + end + end + + local function removed(entity: jecs.Entity, component: jecs.Id) + local r = jecs.entity_index_try_get_fast( + entity_index, entity :: any) + + if not r then + return + end + + local archetype = r.archetype + + if jecs.query_match(query, archetype) then + callback(entity, jecs.OnRemove) + 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 + local signals = { + added = {}, + emplaced = {}, + removed = {}, + deleted = {} + } + + world = world :: jecs.World & {[string]: any} + + world.added = function(_, component, fn) + local listeners = signals.added[component] + if not listeners then + listeners = {} + signals.added[component] = listeners + + local idr = jecs.id_record_ensure(world :: any, component :: any) + local rw = jecs.pair(component, jecs.Wildcard) + local idr_r = jecs.id_record_ensure(world :: any, rw :: any) + local function on_add(entity: number, id: number, value: any) + for _, listener in listeners do + listener(entity, id, value) + end + end + world:set(component, jecs.OnAdd, on_add) + idr.hooks.on_add = on_add :: any + idr_r.hooks.on_add = on_add :: any + end + table.insert(listeners, fn) + end + + world.changed = function(_, component, fn) + local listeners = signals.emplaced[component] + if not listeners then + listeners = {} + signals.emplaced[component] = listeners + local idr = jecs.id_record_ensure(world :: any, component :: any) + local rw = jecs.pair(component, jecs.Wildcard) + local idr_r = jecs.id_record_ensure(world :: any, rw :: any) + local function on_change(entity: number, id: number, value: any) + for _, listener in listeners do + listener(entity, id, value) + end + end + world:set(component, jecs.OnChange, on_change) + idr.hooks.on_change = on_change :: any + idr_r.hooks.on_change = on_change :: any + end + table.insert(listeners, fn) + end + + world.removed = function(_, component, fn) + local listeners = signals.removed[component] + if not listeners then + listeners = {} + signals.removed[component] = listeners + local idr = jecs.id_record_ensure(world :: any, component :: any) + local rw = jecs.pair(component, jecs.Wildcard) + local idr_r = jecs.id_record_ensure(world :: any, rw :: any) + local function on_remove(entity: number, id: number, value: any) + for _, listener in listeners do + listener(entity, id, value) + end + end + world:set(component, jecs.OnRemove, on_remove) + idr.hooks.on_remove = on_remove :: any + idr_r.hooks.on_remove = on_remove :: any + end + table.insert(listeners, fn) + end + + world.signals = signals + + world.observer = observers_new + + world.monitor = monitors_new + + -- local world_delete = world.delete + + -- world.deleted = function(_, fn) + -- local listeners = signals.deleted + -- table.insert(listeners, fn) + -- end + -- world.delete = function(world, entity) + -- world_delete(world, entity) + -- for _, fn in signals.deleted do + -- fn(entity) + -- end + -- end + + return world :: PatchedWorld +end + +return observers_add diff --git a/demo/src/ReplicatedStorage/remotes.luau b/demo/src/ReplicatedStorage/remotes.luau index 64f87e6..4838770 100644 --- a/demo/src/ReplicatedStorage/remotes.luau +++ b/demo/src/ReplicatedStorage/remotes.luau @@ -1,50 +1,50 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local types = require("../ReplicatedStorage/types") - -type Signal = { - Connect: (Signal, fn: (T...) -> ()) -> RBXScriptConnection -} -type Remote = { - FireClient: (Remote, T...) -> (), - FireAllClients: (Remote, T...) -> (), - FireServer: (Remote) -> (), - OnServerEvent: { - Connect: (any, fn: (Player, T...) -> () ) -> () - }, - OnClientEvent: { - Connect: (any, fn: (T...) -> () ) -> () - } - -} - -local function stream_ensure(name): Remote - local remote = ReplicatedStorage:FindFirstChild(name) - if not remote then - remote = Instance.new("RemoteEvent") - remote.Name = name - remote.Parent = ReplicatedStorage - end - return remote :: any -end - -local function datagram_ensure(name): Remote - local remote = ReplicatedStorage:FindFirstChild(name) - if not remote then - remote = Instance.new("UnreliableRemoteEvent") - remote.Name = name - remote.Parent = ReplicatedStorage - end - return remote :: any -end - -return { - input = datagram_ensure("input") :: Remote, - replication = stream_ensure("replication") :: Remote<{ - [string]: { - set: { types.Entity }?, - values: { any }?, - removed: { types.Entity }? - } - }>, - -} +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local types = require("../ReplicatedStorage/types") + +type Signal = { + Connect: (Signal, fn: (T...) -> ()) -> RBXScriptConnection +} +type Remote = { + FireClient: (Remote, T...) -> (), + FireAllClients: (Remote, T...) -> (), + FireServer: (Remote) -> (), + OnServerEvent: { + Connect: (any, fn: (Player, T...) -> () ) -> () + }, + OnClientEvent: { + Connect: (any, fn: (T...) -> () ) -> () + } + +} + +local function stream_ensure(name): Remote + local remote = ReplicatedStorage:FindFirstChild(name) + if not remote then + remote = Instance.new("RemoteEvent") + remote.Name = name + remote.Parent = ReplicatedStorage + end + return remote :: any +end + +local function datagram_ensure(name): Remote + local remote = ReplicatedStorage:FindFirstChild(name) + if not remote then + remote = Instance.new("UnreliableRemoteEvent") + remote.Name = name + remote.Parent = ReplicatedStorage + end + return remote :: any +end + +return { + input = datagram_ensure("input") :: Remote, + replication = stream_ensure("replication") :: Remote<{ + [string]: { + set: { types.Entity }?, + values: { any }?, + removed: { types.Entity }? + } + }>, + +} diff --git a/demo/src/ReplicatedStorage/schedule.luau b/demo/src/ReplicatedStorage/schedule.luau index 14652f3..7030bbe 100644 --- a/demo/src/ReplicatedStorage/schedule.luau +++ b/demo/src/ReplicatedStorage/schedule.luau @@ -1,136 +1,136 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jabby = require(ReplicatedStorage.Packages.jabby) -local jecs = require(ReplicatedStorage.ecs) - -jabby.set_check_function(function() return true end) - -local scheduler = jabby.scheduler.create("jabby scheduler") - -jabby.register({ - applet = jabby.applets.scheduler, - name = "Scheduler", - configuration = { - scheduler = scheduler, - }, -}) - -local ContextActionService = game:GetService("ContextActionService") - -local function create_widget(_, state: Enum.UserInputState) - local client = jabby.obtain_client() - if state ~= Enum.UserInputState.Begin then return end - client.spawn_app(client.apps.home, nil) -end - -local RunService = game:GetService("RunService") - -local System = jecs.component() :: jecs.Id<{ - fn: () -> (), - name: string, -}> -local DependsOn = jecs.component() -local Phase = jecs.tag() -local Event = jecs.component() :: jecs.Id - -local pair = jecs.pair - -local types = require(ReplicatedStorage.types) - -local function ECS_PHASE(world, after: types.Entity) - local phase = world:entity() - world:add(phase, Phase) - if after then - local dependency = pair(DependsOn, after) - world:add(phase, dependency) - end - - return phase -end - -local Heartbeat = jecs.tag() -jecs.meta(Heartbeat, Phase) -jecs.meta(Heartbeat, Event, RunService.Heartbeat) - -local PreSimulation = jecs.tag() -jecs.meta(PreSimulation, Phase) -jecs.meta(PreSimulation, Event, RunService.PreSimulation) - -local PreAnimation = jecs.tag() -jecs.meta(PreAnimation, Phase) -jecs.meta(PreAnimation, Event, RunService.PreAnimation) - -local PreRender = jecs.tag() -jecs.meta(PreRender, Phase) -jecs.meta(PreRender, Event, RunService.PreRender) - -local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?) - local system = world:entity() - local p = phase or Heartbeat - local fn = require(mod) :: (...any) -> () - world:set(system, System, { - fn = fn(world, 0) or fn, - name = mod.Name, - }) - - local depends_on = DependsOn :: jecs.Entity - world:add(system, pair(depends_on, p)) -end -local function find_systems_w_phase(world: types.World, systems, phase: types.Entity) - local phase_name = world:get(phase, jecs.Name) :: string - for _, s in world:query(System):with(pair(DependsOn, phase)) do - table.insert(systems, { - id = scheduler:register_system({ - phase = phase_name, - name = s.name, - }), - fn = s.fn - }) - end - for after in world:query(Phase, pair(DependsOn, phase)) do - find_systems_w_phase(world, systems, after) - end - return systems -end - -local function ECS_RUN(world: types.World) - - jabby.register({ - applet = jabby.applets.world, - name = "MyWorld", - configuration = { - world = world, - }, - }) - - if RunService:IsClient() then - ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4) - end - - for phase, event in world:query(Event, Phase) do - local systems = find_systems_w_phase(world, {}, phase) - event:Connect(function(...) - for _, system in systems do - scheduler:run(system.id, system.fn, world, ...) - end - end) - end - -end - -return { - PHASE = ECS_PHASE, - SYSTEM = ECS_SYSTEM, - RUN = ECS_RUN, - phases = { - Heartbeat = Heartbeat, - PreSimulation = PreSimulation, - PreAnimation = PreAnimation, - PreRender = PreRender - }, - components = { - System = System, - DependsOn = DependsOn, - Phase = Phase, - Event = Event, - } -} +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local jabby = require(ReplicatedStorage.Packages.jabby) +local jecs = require(ReplicatedStorage.ecs) + +jabby.set_check_function(function() return true end) + +local scheduler = jabby.scheduler.create("jabby scheduler") + +jabby.register({ + applet = jabby.applets.scheduler, + name = "Scheduler", + configuration = { + scheduler = scheduler, + }, +}) + +local ContextActionService = game:GetService("ContextActionService") + +local function create_widget(_, state: Enum.UserInputState) + local client = jabby.obtain_client() + if state ~= Enum.UserInputState.Begin then return end + client.spawn_app(client.apps.home, nil) +end + +local RunService = game:GetService("RunService") + +local System = jecs.component() :: jecs.Id<{ + fn: () -> (), + name: string, +}> +local DependsOn = jecs.component() +local Phase = jecs.tag() +local Event = jecs.component() :: jecs.Id + +local pair = jecs.pair + +local types = require(ReplicatedStorage.types) + +local function ECS_PHASE(world, after: types.Entity) + local phase = world:entity() + world:add(phase, Phase) + if after then + local dependency = pair(DependsOn, after) + world:add(phase, dependency) + end + + return phase +end + +local Heartbeat = jecs.tag() +jecs.meta(Heartbeat, Phase) +jecs.meta(Heartbeat, Event, RunService.Heartbeat) + +local PreSimulation = jecs.tag() +jecs.meta(PreSimulation, Phase) +jecs.meta(PreSimulation, Event, RunService.PreSimulation) + +local PreAnimation = jecs.tag() +jecs.meta(PreAnimation, Phase) +jecs.meta(PreAnimation, Event, RunService.PreAnimation) + +local PreRender = jecs.tag() +jecs.meta(PreRender, Phase) +jecs.meta(PreRender, Event, RunService.PreRender) + +local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?) + local system = world:entity() + local p = phase or Heartbeat + local fn = require(mod) :: (...any) -> () + world:set(system, System, { + fn = fn(world, 0) or fn, + name = mod.Name, + }) + + local depends_on = DependsOn :: jecs.Entity + world:add(system, pair(depends_on, p)) +end +local function find_systems_w_phase(world: types.World, systems, phase: types.Entity) + local phase_name = world:get(phase, jecs.Name) :: string + for _, s in world:query(System):with(pair(DependsOn, phase)) do + table.insert(systems, { + id = scheduler:register_system({ + phase = phase_name, + name = s.name, + }), + fn = s.fn + }) + end + for after in world:query(Phase, pair(DependsOn, phase)) do + find_systems_w_phase(world, systems, after) + end + return systems +end + +local function ECS_RUN(world: types.World) + + jabby.register({ + applet = jabby.applets.world, + name = "MyWorld", + configuration = { + world = world, + }, + }) + + if RunService:IsClient() then + ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4) + end + + for phase, event in world:query(Event, Phase) do + local systems = find_systems_w_phase(world, {}, phase) + event:Connect(function(...) + for _, system in systems do + scheduler:run(system.id, system.fn, world, ...) + end + end) + end + +end + +return { + PHASE = ECS_PHASE, + SYSTEM = ECS_SYSTEM, + RUN = ECS_RUN, + phases = { + Heartbeat = Heartbeat, + PreSimulation = PreSimulation, + PreAnimation = PreAnimation, + PreRender = PreRender + }, + components = { + System = System, + DependsOn = DependsOn, + Phase = Phase, + Event = Event, + } +} diff --git a/demo/src/ReplicatedStorage/systems/receive_replication.luau b/demo/src/ReplicatedStorage/systems/receive_replication.luau index c2b090d..b4be32f 100644 --- a/demo/src/ReplicatedStorage/systems/receive_replication.luau +++ b/demo/src/ReplicatedStorage/systems/receive_replication.luau @@ -1,86 +1,86 @@ -local types = require("../types") -local jecs = require(game:GetService("ReplicatedStorage").ecs) -local remotes = require("../remotes") -local collect = require("../collect") -local client_ids = {} - - -local function ecs_map_get(world, id) - local deserialised_id = client_ids[id] - - if not deserialised_id then - if world:has(id, jecs.Name) then - deserialised_id = world:entity(id) - else - deserialised_id = world:entity() - end - - client_ids[id] = deserialised_id - end - - -- local deserialised_id = client_ids[id] - -- if not deserialised_id then - -- if world:has(id, jecs.Name) then - -- deserialised_id = world:entity(id) - -- else - -- if world:exists(id) then - -- deserialised_id = world:entity() - -- else - -- deserialised_id = world:entity(id) - -- end - -- end - -- client_ids[id] = deserialised_id - -- end - - return deserialised_id -end - -local function ecs_make_alive_id(world, id) - local rel = jecs.ECS_PAIR_FIRST(id) - local tgt = jecs.ECS_PAIR_SECOND(id) - - rel = ecs_map_get(world, rel) - tgt = ecs_map_get(world, tgt) - - return jecs.pair(rel, tgt) -end - -local snapshots = collect(remotes.replication.OnClientEvent) - -return function(world: types.World) - for snapshot in snapshots do - for id, map in snapshot do - id = tonumber(id) - if jecs.IS_PAIR(id) then - id = ecs_make_alive_id(world, id) - end - - local set = map.set - if set then - if jecs.is_tag(world, id) then - for _, entity in set do - entity = ecs_map_get(world, entity) - world:add(entity, id) - end - else - local values = map.values - for i, entity in set do - entity = ecs_map_get(world, entity) - world:set(entity, id, values[i]) - end - end - end - - local removed = map.removed - - if removed then - for i, e in removed do - if not world:contains(e) then - continue - end - world:remove(e, id) - end - end - end - end -end +local types = require("../types") +local jecs = require(game:GetService("ReplicatedStorage").ecs) +local remotes = require("../remotes") +local collect = require("../collect") +local client_ids = {} + + +local function ecs_map_get(world, id) + local deserialised_id = client_ids[id] + + if not deserialised_id then + if world:has(id, jecs.Name) then + deserialised_id = world:entity(id) + else + deserialised_id = world:entity() + end + + client_ids[id] = deserialised_id + end + + -- local deserialised_id = client_ids[id] + -- if not deserialised_id then + -- if world:has(id, jecs.Name) then + -- deserialised_id = world:entity(id) + -- else + -- if world:exists(id) then + -- deserialised_id = world:entity() + -- else + -- deserialised_id = world:entity(id) + -- end + -- end + -- client_ids[id] = deserialised_id + -- end + + return deserialised_id +end + +local function ecs_make_alive_id(world, id) + local rel = jecs.ECS_PAIR_FIRST(id) + local tgt = jecs.ECS_PAIR_SECOND(id) + + rel = ecs_map_get(world, rel) + tgt = ecs_map_get(world, tgt) + + return jecs.pair(rel, tgt) +end + +local snapshots = collect(remotes.replication.OnClientEvent) + +return function(world: types.World) + for snapshot in snapshots do + for id, map in snapshot do + id = tonumber(id) + if jecs.IS_PAIR(id) then + id = ecs_make_alive_id(world, id) + end + + local set = map.set + if set then + if jecs.is_tag(world, id) then + for _, entity in set do + entity = ecs_map_get(world, entity) + world:add(entity, id) + end + else + local values = map.values + for i, entity in set do + entity = ecs_map_get(world, entity) + world:set(entity, id, values[i]) + end + end + end + + local removed = map.removed + + if removed then + for i, e in removed do + if not world:contains(e) then + continue + end + world:remove(e, id) + end + end + end + end +end diff --git a/demo/src/ReplicatedStorage/types.luau b/demo/src/ReplicatedStorage/types.luau index ae585e7..6971254 100644 --- a/demo/src/ReplicatedStorage/types.luau +++ b/demo/src/ReplicatedStorage/types.luau @@ -1,15 +1,15 @@ -local jecs = require(game:GetService("ReplicatedStorage").ecs) -local observers_add = require("../ReplicatedStorage/observers_add") - -export type World = typeof(observers_add(jecs.world())) -export type Entity = jecs.Entity -export type Id = jecs.Id -export type Snapshot = { - [string]: { - set: { jecs.Entity }?, - values: { any }?, - removed: { jecs.Entity }? - } -} - -return {} +local jecs = require(game:GetService("ReplicatedStorage").ecs) +local observers_add = require("../ReplicatedStorage/observers_add") + +export type World = typeof(observers_add(jecs.world())) +export type Entity = jecs.Entity +export type Id = jecs.Id +export type Snapshot = { + [string]: { + set: { jecs.Entity }?, + values: { any }?, + removed: { jecs.Entity }? + } +} + +return {} diff --git a/demo/src/ServerScriptService/systems/life_is_painful.luau b/demo/src/ServerScriptService/systems/life_is_painful.luau index d68dd9e..333e64b 100644 --- a/demo/src/ServerScriptService/systems/life_is_painful.luau +++ b/demo/src/ServerScriptService/systems/life_is_painful.luau @@ -1,12 +1,12 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ct = require(ReplicatedStorage.components) -local types = require(ReplicatedStorage.types) - -return function(world: types.World, dt: number) - for e in world:query(ct.Player):without(ct.Health) do - world:set(e, ct.Health, 100) - end - for e in world:query(ct.Player, ct.Health):without(ct.Poison) do - world:set(e, ct.Poison, 10) - end -end +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ct = require(ReplicatedStorage.components) +local types = require(ReplicatedStorage.types) + +return function(world: types.World, dt: number) + for e in world:query(ct.Player):without(ct.Health) do + world:set(e, ct.Health, 100) + end + for e in world:query(ct.Player, ct.Health):without(ct.Poison) do + world:set(e, ct.Poison, 10) + end +end diff --git a/demo/src/ServerScriptService/systems/players_added.luau b/demo/src/ServerScriptService/systems/players_added.luau index a611e8b..46b1ab1 100644 --- a/demo/src/ServerScriptService/systems/players_added.luau +++ b/demo/src/ServerScriptService/systems/players_added.luau @@ -1,20 +1,20 @@ -local collect = require("../../ReplicatedStorage/collect") -local types = require("../../ReplicatedStorage/types") -local ct = require("../../ReplicatedStorage/components") -local Players = game:GetService("Players") - -local player_added = collect(Players.PlayerAdded) -return function(world: types.World, dt: number) - for player in player_added do - local entity = world:entity() - world:set(entity, ct.Player, player) - end - - for entity, player in world:query(ct.Player):without(ct.Renderable) do - local character = player.Character - if character then - if not character.Parent then - world:set(entity, ct.Renderable, character) - end - end -end +local collect = require("../../ReplicatedStorage/collect") +local types = require("../../ReplicatedStorage/types") +local ct = require("../../ReplicatedStorage/components") +local Players = game:GetService("Players") + +local player_added = collect(Players.PlayerAdded) +return function(world: types.World, dt: number) + for player in player_added do + local entity = world:entity() + world:set(entity, ct.Player, player) + end + + for entity, player in world:query(ct.Player):without(ct.Renderable) do + local character = player.Character + if character then + if not character.Parent then + world:set(entity, ct.Renderable, character) + end + end +end diff --git a/demo/src/ServerScriptService/systems/poison_hurts.luau b/demo/src/ServerScriptService/systems/poison_hurts.luau index f5ae04c..a7e1f3e 100644 --- a/demo/src/ServerScriptService/systems/poison_hurts.luau +++ b/demo/src/ServerScriptService/systems/poison_hurts.luau @@ -1,12 +1,12 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local ct = require(ReplicatedStorage.components) -return function(world, dt) - for e, poison, health in world:query(ct.Poison, ct.Health) do - local health_after_tick = health - poison * dt * 0.05 - if health_after_tick < 0 then - world:remove(e, ct.Health) - continue - end - world:set(e, ct.Health, health_after_tick) - end -end +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ct = require(ReplicatedStorage.components) +return function(world, dt) + for e, poison, health in world:query(ct.Poison, ct.Health) do + local health_after_tick = health - poison * dt * 0.05 + if health_after_tick < 0 then + world:remove(e, ct.Health) + continue + end + world:set(e, ct.Health, health_after_tick) + end +end diff --git a/demo/src/ServerScriptService/systems/replication.luau b/demo/src/ServerScriptService/systems/replication.luau index d18b6de..5e5004b 100644 --- a/demo/src/ServerScriptService/systems/replication.luau +++ b/demo/src/ServerScriptService/systems/replication.luau @@ -1,190 +1,190 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local types = require("../../ReplicatedStorage/types") -local ct = require("../../ReplicatedStorage/components") -local jecs = require(ReplicatedStorage.ecs) -local remotes = require("../../ReplicatedStorage/remotes") -local components = ct :: {[string]: jecs.Entity } - -return function(world: ty.World) - - --- integration test - - -- for _ = 1, 10 do - -- local e = world:entity() - -- world:set(e, ct.TestA, true) - -- end - - local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }} - local networked_components = {} - local networked_pairs = {} - - for component in world:each(ct.Networked) do - local name = world:get(component, jecs.Name) :: string - if components[name] == nil then - continue - end - - storages[component] = {} - - table.insert(networked_components, component) - end - - for relation in world:each(ct.NetworkedPair) do - local name = world:get(relation, jecs.Name) :: string - if not components[name] then - continue - end - table.insert(networked_pairs, relation) - end - - for _, component in networked_components do - local name = world:get(component, jecs.Name) :: string - if not components[name] then - error(`Networked Component (%id{component}%name{name})`) - end - local is_tag = jecs.is_tag(world, component) - local storage = storages[component] - if is_tag then - world:added(component, function(entity) - storage[entity] = true - end) - else - world:added(component, function(entity, _, value) - storage[entity] = value - end) - world:changed(component, function(entity, _, value) - storage[entity] = value - end) - end - - world:removed(component, function(entity) - storage[entity] = "jecs.Remove" - end) - end - - for _, relation in networked_pairs do - world:added(relation, function(entity, id, value) - local is_tag = jecs.is_tag(world, id) - local storage = storages[id] - if not storage then - storage = {} - storages[id] = storage - end - if is_tag then - storage[entity] = true - else - storage[entity] = value - end - end) - - world:changed(relation, function(entity, id, value) - local is_tag = jecs.is_tag(world, id) - if is_tag then - return - end - - local storage = storages[id] - if not storage then - storage = {} - storages[id] = storage - end - - storage[entity] = value - end) - - world:removed(relation, function(entity, id) - local storage = storages[id] - if not storage then - storage = {} - storages[id] = storage - end - - storage[entity] = "jecs.Remove" - end) - end - - local players_added = collect(Players.PlayerAdded) - - return function() - local snapshot_lazy: ty.Snapshot - local set_ids_lazy: { jecs.Entity } - - for player in players_added do - if not snapshot_lazy then - snapshot_lazy, set_ids_lazy = {}, {} - - for component, storage in storages do - local set_values = {} - local set_n = 0 - - local q = world:query(component) - local is_tag = jecs.is_tag(world, component) - for _, archetype in q:archetypes() do - local entities = archetype.entities - local entities_len = #entities - table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy) - if is_tag then - set_values = table.create(entities_len, true) - else - local column = archetype.columns[archetype.records[component]] - table.move(column, 1, entities_len, set_n + 1, set_values) - end - - set_n += entities_len - end - - local set = table.move(set_ids_lazy, 1, set_n, 1, {}) - - snapshot_lazy[tostring(component)] = { - set = if set_n > 0 then set else nil, - values = if set_n > 0 then set_values else nil, - } - end - end - - remotes.replication:FireClient(player, snapshot_lazy) - end - - local snapshot = {} :: ty.Snapshot - - local set_ids = {} - local removed_ids = {} - - for component, storage in storages do - local set_values = {} :: { any } - local set_n = 0 - local removed_n = 0 - for e, v in storage do - if v ~= "jecs.Remove" then - set_n += 1 - set_ids[set_n] = e - set_values[set_n] = v or true - elseif not world:contains(e) then - removed_n += 1 - removed_ids[removed_n] = e - end - end - - table.clear(storage) - - local dirty = false - - if set_n > 0 or removed_n > 0 then - dirty = true - end - - if dirty then - local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity } - local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity } - snapshot[tostring(component)] = { - set = if set_n > 0 then set else nil, - values = if set_n > 0 then set_values else nil, - removed = if removed_n > 0 then removed else nil - } - end - end - if next(snapshot) ~= nil then - remotes.replication:FireAllClients(snapshot) - end - end -end +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local types = require("../../ReplicatedStorage/types") +local ct = require("../../ReplicatedStorage/components") +local jecs = require(ReplicatedStorage.ecs) +local remotes = require("../../ReplicatedStorage/remotes") +local components = ct :: {[string]: jecs.Entity } + +return function(world: ty.World) + + --- integration test + + -- for _ = 1, 10 do + -- local e = world:entity() + -- world:set(e, ct.TestA, true) + -- end + + local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }} + local networked_components = {} + local networked_pairs = {} + + for component in world:each(ct.Networked) do + local name = world:get(component, jecs.Name) :: string + if components[name] == nil then + continue + end + + storages[component] = {} + + table.insert(networked_components, component) + end + + for relation in world:each(ct.NetworkedPair) do + local name = world:get(relation, jecs.Name) :: string + if not components[name] then + continue + end + table.insert(networked_pairs, relation) + end + + for _, component in networked_components do + local name = world:get(component, jecs.Name) :: string + if not components[name] then + error(`Networked Component (%id{component}%name{name})`) + end + local is_tag = jecs.is_tag(world, component) + local storage = storages[component] + if is_tag then + world:added(component, function(entity) + storage[entity] = true + end) + else + world:added(component, function(entity, _, value) + storage[entity] = value + end) + world:changed(component, function(entity, _, value) + storage[entity] = value + end) + end + + world:removed(component, function(entity) + storage[entity] = "jecs.Remove" + end) + end + + for _, relation in networked_pairs do + world:added(relation, function(entity, id, value) + local is_tag = jecs.is_tag(world, id) + local storage = storages[id] + if not storage then + storage = {} + storages[id] = storage + end + if is_tag then + storage[entity] = true + else + storage[entity] = value + end + end) + + world:changed(relation, function(entity, id, value) + local is_tag = jecs.is_tag(world, id) + if is_tag then + return + end + + local storage = storages[id] + if not storage then + storage = {} + storages[id] = storage + end + + storage[entity] = value + end) + + world:removed(relation, function(entity, id) + local storage = storages[id] + if not storage then + storage = {} + storages[id] = storage + end + + storage[entity] = "jecs.Remove" + end) + end + + local players_added = collect(Players.PlayerAdded) + + return function() + local snapshot_lazy: ty.Snapshot + local set_ids_lazy: { jecs.Entity } + + for player in players_added do + if not snapshot_lazy then + snapshot_lazy, set_ids_lazy = {}, {} + + for component, storage in storages do + local set_values = {} + local set_n = 0 + + local q = world:query(component) + local is_tag = jecs.is_tag(world, component) + for _, archetype in q:archetypes() do + local entities = archetype.entities + local entities_len = #entities + table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy) + if is_tag then + set_values = table.create(entities_len, true) + else + local column = archetype.columns[archetype.records[component]] + table.move(column, 1, entities_len, set_n + 1, set_values) + end + + set_n += entities_len + end + + local set = table.move(set_ids_lazy, 1, set_n, 1, {}) + + snapshot_lazy[tostring(component)] = { + set = if set_n > 0 then set else nil, + values = if set_n > 0 then set_values else nil, + } + end + end + + remotes.replication:FireClient(player, snapshot_lazy) + end + + local snapshot = {} :: ty.Snapshot + + local set_ids = {} + local removed_ids = {} + + for component, storage in storages do + local set_values = {} :: { any } + local set_n = 0 + local removed_n = 0 + for e, v in storage do + if v ~= "jecs.Remove" then + set_n += 1 + set_ids[set_n] = e + set_values[set_n] = v or true + elseif not world:contains(e) then + removed_n += 1 + removed_ids[removed_n] = e + end + end + + table.clear(storage) + + local dirty = false + + if set_n > 0 or removed_n > 0 then + dirty = true + end + + if dirty then + local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity } + local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity } + snapshot[tostring(component)] = { + set = if set_n > 0 then set else nil, + values = if set_n > 0 then set_values else nil, + removed = if removed_n > 0 then removed else nil + } + end + end + if next(snapshot) ~= nil then + remotes.replication:FireAllClients(snapshot) + end + end +end diff --git a/test/lol.luau b/test/lol.luau index 0be2c57..0749543 100644 --- a/test/lol.luau +++ b/test/lol.luau @@ -1,158 +1,158 @@ -local c = { - white_underline = function(s: any) - return `\27[1;4m{s}\27[0m` - end, - - white = function(s: any) - return `\27[37;1m{s}\27[0m` - end, - - green = function(s: any) - return `\27[32;1m{s}\27[0m` - end, - - red = function(s: any) - return `\27[31;1m{s}\27[0m` - end, - - yellow = function(s: any) - return `\27[33;1m{s}\27[0m` - end, - - red_highlight = function(s: any) - return `\27[41;1;30m{s}\27[0m` - end, - - green_highlight = function(s: any) - return `\27[42;1;30m{s}\27[0m` - end, - - gray = function(s: any) - return `\27[30;1m{s}\27[0m` - end, -} - - -local ECS_PAIR_FLAG = 0x8 -local ECS_ID_FLAGS_MASK = 0x10 -local ECS_ENTITY_MASK = bit32.lshift(1, 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) - -type i53 = number -type i24 = number - -local function ECS_ENTITY_T_LO(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e -end - -local function ECS_GENERATION(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 -end - -local ECS_ID = ECS_ENTITY_T_LO - -local function ECS_COMBINE(source: number, target: number): i53 - return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) -end - -local function ECS_GENERATION_INC(e: i53) - if e > ECS_ENTITY_MASK then - local flags = e // ECS_ID_FLAGS_MASK - local id = flags // ECS_ENTITY_MASK - local generation = flags % ECS_GENERATION_MASK - - local next_gen = generation + 1 - if next_gen > ECS_GENERATION_MASK then - return id - end - - return ECS_COMBINE(id, next_gen) + flags - end - return ECS_COMBINE(e, 1) -end - -local function bl() - print("") -end - -local function pe(e) - local gen = ECS_GENERATION(e) - return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`) -end - -local function dprint(tbl: { [number]: number }) - bl() - print("--------") - for i, e in tbl do - print("| "..pe(e).." |") - print("--------") - end - bl() -end - -local max_id = 0 -local alive_count = 0 -local dense = {} -local sparse = {} -local function alloc() - if alive_count ~= #dense then - alive_count += 1 - print("*recycled", pe(dense[alive_count])) - return dense[alive_count] - end - max_id += 1 - local id = max_id - alive_count += 1 - dense[alive_count] = id - sparse[id] = { - dense = alive_count - } - print("*allocated", pe(id)) - return id -end - -local function remove(entity) - local id = ECS_ID(entity) - local r = sparse[id] - local index_of_deleted_entity = r.dense - local last_entity_alive_at_index = alive_count -- last entity alive - alive_count -= 1 - local last_alive_entity = dense[last_entity_alive_at_index] - local r_swap = sparse[ECS_ID(last_alive_entity)] - r_swap.dense = r.dense - r.dense = last_entity_alive_at_index - dense[index_of_deleted_entity] = last_alive_entity - dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) - print("*dellocated", pe(id)) -end - -local function alive(e) - local r = sparse[ECS_ID(e)] - - return dense[r.dense] == e -end - -local function pa(e) - print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`) -end - -local tprint = require("@testkit").print -local e1v0 = alloc() -local e2v0 = alloc() -local e3v0 = alloc() -local e4v0 = alloc() -local e5v0 = alloc() -pa(e1v0) -pa(e4v0) -remove(e5v0) -pa(e5v0) - -local e5v1 = alloc() -pa(e5v0) -pa(e5v1) -pa(e2v0) -print(ECS_ID(e2v0)) - -dprint(dense) -remove(e2v0) -dprint(dense) +local c = { + white_underline = function(s: any) + return `\27[1;4m{s}\27[0m` + end, + + white = function(s: any) + return `\27[37;1m{s}\27[0m` + end, + + green = function(s: any) + return `\27[32;1m{s}\27[0m` + end, + + red = function(s: any) + return `\27[31;1m{s}\27[0m` + end, + + yellow = function(s: any) + return `\27[33;1m{s}\27[0m` + end, + + red_highlight = function(s: any) + return `\27[41;1;30m{s}\27[0m` + end, + + green_highlight = function(s: any) + return `\27[42;1;30m{s}\27[0m` + end, + + gray = function(s: any) + return `\27[30;1m{s}\27[0m` + end, +} + + +local ECS_PAIR_FLAG = 0x8 +local ECS_ID_FLAGS_MASK = 0x10 +local ECS_ENTITY_MASK = bit32.lshift(1, 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) + +type i53 = number +type i24 = number + +local function ECS_ENTITY_T_LO(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e +end + +local function ECS_GENERATION(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 +end + +local ECS_ID = ECS_ENTITY_T_LO + +local function ECS_COMBINE(source: number, target: number): i53 + return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) +end + +local function ECS_GENERATION_INC(e: i53) + if e > ECS_ENTITY_MASK then + local flags = e // ECS_ID_FLAGS_MASK + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK + + local next_gen = generation + 1 + if next_gen > ECS_GENERATION_MASK then + return id + end + + return ECS_COMBINE(id, next_gen) + flags + end + return ECS_COMBINE(e, 1) +end + +local function bl() + print("") +end + +local function pe(e) + local gen = ECS_GENERATION(e) + return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`) +end + +local function dprint(tbl: { [number]: number }) + bl() + print("--------") + for i, e in tbl do + print("| "..pe(e).." |") + print("--------") + end + bl() +end + +local max_id = 0 +local alive_count = 0 +local dense = {} +local sparse = {} +local function alloc() + if alive_count ~= #dense then + alive_count += 1 + print("*recycled", pe(dense[alive_count])) + return dense[alive_count] + end + max_id += 1 + local id = max_id + alive_count += 1 + dense[alive_count] = id + sparse[id] = { + dense = alive_count + } + print("*allocated", pe(id)) + return id +end + +local function remove(entity) + local id = ECS_ID(entity) + local r = sparse[id] + local index_of_deleted_entity = r.dense + local last_entity_alive_at_index = alive_count -- last entity alive + alive_count -= 1 + local last_alive_entity = dense[last_entity_alive_at_index] + local r_swap = sparse[ECS_ID(last_alive_entity)] + r_swap.dense = r.dense + r.dense = last_entity_alive_at_index + dense[index_of_deleted_entity] = last_alive_entity + dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) + print("*dellocated", pe(id)) +end + +local function alive(e) + local r = sparse[ECS_ID(e)] + + return dense[r.dense] == e +end + +local function pa(e) + print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`) +end + +local tprint = require("@testkit").print +local e1v0 = alloc() +local e2v0 = alloc() +local e3v0 = alloc() +local e4v0 = alloc() +local e5v0 = alloc() +pa(e1v0) +pa(e4v0) +remove(e5v0) +pa(e5v0) + +local e5v1 = alloc() +pa(e5v0) +pa(e5v1) +pa(e2v0) +print(ECS_ID(e2v0)) + +dprint(dense) +remove(e2v0) +dprint(dense) diff --git a/test/stress.client.luau b/test/stress.client.luau index 13b5e86..9bc7775 100644 --- a/test/stress.client.luau +++ b/test/stress.client.luau @@ -1,122 +1,122 @@ -local RunService = game:GetService("RunService") -local ReplicatedStorage = game:GetService("ReplicatedStorage") -_G.__JECS_HI_COMPONENT_ID = 300 -local ecs = require(ReplicatedStorage.ecs) - --- 500 entities --- 2-30 components on each entity --- 300 unique components --- 200 systems --- 1-10 components to query per system - -local startTime = os.clock() - -local world = ecs.World.new() - -local components = {} - -for i = 1, 300 do -- 300 components - components[i] = world:component() -end - -local archetypes = {} -for i = 1, 50 do -- 50 archetypes - local archetype = {} - - for _ = 1, math.random(2, 30) do - local componentId = math.random(1, #components) - - table.insert(archetype, components[componentId]) - end - - archetypes[i] = archetype -end - -for _ = 1, 1000 do -- 1000 entities in the world - local componentsToAdd = {} - - local archetypeId = math.random(1, #archetypes) - local e = world:entity() - for _, component in ipairs(archetypes[archetypeId]) do - world:set(e, component, { - DummyData = math.random(1, 5000), - }) - end -end - -local function values(t) - local array = {} - for _, v in t do - table.insert(array, v) - end - return array -end - -local contiguousComponents = values(components) -local systemComponentsToQuery = {} - -for _ = 1, 200 do -- 200 systems - local numComponentsToQuery = math.random(1, 10) - local componentsToQuery = {} - - for _ = 1, numComponentsToQuery do - table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)]) - end - - table.insert(systemComponentsToQuery, componentsToQuery) -end - -local worldCreateTime = os.clock() - startTime -local results = {} -startTime = os.clock() - -RunService.Heartbeat:Connect(function() - local added = 0 - local systemStartTime = os.clock() - debug.profilebegin("systems") - for _, componentsToQuery in ipairs(systemComponentsToQuery) do - debug.profilebegin("system") - for entityId, firstComponent in world:query(unpack(componentsToQuery)) do - world:set( - entityId, - { - DummyData = firstComponent.DummyData + 1, - } - ) - added += 1 - end - debug.profileend() - end - debug.profileend() - - if os.clock() - startTime < 4 then - -- discard first 4 seconds - return - end - - if results == nil then - return - elseif #results < 1000 then - table.insert(results, os.clock() - systemStartTime) - else - print("added", added) - print("World created in", worldCreateTime * 1000, "ms") - local sum = 0 - for _, result in ipairs(results) do - sum += result - end - print(("Average frame time: %fms"):format((sum / #results) * 1000)) - - results = nil - - local n = #world.archetypes - - print( - ("X entities\n%d components\n%d systems\n%d archetypes"):format( - #components, - #systemComponentsToQuery, - n - ) - ) - end -end) +local RunService = game:GetService("RunService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +_G.__JECS_HI_COMPONENT_ID = 300 +local ecs = require(ReplicatedStorage.ecs) + +-- 500 entities +-- 2-30 components on each entity +-- 300 unique components +-- 200 systems +-- 1-10 components to query per system + +local startTime = os.clock() + +local world = ecs.World.new() + +local components = {} + +for i = 1, 300 do -- 300 components + components[i] = world:component() +end + +local archetypes = {} +for i = 1, 50 do -- 50 archetypes + local archetype = {} + + for _ = 1, math.random(2, 30) do + local componentId = math.random(1, #components) + + table.insert(archetype, components[componentId]) + end + + archetypes[i] = archetype +end + +for _ = 1, 1000 do -- 1000 entities in the world + local componentsToAdd = {} + + local archetypeId = math.random(1, #archetypes) + local e = world:entity() + for _, component in ipairs(archetypes[archetypeId]) do + world:set(e, component, { + DummyData = math.random(1, 5000), + }) + end +end + +local function values(t) + local array = {} + for _, v in t do + table.insert(array, v) + end + return array +end + +local contiguousComponents = values(components) +local systemComponentsToQuery = {} + +for _ = 1, 200 do -- 200 systems + local numComponentsToQuery = math.random(1, 10) + local componentsToQuery = {} + + for _ = 1, numComponentsToQuery do + table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)]) + end + + table.insert(systemComponentsToQuery, componentsToQuery) +end + +local worldCreateTime = os.clock() - startTime +local results = {} +startTime = os.clock() + +RunService.Heartbeat:Connect(function() + local added = 0 + local systemStartTime = os.clock() + debug.profilebegin("systems") + for _, componentsToQuery in ipairs(systemComponentsToQuery) do + debug.profilebegin("system") + for entityId, firstComponent in world:query(unpack(componentsToQuery)) do + world:set( + entityId, + { + DummyData = firstComponent.DummyData + 1, + } + ) + added += 1 + end + debug.profileend() + end + debug.profileend() + + if os.clock() - startTime < 4 then + -- discard first 4 seconds + return + end + + if results == nil then + return + elseif #results < 1000 then + table.insert(results, os.clock() - systemStartTime) + else + print("added", added) + print("World created in", worldCreateTime * 1000, "ms") + local sum = 0 + for _, result in ipairs(results) do + sum += result + end + print(("Average frame time: %fms"):format((sum / #results) * 1000)) + + results = nil + + local n = #world.archetypes + + print( + ("X entities\n%d components\n%d systems\n%d archetypes"):format( + #components, + #systemComponentsToQuery, + n + ) + ) + end +end) diff --git a/test/tools/entity_visualiser.luau b/test/tools/entity_visualiser.luau index 1df9dc0..9698939 100644 --- a/test/tools/entity_visualiser.luau +++ b/test/tools/entity_visualiser.luau @@ -1,24 +1,24 @@ -local jecs = require("@jecs") -local pair = jecs.pair -local ChildOf = jecs.ChildOf -local lifetime_tracker_add = require("@tools/lifetime_tracker") -local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false}) -local FriendsWith = world:component() -world:print_snapshot() -local e1 = world:entity() -local e2 = world:entity() -world:delete(e2) - -world:print_snapshot() -local e3 = world:entity() -world:add(e3, pair(ChildOf, e1)) -local e4 = world:entity() -world:add(e4, pair(FriendsWith, e3)) -world:print_snapshot() -world:delete(e1) -world:delete(e3) -world:print_snapshot() -world:print_entity_index() -world:entity() -world:entity() -world:print_snapshot() +local jecs = require("@jecs") +local pair = jecs.pair +local ChildOf = jecs.ChildOf +local lifetime_tracker_add = require("@tools/lifetime_tracker") +local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false}) +local FriendsWith = world:component() +world:print_snapshot() +local e1 = world:entity() +local e2 = world:entity() +world:delete(e2) + +world:print_snapshot() +local e3 = world:entity() +world:add(e3, pair(ChildOf, e1)) +local e4 = world:entity() +world:add(e4, pair(FriendsWith, e3)) +world:print_snapshot() +world:delete(e1) +world:delete(e3) +world:print_snapshot() +world:print_entity_index() +world:entity() +world:entity() +world:print_snapshot()