diff --git a/demo/src/ReplicatedStorage/start.luau b/demo/src/ReplicatedStorage/start.luau index 81e19d02..1c4f3737 100644 --- a/demo/src/ReplicatedStorage/start.luau +++ b/demo/src/ReplicatedStorage/start.luau @@ -2,18 +2,16 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local UserInputService = game:GetService("UserInputService") local jabby = require(ReplicatedStorage.Packages.jabby) -local std = require(ReplicatedStorage.std) -local Scheduler = std.Scheduler -local world = std.world +local std = ReplicatedStorage.std +local scheduler = require(std.scheduler) +local world = require(std.world) local function start(modules) - local scheduler = Scheduler.new(world, require(ReplicatedStorage.std.components)) for _, module in modules do - require(module)(scheduler) + require(module) end - local events = scheduler.collect.all() - scheduler.systems.begin(events) - + local events = scheduler.COLLECT() + scheduler.BEGIN(events) jabby.set_check_function(function(player) return true end) diff --git a/demo/src/ReplicatedStorage/std/bt.luau b/demo/src/ReplicatedStorage/std/bt.luau index 24473c2a..81d11c57 100644 --- a/demo/src/ReplicatedStorage/std/bt.luau +++ b/demo/src/ReplicatedStorage/std/bt.luau @@ -3,15 +3,15 @@ -- original author @centau -local SUCCESS = 0 -local FAILURE = 1 -local RUNNING = 2 +local FAILURE = -1 +local RUNNING = 0 +local SUCCESS = 1 local function SEQUENCE(nodes) return function(...) for _, node in nodes do local status = node(...) - if status == FAILURE or status == RUNNING then + if status <= RUNNING then return status end end @@ -23,7 +23,7 @@ local function FALLBACK(nodes) return function(...) for _, node in nodes do local status = node(...) - if status == SUCCESS or status == RUNNING then + if status > FAILURE then return status end end diff --git a/demo/src/ReplicatedStorage/std/components.luau b/demo/src/ReplicatedStorage/std/components.luau index 88e27a87..0e2d4065 100644 --- a/demo/src/ReplicatedStorage/std/components.luau +++ b/demo/src/ReplicatedStorage/std/components.luau @@ -8,7 +8,7 @@ local components: { Model: Entity, Player: Entity, Target: Entity, - Transform: Entity, + Transform: Entity<{ new: CFrame, old: CFrame }>, Velocity: Entity, Previous: Entity, } = @@ -23,4 +23,8 @@ local components: { Previous = world:component(), } +for name, component in components :: {[string]: jecs.Entity} do + world:set(component, jecs.Name, name) +end + return table.freeze(components) diff --git a/demo/src/ReplicatedStorage/std/ctx.luau b/demo/src/ReplicatedStorage/std/ctx.luau deleted file mode 100644 index 91a0db68..00000000 --- a/demo/src/ReplicatedStorage/std/ctx.luau +++ /dev/null @@ -1,11 +0,0 @@ -local handle = require(script.Parent.handle) -local world = require(script.Parent.world) - -local singleton = world:entity() - -local function ctx() - -- Cannot cache handles because they will get invalidated - return handle(singleton) -end - -return ctx diff --git a/demo/src/ReplicatedStorage/std/handle.luau b/demo/src/ReplicatedStorage/std/handle.luau deleted file mode 100644 index cb4aba8f..00000000 --- a/demo/src/ReplicatedStorage/std/handle.luau +++ /dev/null @@ -1,56 +0,0 @@ -local jecs = require(game:GetService("ReplicatedStorage").ecs) -local world = require(script.Parent.world) - -type Handle = { - has: (self: Handle, id: jecs.Entity) -> boolean, - get: (self: Handle, id: jecs.Entity) -> T?, - add: (self: Handle, id: jecs.Entity) -> Handle, - set: (self: Handle, id: jecs.Entity, value: T) -> Handle, - id: (self: Handle?) -> jecs.Entity, -} - -local handle: (e: jecs.Entity) -> Handle - -do - local e - local function has(_, id) - return world:has(e, id) - end - local function get(_, id) - return world:get(e, id) - end - local function set(self, id, value) - world:set(e, id, value) - return self - end - local function add(self, id) - world:add(e, id) - return self - end - local function clear(self) - world:clear(e) - return self - end - local function delete(self) - world:delete(e) - end - local function id() - return e - end - - local entity = { - has = has, - get = get, - set = set, - add = add, - clear = clear, - id = id, - } - - function handle(id) - e = id - return entity - end -end - -return handle diff --git a/demo/src/ReplicatedStorage/std/hooks.luau b/demo/src/ReplicatedStorage/std/hooks.luau deleted file mode 100644 index abf66ada..00000000 --- a/demo/src/ReplicatedStorage/std/hooks.luau +++ /dev/null @@ -1,32 +0,0 @@ ---!native ---!optimize 2 -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local jecs = require(ReplicatedStorage.ecs) - -local function create_cache(hook) - local columns = setmetatable({}, { - __index = function(self, component) - local column = {} - self[component] = column - return column - end, - }) - - return function(world, component, fn) - local column = columns[component] - table.insert(column, fn) - world:set(component, hook, function(entity, value) - for _, callback in column do - callback(entity, value) - end - end) - end -end - -local hooks = { - OnSet = create_cache(jecs.OnSet), - OnAdd = create_cache(jecs.OnAdd), - OnRemove = create_cache(jecs.OnRemove), -} - -return hooks diff --git a/demo/src/ReplicatedStorage/std/init.luau b/demo/src/ReplicatedStorage/std/init.luau deleted file mode 100644 index 4fc23498..00000000 --- a/demo/src/ReplicatedStorage/std/init.luau +++ /dev/null @@ -1,25 +0,0 @@ -local jecs = require(game:GetService("ReplicatedStorage").ecs) - -local world = require(script.world) :: jecs.World -export type World = jecs.World - -local Scheduler = require(script.scheduler) -export type Scheduler = Scheduler.Scheduler - -local std = { - ChangeTracker = require(script.changetracker), - Scheduler = Scheduler, - bt = require(script.bt), - collect = require(script.collect), - components = require(script.components), - ctx = require(script.ctx), - handle = require(script.handle), - interval = require(script.interval), - ref = require(script.ref), - world = world :: World, - pair = jecs.pair, - __ = jecs.w, - hooks = require(script.hooks), -} - -return std diff --git a/demo/src/ReplicatedStorage/std/interval.luau b/demo/src/ReplicatedStorage/std/interval.luau index 37b40538..99cda27f 100644 --- a/demo/src/ReplicatedStorage/std/interval.luau +++ b/demo/src/ReplicatedStorage/std/interval.luau @@ -16,4 +16,4 @@ local function interval(s) return throttle end -return interval +return interval \ No newline at end of file diff --git a/demo/src/ReplicatedStorage/std/phases.luau b/demo/src/ReplicatedStorage/std/phases.luau new file mode 100644 index 00000000..30e6bbee --- /dev/null +++ b/demo/src/ReplicatedStorage/std/phases.luau @@ -0,0 +1,14 @@ +local std = game:GetService("ReplicatedStorage").std +local Players = game:GetService("Players") + +local scheduler = require(std.scheduler) +local PHASE = scheduler.PHASE + +return { + PlayerAdded = PHASE({ + event = Players.PlayerAdded + }), + PlayerRemoved = PHASE({ + event = Players.PlayerRemoving + }) +} diff --git a/demo/src/ReplicatedStorage/std/ref.luau b/demo/src/ReplicatedStorage/std/ref.luau index abd1b3af..6700f142 100644 --- a/demo/src/ReplicatedStorage/std/ref.luau +++ b/demo/src/ReplicatedStorage/std/ref.luau @@ -1,16 +1,18 @@ -local handle = require(script.Parent.handle) local world = require(script.Parent.world) -local refs = {} +local jecs = require(game:GetService("ReplicatedStorage").ecs) +local refs: {[any]: jecs.Entity} = {} -local function fini(key) +local function fini(key): () -> () return function() refs[key] = nil end end -local function ref(key): (handle.Handle, (() -> ())?) +local function noop() end + +local function ref(key): (jecs.Entity, () -> ()) if not key then - return handle(world:entity()) + return world:entity(), noop end local e = refs[key] if not e then @@ -18,7 +20,7 @@ local function ref(key): (handle.Handle, (() -> ())?) refs[key] = e end -- Cannot cache handles because they will get invalidated - return handle(e), fini(key) + return e, fini(key) end return ref diff --git a/demo/src/ReplicatedStorage/std/registry.luau b/demo/src/ReplicatedStorage/std/registry.luau deleted file mode 100644 index 7be5d09f..00000000 --- a/demo/src/ReplicatedStorage/std/registry.luau +++ /dev/null @@ -1,31 +0,0 @@ -local reserved = 0 - -local function reserve() - reserved += 1 - return reserved -end - --- If you don't like passing around a world singleton --- and you need to register component IDs, just register them. --- I dont use this because I like adding component traits ---[[ - local components = { - Model = registry.reserve(), - Transform = registry.reserve(), - } - - local world = registry.register(jecs.World.new()) - local e = world:entity() - world:set(e, components.Transform, CFrame) -]] -local function register(world) - for _ = 1, reserved do - world:component() - end - return world -end - -return { - reserve = reserve, - register = register, -} diff --git a/demo/src/ReplicatedStorage/std/scheduler.luau b/demo/src/ReplicatedStorage/std/scheduler.luau index 2f0bca67..4519b5eb 100644 --- a/demo/src/ReplicatedStorage/std/scheduler.luau +++ b/demo/src/ReplicatedStorage/std/scheduler.luau @@ -1,6 +1,7 @@ --!native --!optimize 2 local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") local jabby = require(ReplicatedStorage.Packages.jabby) local jecs = require(ReplicatedStorage.ecs) local pair = jecs.pair @@ -8,6 +9,7 @@ local Name = jecs.Name type World = jecs.World type Entity = jecs.Entity +type Id = jecs.Id type System = { callback: (world: World) -> (), @@ -21,226 +23,145 @@ type Events = { Heartbeat: Systems, } -export type Scheduler = { - components: { - Disabled: Entity, - System: Entity, - Phase: Entity, - DependsOn: Entity, - }, +local world = require(script.Parent.world) +local Disabled = world:entity() +local System = world:component() :: Id<{ callback: (any) -> (), name: string}> +local DependsOn = world:entity() +local Event = world:component() :: Id +local Phase = world:entity() - collect: { - under_event: (event: Entity) -> Systems, - all: () -> Events, - }, +local PreRender = world:entity() +local Heartbeat = world:entity() +local PreAnimation = world:entity() +local PreSimulation = world:entity() - systems: { - begin: (events: Events) -> { [Entity]: thread }, - new: (callback: (dt: number) -> (), phase: Entity) -> Entity, - }, +local sys: System +local dt: number - phases: { - RenderStepped: Entity, - Heartbeat: Entity, - }, +local jabby_scheduler = jabby.scheduler.create("Scheduler") - phase: (after: Entity) -> Entity, +local a, b, c, d +local function run() + local id = sys.id + jabby_scheduler:run(id, sys.callback, a, b, c, d) + return nil +end - debugging: boolean, -} +world:add(Heartbeat, Phase) +world:set(Heartbeat, Event, RunService.Heartbeat) -local scheduler_new: (w: World, components: { [string]: Entity }) -> Scheduler +world:add(PreSimulation, Phase) +world:set(PreSimulation, Event, RunService.PreSimulation) -do - local world: World - local Disabled: Entity - local System: Entity - local DependsOn: Entity - local Phase: Entity - local Event: Entity +world:add(PreAnimation, Phase) +world:set(PreAnimation, Event, RunService.PreAnimation) - local scheduler +table.insert(jabby.public, { + class_name = "World", + name = "MyWorld", + world = world, + debug = Name, + entities = {}, +}) - local RenderStepped - local Heartbeat - local PreAnimation - local PreSimulation +jabby.public.updated = true - local sys: System - local dt +table.insert(jabby.public, jabby_scheduler) - local function run() - local id = sys.id - scheduler:run(id, sys.callback, dt) - end +if RunService:IsClient() then + world:add(PreRender, Phase) + world:set(PreRender, Event, (RunService :: RunService).PreRender) +end - local function panic(str) - -- We don't want to interrupt the loop when we error - task.spawn(error, str) - end - local function begin(events: { Systems }) - local connections = {} - for event, systems in events do - if not event then - continue - end - local event_name = tostring(event) - connections[event] = event:Connect(function(delta) - debug.profilebegin(event_name) - for _, s in systems do - sys = s - dt = delta - local didNotYield, why = xpcall(function() - for _ in run do - break - end - end, debug.traceback) - - if didNotYield then - continue - end - - if string.find(why, "thread is not yieldable") then - panic( - "Not allowed to yield in the systems." - .. "\n" - .. "System: " - .. debug.info(s.callback, "n") - .. " has been ejected" - ) - continue - end - panic(why) - end - debug.profileend() - end) +local function begin(events: { [RBXScriptSignal]: Systems }) + local connections = {} + for event, systems in events do + if not event then + continue end - return threads - end + local event_name = tostring(event) + connections[event] = event:Connect(function(...) + debug.profilebegin(event_name) + for _, s in systems do + sys = s + a, b, c, d = ... + + for _ in run do + break + end - local function scheduler_collect_systems_under_phase_recursive(systems, phase) - local phase_name = world:get(phase, Name) - for _, s in world:query(System):with(pair(DependsOn, phase)) do - table.insert(systems, { - id = scheduler:register_system({ - name = s.name, - phase = phase_name, - }), - callback = s.callback, - }) - end - for after in world:query(Phase):with(pair(DependsOn, phase)) do - scheduler_collect_systems_under_phase_recursive(systems, after) - end + end + debug.profileend() + end) end + return connections +end - local function scheduler_collect_systems_under_event(event) - local systems = {} - scheduler_collect_systems_under_phase_recursive(systems, event) - return systems +local function scheduler_collect_systems_under_phase_recursive(systems, phase: Entity) + local phase_name = world:get(phase, Name) + for _, s in world:query(System):with(pair(DependsOn, phase)) do + table.insert(systems, { + id = jabby_scheduler:register_system({ + name = s.name, + phase = phase_name, + } :: any), + callback = s.callback, + }) + end + for after in world:query(Phase):with(pair(DependsOn, phase)):iter() do + scheduler_collect_systems_under_phase_recursive(systems, after) end +end - local function scheduler_collect_systems_all() - local events = {} - for phase, event in world:query(Event):with(Phase) do - events[event] = scheduler_collect_systems_under_event(phase) - end - return events +local function scheduler_collect_systems_under_event(event) + local systems = {} + scheduler_collect_systems_under_phase_recursive(systems, event) + return systems +end + +local function scheduler_collect_systems_all() + local events = {} + for phase, event in world:query(Event):with(Phase) do + events[event] = scheduler_collect_systems_under_event(phase) end + return events +end - local function scheduler_phase_new(after) - local phase = world:entity() - world:add(phase, Phase) - local dependency = pair(DependsOn, after) +local function scheduler_phase_new(d: { after: Entity?, event: RBXScriptSignal? }) + local phase = world:entity() + world:add(phase, Phase) + local after = d.after + if after then + local dependency = pair(DependsOn, after :: Entity) world:add(phase, dependency) - return phase end - - local function scheduler_systems_new(callback, phase) - local system = world:entity() - local name = debug.info(callback, "n") - world:set(system, System, { callback = callback, name = name }) - world:add(system, pair(DependsOn, phase)) - return system + + local event = d.event + if event then + world:set(phase, Event, event) end + return phase +end - function scheduler_new(w: World, components: { [string]: Entity }) - world = w - Disabled = world:component() - System = world:component() - Phase = world:component() - DependsOn = world:component() - Event = world:component() - - RenderStepped = world:component() - Heartbeat = world:component() - PreSimulation = world:component() - PreAnimation = world:component() - - local RunService = game:GetService("RunService") - if RunService:IsClient() then - world:add(RenderStepped, Phase) - world:set(RenderStepped, Event, RunService.RenderStepped) - end - - world:add(Heartbeat, Phase) - world:set(Heartbeat, Event, RunService.Heartbeat) - - world:add(PreSimulation, Phase) - world:set(PreSimulation, Event, RunService.PreSimulation) - - world:add(PreAnimation, Phase) - world:set(PreAnimation, Event, RunService.PreAnimation) - - for name, component in components do - world:set(component, Name, name) - end - - table.insert(jabby.public, { - class_name = "World", - name = "MyWorld", - world = world, - debug = Name, - entities = {}, - }) - - jabby.public.updated = true - scheduler = jabby.scheduler.create("scheduler") - - table.insert(jabby.public, scheduler) - - return { - phase = scheduler_phase_new, - - phases = { - RenderStepped = RenderStepped, - PreSimulation = PreSimulation, - Heartbeat = Heartbeat, - PreAnimation = PreAnimation, - }, - - world = world, - - components = { - DependsOn = DependsOn, - Disabled = Disabled, - Phase = Phase, - System = System, - }, - - collect = { - under_event = scheduler_collect_systems_under_event, - all = scheduler_collect_systems_all, - }, - - systems = { - new = scheduler_systems_new, - begin = begin, - }, - } - end +local function scheduler_systems_new(callback: (any) -> (), phase: Entity?) + local system = world:entity() + world:set(system, System, { callback = callback, name = debug.info(callback, "n") }) + local depends_on = DependsOn :: jecs.Entity + local p: Entity = phase or Heartbeat + world:add(system, pair(depends_on, p)) + + return system end return { - new = scheduler_new, + SYSTEM = scheduler_systems_new, + BEGIN = begin, + PHASE = scheduler_phase_new, + COLLECT = scheduler_collect_systems_all, + phases = { + Heartbeat = Heartbeat, + PreSimulation = PreSimulation, + PreAnimation = PreAnimation, + PreRender = PreRender + } } diff --git a/demo/src/ServerScriptService/systems/mobs.luau b/demo/src/ServerScriptService/systems/mobs.luau index 20aab19d..07b0d9ff 100644 --- a/demo/src/ServerScriptService/systems/mobs.luau +++ b/demo/src/ServerScriptService/systems/mobs.luau @@ -6,13 +6,12 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local blink = require(game:GetService("ServerScriptService").net) local jecs = require(ReplicatedStorage.ecs) local __ = jecs.Wildcard +local std = ReplicatedStorage.std +local ref = require(std.ref) +local interval = require(std.interval) -local std = require(ReplicatedStorage.std) -local ref = std.ref -local interval = std.interval - -local world: std.World = std.world -local cts = std.components +local world = require(std.world) +local cts = require(std.components) local Mob = cts.Mob local Transform = cts.Transform @@ -20,13 +19,26 @@ local Velocity = cts.Velocity local Player = cts.Player local Character = cts.Character +local characters = world + :query(Character) + :with(Player) + :cached() + + +local moving_mobs = world + :query(Transform, Velocity) + :with(Mob) + :cached() + + local function mobsMove(dt: number) local targets = {} - for _, character in world:query(Character):with(Player):iter() do + + for _, character in characters do table.insert(targets, (character.PrimaryPart :: Part).Position) end - for mob, transform, v in world:query(Transform, Velocity):with(Mob):iter() do + for mob, transform, v in moving_mobs do local cf = transform.new local p = cf.Position @@ -53,21 +65,24 @@ end local throttle = interval(5) -local function spawnMobs() +local function spawnMobs() if throttle() then local p = Vector3.new(0, 5, 0) local cf = CFrame.new(p) local v = 5 - local id = ref():set(Velocity, v):set(Transform, { new = cf }):add(Mob).id() + local e = world:entity() + world:set(e, Velocity, v) + world:set(e, Transform, { new = cf }) + world:add(e, Mob) - blink.SpawnMob.FireAll(id, cf, v) + blink.SpawnMob.FireAll(e, cf, v) end end -return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(mobsMove, phases.Heartbeat) - system_new(spawnMobs, phases.Heartbeat) -end +local scheduler = require(std.scheduler) + +scheduler.SYSTEM(spawnMobs) +scheduler.SYSTEM(mobsMove) + +return 0 \ No newline at end of file diff --git a/demo/src/ServerScriptService/systems/players.luau b/demo/src/ServerScriptService/systems/players.luau index 0532f7ac..27dcc8ba 100644 --- a/demo/src/ServerScriptService/systems/players.luau +++ b/demo/src/ServerScriptService/systems/players.luau @@ -1,43 +1,40 @@ local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") -local std = require(ReplicatedStorage.std) -local ref = std.ref -local collect = std.collect +local std = ReplicatedStorage.std +local ref = require(std.ref) +local collect = require(std.collect) -local cts = std.components +local cts = require(std.components) +local world = require(std.world) local Player = cts.Player local Character = cts.Character -local playersAdded = collect(Players.PlayerAdded) -local playersRemoved = collect(Players.PlayerRemoving) -local world: std.World = std.world - local conn = {} -local function players() - for _, player in playersAdded do - world:set(std.world:entity(), cts.Transform) - - local e = ref(player.UserId):set(Player, player) - local characterAdd = player.CharacterAdded - conn[e.id()] = characterAdd:Connect(function(rig) - while rig.Parent ~= workspace do - task.wait() - end - e:set(Character, rig) - end) - end - - for _, player in playersRemoved do - local id = ref(player.UserId):clear().id() - conn[id]:Disconnect() - conn[id] = nil - end +local function playersAdded(player: Player) + local e = ref(player.UserId) + world:set(e, Player, player) + local characterAdd = player.CharacterAdded + conn[e] = characterAdd:Connect(function(rig) + while rig.Parent ~= workspace do + task.wait() + end + world:set(e, Character, rig) + end) end -return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(players, phases.Heartbeat) +local function playersRemoved(player: Player) + local e = ref(player.UserId) + world:clear(e) + local connection = conn[e] + connection:Disconnect() + conn[e] = nil end + +local scheduler = require(std.scheduler) +local phases = require(std.phases) +scheduler.SYSTEM(playersAdded, phases.PlayerAdded) +scheduler.SYSTEM(playersRemoved, phases.PlayerRemoved) + +return 0 \ No newline at end of file diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau index 0c649332..f0fc7108 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau @@ -1,20 +1,22 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local blink = require(ReplicatedStorage.net) -local std = require(ReplicatedStorage.std) -local world = std.world -local ref = std.ref +local std = ReplicatedStorage.std +local world = require(std.world) +local ref = require(std.ref) -local cts = std.components +local cts = require(std.components) local Model = cts.Model local Transform = cts.Transform +local moving_models = world:query(Transform, Model):cached() local function move(dt: number) - for _, transform, model in world:query(Transform, Model):iter() do + for _, transform, model in moving_models do local cf = transform.new if cf ~= transform.old then - local origo = model.PrimaryPart.CFrame - model.PrimaryPart.CFrame = origo:Lerp(cf, 1) + local part = model.PrimaryPart :: BasePart + local origo = part.CFrame + part.CFrame = origo:Lerp(cf, 1) transform.old = cf end end @@ -22,8 +24,8 @@ end local function syncTransforms() for _, id, cf in blink.UpdateTransform.Iter() do - local e = ref("server-" .. id) - local transform = e:get(cts.Transform) + local e = ref("server-" .. tostring(id)) + local transform = world:get(e, Transform) if not transform then continue end @@ -31,9 +33,9 @@ local function syncTransforms() end end -return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(move, phases.Heartbeat) - system_new(syncTransforms, phases.RenderStepped) -end +local scheduler = require(std.scheduler) + +scheduler.SYSTEM(move) +scheduler.SYSTEM(syncTransforms) + +return 0 \ No newline at end of file diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau index 94cd97db..c28bbfa3 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau @@ -1,9 +1,9 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local blink = require(ReplicatedStorage.net) -local std = require(ReplicatedStorage.std) -local ref = std.ref -local world = std.world -local cts = std.components +local std = ReplicatedStorage.std +local ref = require(std.ref) +local world = require(std.world) +local cts = require(std.components) local function syncMobs() for _, id, cf, vel in blink.SpawnMob.Iter() do @@ -16,16 +16,16 @@ local function syncMobs() part.Parent = model model.Parent = workspace - ref("server-" .. id) - :set(cts.Transform, { new = cf, old = cf }) - :set(cts.Velocity, vel) - :set(cts.Model, model) - :add(cts.Mob) + local e = ref("server-" .. tostring(id)) + world:set(e, cts.Transform, { new = cf, old = cf }) + world:set(e, cts.Velocity, vel) + world:set(e, cts.Model, model) + world:add(e, cts.Mob) end end -return function(scheduler: std.Scheduler) - local phases = scheduler.phases - local system_new = scheduler.systems.new - system_new(syncMobs, phases.RenderStepped) -end +local scheduler = require(std.scheduler) +scheduler.SYSTEM(syncMobs) + +return 0 + diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/test.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/test.luau new file mode 100644 index 00000000..250bbf1a --- /dev/null +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/test.luau @@ -0,0 +1,44 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local std = ReplicatedStorage.std +local world = require(std.world) + +local A = world:component() +local B = world:component() +local C = world:component() +local D = world:component() + +local function flip() + return math.random() >= 0.15 +end + +for i = 1, 2^8 do + local e = world:entity() + if flip() then + world:set(e, A, true) + end + if flip() then + world:set(e, B, true) + end + if flip() then + world:set(e, C, true) + end + if flip() then + world:set(e, D, true) + end +end + +local function uncached() + for _ in world:query(A, B, C, D) do + end +end + +local q = world:query(A, B, C, D):cached() +local function cached() + for _ in q do + end +end + +local scheduler = require(std.scheduler) +scheduler.SYSTEM(uncached) +scheduler.SYSTEM(cached) +return 0 \ No newline at end of file diff --git a/jecs.luau b/jecs.luau index 1b0ac082..68228560 100644 --- a/jecs.luau +++ b/jecs.luau @@ -78,30 +78,32 @@ type EntityIndex = { local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 -- stylua: ignore start -local EcsOnAdd = HI_COMPONENT_ID + 1 -local EcsOnRemove = HI_COMPONENT_ID + 2 -local EcsOnSet = HI_COMPONENT_ID + 3 -local EcsWildcard = HI_COMPONENT_ID + 4 -local EcsChildOf = HI_COMPONENT_ID + 5 -local EcsComponent = HI_COMPONENT_ID + 6 -local EcsOnDelete = HI_COMPONENT_ID + 7 -local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 -local EcsDelete = HI_COMPONENT_ID + 9 -local EcsRemove = HI_COMPONENT_ID + 10 -local EcsName = HI_COMPONENT_ID + 11 -local EcsRest = HI_COMPONENT_ID + 12 +local EcsOnAdd = HI_COMPONENT_ID + 1 +local EcsOnRemove = HI_COMPONENT_ID + 2 +local EcsOnSet = HI_COMPONENT_ID + 3 +local EcsWildcard = HI_COMPONENT_ID + 4 +local EcsChildOf = HI_COMPONENT_ID + 5 +local EcsComponent = HI_COMPONENT_ID + 6 +local EcsOnDelete = HI_COMPONENT_ID + 7 +local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 +local EcsDelete = HI_COMPONENT_ID + 9 +local EcsRemove = HI_COMPONENT_ID + 10 +local EcsName = HI_COMPONENT_ID + 11 +local EcsArchetypeCreate = HI_COMPONENT_ID + 12 +local EcsArchetypeDelete = HI_COMPONENT_ID + 13 +local EcsRest = HI_COMPONENT_ID + 14 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) -local ECS_ID_DELETE = 0b0000_0001 -local ECS_ID_IS_TAG = 0b0000_0010 -local ECS_ID_HAS_ON_ADD = 0b0000_0100 -local ECS_ID_HAS_ON_SET = 0b0000_1000 -local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 -local ECS_ID_MASK = 0b0000_0000 +local ECS_ID_DELETE = 0b0000_0001 +local ECS_ID_IS_TAG = 0b0000_0010 +local ECS_ID_HAS_ON_ADD = 0b0000_0100 +local ECS_ID_HAS_ON_SET = 0b0000_1000 +local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 +local ECS_ID_MASK = 0b0000_0000 -- stylua: ignore end local NULL_ARRAY = table.freeze({}) :: Column @@ -250,6 +252,36 @@ local function ecs_pair_second(world, e) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e)) end +local function query_match(query, archetype: Archetype) + local records = archetype.records + local with = query.filter_with + + for _, id in with do + if not records[id] then + return false + end + end + + local without = query.filter_without + if without then + for _, id in without do + if records[id] then + return false + end + end + end + + return true +end + +local function find_observers(world: World, event, component): { Observer }? + local cache = world.observerable[event] + if not cache then + return nil + end + return cache[component] :: any +end + local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) local src_columns = from.columns local dst_columns = to.columns @@ -549,6 +581,20 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) local columns = (table.create(length) :: any) :: { Column } local records: { ArchetypeRecord } = {} + + local archetype: Archetype = { + columns = columns, + entities = {}, + id = archetype_id, + records = records, + type = ty, + types = id_types, + + add = {}, + remove = {}, + refs = {} :: GraphEdge, + } + for i, componentId in id_types do local idr = id_record_ensure(world, componentId) archetype_append_to_records(idr, archetype_id, records, componentId, i) @@ -572,18 +618,17 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) end end - local archetype: Archetype = { - columns = columns, - entities = {}, - id = archetype_id, - records = records, - type = ty, - types = id_types, - - add = {}, - remove = {}, - refs = {} :: GraphEdge, - } + for _, id in id_types do + local observer_list = find_observers(world, EcsArchetypeCreate, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end + end world.archetypeIndex[ty] = archetype world.archetypes[archetype_id] = archetype @@ -626,13 +671,13 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number end local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype - local types = node.types + local id_types = node.types -- Component IDs are added incrementally, so inserting and sorting -- them each time would be expensive. Instead this insertion sort can find the insertion -- point in the types array. local dst = table.clone(node.types) :: { i53 } - local at = find_insert(types, id) + local at = find_insert(id_types, id) if at == -1 then -- If it finds a duplicate, it just means it is the same archetype so it can return it -- directly instead of needing to hash types for a lookup to the archetype. @@ -644,13 +689,13 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch end local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype - local types = node.types - local at = table.find(types, id) + local id_types = node.types + local at = table.find(id_types, id) if at == nil then return node end - local dst = table.clone(types) + local dst = table.clone(id_types) table.remove(dst, at) return archetype_ensure(world, dst) @@ -1006,6 +1051,18 @@ local function archetype_destroy(world: World, archetype: Archetype) world.archetypeIndex[archetype.type] = nil :: any local records = archetype.records + for id in records do + local observer_list = find_observers(world, EcsArchetypeDelete, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end + end + for id in records do local idr = component_index[id] idr.cache[archetype_id] = nil :: any @@ -1064,23 +1121,30 @@ do local idr = component_index[delete] if idr then - local children = {} - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - - for i, child in idr_archetype.entities do - table.insert(children, child) - end - end local flags = idr.flags if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for _, child in children do - -- Cascade deletion to children - world_delete(world, child) + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_delete(world, entities[i]) + end end else - for _, child in children do - world_remove(world, child, delete) + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_remove(world, entities[i], delete) + end + end + + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + archetype_destroy(world, idr_archetype) end end end @@ -1167,7 +1231,7 @@ local EMPTY_QUERY = { setmetatable(EMPTY_QUERY, EMPTY_QUERY) -local function query_iter_init(query): () -> (number, ...any) +local function query_iter_init(query: QueryInner): () -> (number, ...any) local world_query_iter_next local compatible_archetypes = query.compatible_archetypes @@ -1246,7 +1310,7 @@ local function query_iter_init(query): () -> (number, ...any) i = #entities entityId = entities[i] columns = archetype.columns - local records = archetype.records + records = archetype.records a = columns[records[A].column] end @@ -1269,7 +1333,7 @@ local function query_iter_init(query): () -> (number, ...any) i = #entities entityId = entities[i] columns = archetype.columns - local records = archetype.records + records = archetype.records a = columns[records[A].column] b = columns[records[B].column] end @@ -1293,7 +1357,7 @@ local function query_iter_init(query): () -> (number, ...any) i = #entities entityId = entities[i] columns = archetype.columns - local records = archetype.records + records = archetype.records a = columns[records[A].column] b = columns[records[B].column] c = columns[records[C].column] @@ -1318,7 +1382,7 @@ local function query_iter_init(query): () -> (number, ...any) i = #entities entityId = entities[i] columns = archetype.columns - local records = archetype.records + records = archetype.records a = columns[records[A].column] b = columns[records[B].column] c = columns[records[C].column] @@ -1345,7 +1409,7 @@ local function query_iter_init(query): () -> (number, ...any) i = #entities entityId = entities[i] columns = archetype.columns - local records = archetype.records + records = archetype.records if not F then a = columns[records[A].column] @@ -1414,65 +1478,64 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: { compatible_archetypes: { Archetype } }, ...) +local function query_without(query: QueryInner, ...: i53) + local without = { ... } + query.filter_without = without local compatible_archetypes = query.compatible_archetypes - local N = select("#", ...) for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] local records = archetype.records - local shouldRemove = false + local matches = true - for j = 1, N do - local id = select(j, ...) + for _, id in without do if records[id] then - shouldRemove = true + matches = false break end end - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any + if matches then + continue end - end - if #compatible_archetypes == 0 then - return EMPTY_QUERY + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any end return query :: any end -local function query_with(query: { compatible_archetypes: { Archetype } }, ...) +local function query_with(query: QueryInner, ...: i53) local compatible_archetypes = query.compatible_archetypes - local N = select("#", ...) + local with = { ... } + query.filter_with = with + for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] local records = archetype.records - local shouldRemove = false + local matches = true - for j = 1, N do - local id = select(j, ...) + for _, id in with do if not records[id] then - shouldRemove = true + matches = false break end end - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any + if matches then + continue end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any end - if #compatible_archetypes == 0 then - return EMPTY_QUERY - end + return query :: any end @@ -1483,6 +1546,311 @@ local function query_archetypes(query) return query.compatible_archetypes end +local function query_cached(query: QueryInner) + local archetypes = query.compatible_archetypes + local world = query.world :: World + -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively + -- because the event will be emitted for all components of that Archetype. + local first = query.ids[1] + local observerable = world.observerable + local on_create_action = observerable[EcsArchetypeCreate] + if not on_create_action then + on_create_action = {} + observerable[EcsArchetypeCreate] = on_create_action + end + local query_cache_on_create = on_create_action[first] + if not query_cache_on_create then + query_cache_on_create = {} + on_create_action[first] = query_cache_on_create + end + + local on_delete_action = observerable[EcsArchetypeDelete] + if not on_delete_action then + on_delete_action = {} + observerable[EcsArchetypeDelete] = on_delete_action + end + local query_cache_on_delete = on_delete_action[first] + if not query_cache_on_delete then + query_cache_on_delete = {} + on_delete_action[first] = query_cache_on_delete + end + + local function on_create_callback(archetype) + table.insert(archetypes, archetype) + end + + local function on_delete_callback(archetype) + local i = table.find(archetypes, archetype) :: number + local n = #archetypes + archetypes[i] = archetypes[n] + archetypes[n] = nil + end + + local with = query.filter_with + local ids = query.ids + if with then + table.move(ids, 1, #ids, #with, with) + else + query.filter_with = ids + end + + local observer_for_create = { query = query, callback = on_create_callback } + local observer_for_delete = { query = query, callback = on_delete_callback } + + table.insert(query_cache_on_create, observer_for_create) + table.insert(query_cache_on_delete, observer_for_delete) + + local compatible_archetypes = query.compatible_archetypes + local lastArchetype = 1 + + local A, B, C, D, E, F, G, H, I = unpack(ids) + local a: Column, b: Column, c: Column, d: Column + local e: Column, f: Column, g: Column, h: Column + + local world_query_iter_next + local columns: { Column } + local entities: { i53 } + local i: number + local archetype: Archetype + local records: { ArchetypeRecord } + + if not B then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A].column] + end + + local row = i + i -= 1 + + return entityId, a[row] + end + elseif not C then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + end + + local row = i + i -= 1 + + return entityId, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + end + + local row = i + i -= 1 + + return entityId, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + end + + local row = i + i -= 1 + + return entityId, a[row], b[row], c[row], d[row] + end + else + local queryOutput = {} + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + local records = archetype.records + + if not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end + end + + local row = i + i -= 1 + + if not F then + return entityId, a[row], b[row], c[row], d[row], e[row] + elseif not G then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row] + elseif not H then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + elseif not I then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + + local records = archetype.records + for j, id in ids do + queryOutput[j] = columns[records[id].column][row] + end + + return entityId, unpack(queryOutput) + end + end + + local function cached_query_iter() + lastArchetype = 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return NOOP + end + entities = archetype.entities + i = #entities + records = archetype.records + columns = archetype.columns + if not B then + a = columns[records[A].column] + elseif not C then + a = columns[records[A].column] + b = columns[records[B].column] + elseif not D then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + elseif not E then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + elseif not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end + + return world_query_iter_next + end + + local cached_query = query :: any + cached_query.archetypes = query_archetypes + cached_query.__iter = cached_query_iter + cached_query.iter = cached_query_iter + setmetatable(cached_query, cached_query) + return cached_query +end + local Query = {} Query.__index = Query Query.__iter = query_iter @@ -1490,6 +1858,7 @@ Query.iter = query_iter_init Query.without = query_without Query.with = query_with Query.archetypes = query_archetypes +Query.cached = query_cached local function world_query(world: World, ...) local compatible_archetypes = {} @@ -1502,10 +1871,16 @@ local function world_query(world: World, ...) local idr: IdRecord? local componentIndex = world.componentIndex + local q = setmetatable({ + ids = ids, + compatible_archetypes = compatible_archetypes, + world = world, + }, Query) + for _, id in ids do local map = componentIndex[id] if not map then - return EMPTY_QUERY + return q end if idr == nil or map.size < idr.size then @@ -1514,7 +1889,7 @@ local function world_query(world: World, ...) end if not idr then - return EMPTY_QUERY + return q end for archetype_id in idr.cache do @@ -1542,15 +1917,6 @@ local function world_query(world: World, ...) compatible_archetypes[length] = compatibleArchetype end - if length == 0 then - return EMPTY_QUERY - end - - local q = setmetatable({ - compatible_archetypes = compatible_archetypes, - ids = ids, - }, Query) :: any - return q end @@ -1736,6 +2102,7 @@ function World.new() nextComponentId = 0 :: number, nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, + observerable = {}, }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}, "") @@ -1781,7 +2148,7 @@ type function ecs_entity_t(entity) return entity:components()[2]:readproperty(types.singleton("__T")) end -export type function Pair(first, second) +type function Pair(first, second) local thing = first:components()[2] if thing:readproperty(types.singleton("__T")):is("nil") then @@ -1797,15 +2164,38 @@ export type Entity = number & { __T: T } type Iter = (query: Query) -> () -> (Entity, T...) -type Query = typeof(setmetatable({}, { +export type Query = typeof(setmetatable({}, { __iter = (nil :: any) :: Iter, })) & { iter: Iter, - with: (self: Query, ...i53) -> Query, - without: (self: Query, ...i53) -> Query, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, +} + +type QueryInner = { + compatible_archetypes: { Archetype }, + filter_with: { i53 }?, + filter_without: { i53 }?, + ids: { i53 }, + world: {}, -- Downcasted to be serializable by the analyzer + next: () -> Item +} + +type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, } +type function ecs_partial_t(ty) + local output = types.newtable() + for k, v in ty:properties() do + output:setproperty(k, types.unionof(v.write, types.singleton(nil))) + end + return output +end + export type World = { archetypeIndex: { [string]: Archetype }, archetypes: Archetypes, @@ -1816,6 +2206,8 @@ export type World = { nextComponentId: number, nextEntityId: number, nextArchetypeId: number, + + observerable: { [i53]: { [i53]: { { query: Query } } } }, } & { --- Creates a new entity entity: (self: World) -> Entity, diff --git a/test/tests.luau b/test/tests.luau index 70ce8c0d..8da151f1 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -63,6 +63,10 @@ local function debug_world_inspect(world: World) } end +local function name(world, e) + return world:get(e, jecs.Name) +end + TEST("archetype", function() local archetype_append_to_records = jecs.archetype_append_to_records local id_record_ensure = jecs.id_record_ensure @@ -359,6 +363,39 @@ TEST("world:add()", function() end) TEST("world:query()", function() + do CASE "cached" + local world = world_new() + local Foo = world:component() + local Bar = world:component() + local Baz = world:component() + local e = world:entity() + local q = world:query(Foo, Bar):without(Baz):cached() + world:set(e, Foo, true) + world:set(e, Bar, false) + local i = 0 + + for _, e in q:iter() do + i=1 + end + CHECK(i == 1) + for _, e in q:iter() do + i=2 + end + CHECK(i == 2) + for _, e in q do + i=3 + end + CHECK(i == 3) + for _, e in q do + i=4 + end + CHECK(i == 4) + + CHECK(#q:archetypes() == 1) + CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")])) + world:delete(Foo) + CHECK(#q:archetypes() == 0) + end do CASE("multiple iter") local world = jecs.World.new() local A = world:component() @@ -814,40 +851,6 @@ TEST("world:query()", function() CHECK(withoutCount == 0) end - do - CASE("Empty Query") - do - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local e1 = world:entity() - world:add(e1, A) - - local query = world:query(B) - CHECK(query:without() == query) - CHECK(query:with() == query) - -- They always return the same EMPTY_LIST - CHECK(query:archetypes() == world:query(B):archetypes()) - end - - do - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local e1 = world:entity() - world:add(e1, A) - - local count = 0 - for id in world:query(B) do - count += 1 - end - - CHECK(count == 0) - end - end - do CASE("without") do