diff --git a/demo.rbxl b/demo.rbxl new file mode 100644 index 0000000..b0961d8 Binary files /dev/null and b/demo.rbxl differ diff --git a/demo/src/ReplicatedStorage/std/init.luau b/demo/src/ReplicatedStorage/std/init.luau index 0538efa..ae777b4 100644 --- a/demo/src/ReplicatedStorage/std/init.luau +++ b/demo/src/ReplicatedStorage/std/init.luau @@ -1,9 +1,14 @@ 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 = require(script.scheduler), + Scheduler = scheduler.new(world), bt = require(script.bt), collect = require(script.collect), components = require(script.components), diff --git a/demo/src/ReplicatedStorage/std/scheduler.luau b/demo/src/ReplicatedStorage/std/scheduler.luau index c0aac28..62ec66f 100644 --- a/demo/src/ReplicatedStorage/std/scheduler.luau +++ b/demo/src/ReplicatedStorage/std/scheduler.luau @@ -1,64 +1,207 @@ -local function panic(str) - -- We don't want to interrupt the loop when we error - task.spawn(error, str) -end +--!native +--!optimize 2 +local jecs = require(game:GetService("ReplicatedStorage").ecs) +local pair = jecs.pair +type World = jecs.World +type Entity = jecs.Entity + +type System = { + callback: (world: World) -> () +} + +type Systems = { System } + + +type Events = { + RenderStepped: Systems, + Heartbeat: Systems +} + +export type Scheduler = { + components: { + Disabled: Entity, + System: Entity, + Phase: Entity, + DependsOn: Entity + }, + + collect: { + under_event: (event: Entity) -> Systems, + all: () -> Events + }, + + systems: { + begin: (events: Events) -> (), + new: (callback: (dt: number) -> (), phase: Entity) -> Entity + }, + + phases: { + RenderStepped: Entity, + Heartbeat: Entity + }, + + phase: (after: Entity) -> Entity +} + +local scheduler_new: (w: World) -> Scheduler + +do + local world + local Disabled + local System + local DependsOn + local Phase + local Event + local Name + + local RenderStepped + local Heartbeat + local PreAnimation + local PreSimulation -local function Scheduler(...) - local systems = { ... } - local systemsNames = {} - local N = #systems local system local dt - - for i, module in systems do - local sys = require(module) - systems[i] = sys - local file, line = debug.info(2, "sl") - systemsNames[sys] = `{file}->::{line}::->{debug.info(sys, "n")}` - end - local function run() - local name = systemsNames[system] - - debug.profilebegin(name) - debug.setmemorycategory(name) - system(dt) + debug.profilebegin(system.name) + system.callback(dt) debug.profileend() 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) + local connections = {} + for event, systems in events do - local function loop(sinceLastFrame) - debug.profilebegin("loop()") + if not event then continue end + local event_name = tostring(event) + connections[event] = event:Connect(function(last) + debug.profilebegin(event_name) + for _, sys in systems do + system = sys + dt = last - for i = N, 1, -1 do - system = systems[i] + local didNotYield, why = xpcall(function() + for _ in run do end + end, debug.traceback) - dt = sinceLastFrame + if didNotYield then + continue + end - local didNotYield, why = xpcall(function() - for _ in run do end - end, debug.traceback) + if string.find(why, "thread is not yieldable") then + panic("Not allowed to yield in the systems.") + else + panic(why) + end + end + debug.profileend() + end) + end + return connections + end - if didNotYield then - continue - end + local function scheduler_collect_systems_under_phase_recursive(systems, phase) + for _, system in world:query(System):with(pair(DependsOn, phase)) do + table.insert(systems, system) + end + for dependant in world:query(Phase):with(pair(DependsOn, phase)) do + scheduler_collect_systems_under_phase_recursive(systems, dependant) + end + end - if string.find(why, "thread is not yieldable") then - N -= 1 - local name = table.remove(systems, i) - panic("Not allowed to yield in the systems." - .. "\n" - .. `System: {name} has been ejected` - ) - else - panic(why) - end + 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 systems = {} + for phase, event in world:query(Event):with(Phase) do + systems[event] = scheduler_collect_systems_under_event(phase) + end + return systems + end + + local function scheduler_phase_new(after) + local phase = world:entity() + world:add(phase, Phase) + local dependency = pair(DependsOn, after) + 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 + end + + function scheduler_new(w) + 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 - debug.profileend() - debug.resetmemorycategory() - end + world:add(Heartbeat, Phase) + world:set(Heartbeat, Event, RunService.Heartbeat) - return loop + world:add(PreSimulation, Phase) + world:set(PreSimulation, Event, RunService.PreSimulation) + + world:add(PreAnimation, Phase) + world:set(PreAnimation, Event, RunService.PreAnimation) + + 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 end -return Scheduler + +return { + new = scheduler_new +} diff --git a/demo/src/ServerScriptService/main.server.luau b/demo/src/ServerScriptService/main.server.luau index 6a521b2..2cb8378 100644 --- a/demo/src/ServerScriptService/main.server.luau +++ b/demo/src/ServerScriptService/main.server.luau @@ -1,4 +1,8 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local std = require(ReplicatedStorage.std) -local loop = std.Scheduler(unpack(script.Parent.systems:GetChildren())) -game:GetService("RunService").Heartbeat:Connect(loop) +local scheduler = std.Scheduler +for _, module in script.Parent.systems:GetChildren() do + require(module)(scheduler) +end +local events = scheduler.collect.all() +scheduler.systems.begin(events) diff --git a/demo/src/ServerScriptService/systems/mobsMove.luau b/demo/src/ServerScriptService/systems/mobsMove.luau index 89c1935..45b44dd 100644 --- a/demo/src/ServerScriptService/systems/mobsMove.luau +++ b/demo/src/ServerScriptService/systems/mobsMove.luau @@ -1,4 +1,7 @@ +--!optimize 2 +--!native --!strict + local ReplicatedStorage = game:GetService("ReplicatedStorage") local blink = require(game:GetService("ServerScriptService").net) local jecs = require(ReplicatedStorage.ecs) @@ -15,25 +18,22 @@ local Player = cts.Player local Character = cts.Character local function mobsMove(dt: number) - local players = world:query(Character):with(Player) + local targets = {} + for _, character in world:query(Character):with(Player):iter() do + table.insert(targets, (character.PrimaryPart :: Part).Position) + end for mob, cf, v in world:query(Transform, Velocity):with(Mob):iter() do local p = cf.Position local target + local closest - for playerId, character in players:iter() do - local pos = (character.PrimaryPart::Part).Position - if true then - target = pos - break - end - if not target then - target = pos - elseif (p - pos).Magnitude - < (p - target).Magnitude - then + for _, pos in targets do + local distance = (p - pos).Magnitude + if not target or distance < closest then target = pos + closest = distance end end @@ -47,4 +47,7 @@ local function mobsMove(dt: number) end end -return mobsMove +return function(scheduler: std.Scheduler) + return scheduler.systems.new(mobsMove, + scheduler.phases.Heartbeat) +end diff --git a/demo/src/ServerScriptService/systems/players.luau b/demo/src/ServerScriptService/systems/players.luau index beacce9..ab7dc4a 100644 --- a/demo/src/ServerScriptService/systems/players.luau +++ b/demo/src/ServerScriptService/systems/players.luau @@ -39,4 +39,7 @@ local function players() end end -return players +return function(scheduler: std.Scheduler) + return scheduler.systems.new(players, + scheduler.phases.Heartbeat) +end diff --git a/demo/src/ServerScriptService/systems/spawnMobs.luau b/demo/src/ServerScriptService/systems/spawnMobs.luau index 748a03d..9bbbb90 100644 --- a/demo/src/ServerScriptService/systems/spawnMobs.luau +++ b/demo/src/ServerScriptService/systems/spawnMobs.luau @@ -27,4 +27,7 @@ local function spawnMobs() end end -return spawnMobs +return function(scheduler: std.Scheduler) + return scheduler.systems.new(spawnMobs, + scheduler.phases.Heartbeat) +end diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/main.client.luau b/demo/src/StarterPlayer/StarterPlayerScripts/main.client.luau index 1d7b555..9a6bb0d 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/main.client.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/main.client.luau @@ -1,6 +1,9 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local std = require(ReplicatedStorage.std) +local scheduler = std.Scheduler +for _, module in script.Parent:WaitForChild("systems"):GetChildren() do + require(module)(scheduler) +end +local events = scheduler.collect.all() -print(script.Parent:WaitForChild("systems"):GetChildren()) -local loop = std.Scheduler(unpack(script.Parent:WaitForChild("systems"):GetChildren())) -game:GetService("RunService").Heartbeat:Connect(loop) +scheduler.systems.begin(events) diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau index a3270b9..84f9287 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/move.luau @@ -14,4 +14,7 @@ local function move(dt: number) end end -return move +return function(scheduler: std.Scheduler) + return scheduler.systems.new(move, + scheduler.phases.RenderStepped) +end diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau index 0c6e89f..bf73e00 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncMobs.luau @@ -22,7 +22,9 @@ local function syncMobs() :set(cts.Model, model) :add(cts.Mob) end - end -return syncMobs +return function(scheduler: std.Scheduler) + return scheduler.systems.new(syncMobs, + scheduler.phases.RenderStepped) +end diff --git a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncTransforms.luau b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncTransforms.luau index 6fa3064..01ba86e 100644 --- a/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncTransforms.luau +++ b/demo/src/StarterPlayer/StarterPlayerScripts/systems/syncTransforms.luau @@ -13,4 +13,7 @@ local function syncTransforms() end end -return syncTransforms +return function(scheduler: std.Scheduler) + return scheduler.systems.new(syncTransforms, + scheduler.phases.RenderStepped) +end diff --git a/test/tests.luau b/test/tests.luau index 4c0692d..31f2614 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1264,11 +1264,12 @@ TEST("scheduler", function() DependsOn: Entity }, + collect: { + under_event: (event: Entity) -> Systems, + all: () -> Events + }, + systems: { - collect: { - under_event: (event: Entity) -> Systems, - all: () -> Events - }, run: (events: Events) -> (), new: (callback: (world: World) -> (), phase: Entity) -> Entity }, @@ -1287,8 +1288,20 @@ TEST("scheduler", function() local System local DependsOn local Phase + local Event local RenderStepped local Heartbeat + local Name + + local function scheduler_systems_run(events) + for _, system in events[RenderStepped] do + system.callback() + end + print(Heartbeat, events[Heartbeat]) + for _, system in events[Heartbeat] do + system.callback() + end + end local function scheduler_collect_systems_under_phase_recursive(systems, phase) for _, system in world:query(System):with(pair(DependsOn, phase)) do @@ -1306,24 +1319,13 @@ TEST("scheduler", function() end local function scheduler_collect_systems_all() - local systems = { - RenderStepped = scheduler_collect_systems_under_event( - RenderStepped), - Heartbeat = scheduler_collect_systems_under_event( - Heartbeat) - } + local systems = {} + for phase in world:query(Phase, Event) do + systems[phase] = scheduler_collect_systems_under_event(phase) + end return systems end - local function scheduler_run_systems(events) - for _, system in events.RenderStepped do - system.callback(world) - end - for _, system in events.Heartbeat do - system.callback(world) - end - end - local function scheduler_phase_new(after) local phase = world:entity() world:add(phase, Phase) @@ -1345,12 +1347,15 @@ TEST("scheduler", function() System = world:component() Phase = world:component() DependsOn = world:component() + Event = world:component() RenderStepped = world:component() Heartbeat = world:component() world:add(RenderStepped, Phase) + world:add(RenderStepped, Event) world:add(Heartbeat, Phase) + world:add(Heartbeat, Event) return { phase = scheduler_phase_new, @@ -1371,13 +1376,14 @@ TEST("scheduler", function() System = System, }, + collect = { + under_event = scheduler_collect_systems_under_event, + all = scheduler_collect_systems_all + }, + systems = { - run = scheduler_run_systems, - collect = { - under_event = scheduler_collect_systems_under_event, - all = scheduler_collect_systems_all - }, new = scheduler_systems_new, + run = scheduler_systems_run } } end @@ -1432,7 +1438,7 @@ TEST("scheduler", function() createSystem(hit, Collisions) createSystem(move, Physics) - local events = scheduler.systems.collect.all() + local events = scheduler.collect.all() scheduler.systems.run(events) order ..= "->END" @@ -1465,20 +1471,20 @@ TEST("scheduler", function() createSystem(move, Physics) createSystem(camera, Render) - local systems = scheduler.systems.collect.under_event(Collisions) + local systems = scheduler.collect.under_event(Collisions) CHECK(#systems == 1) CHECK(systems[1].callback == hit) - systems = scheduler.systems.collect.under_event(Physics) + systems = scheduler.collect.under_event(Physics) CHECK(#systems == 2) - systems = scheduler.systems.collect.under_event(Heartbeat) + systems = scheduler.collect.under_event(Heartbeat) CHECK(#systems == 2) - systems = scheduler.systems.collect.under_event(Render) + systems = scheduler.collect.under_event(Render) CHECK(#systems == 1) CHECK(systems[1].callback == camera)