Add scheduler to demo (#106)

* Add example to make scheduler with phases

* Add more builtin phases
This commit is contained in:
Marcus 2024-08-21 17:57:40 +02:00 committed by GitHub
parent 3d8d71b48d
commit f220b95d2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 279 additions and 101 deletions

BIN
demo.rbxl Normal file

Binary file not shown.

View file

@ -1,9 +1,14 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs) local jecs = require(game:GetService("ReplicatedStorage").ecs)
local world = require(script.world) :: jecs.World local world = require(script.world) :: jecs.World
export type World = jecs.World export type World = jecs.World
local scheduler = require(script.scheduler)
export type Scheduler = scheduler.Scheduler
local std = { local std = {
ChangeTracker = require(script.changetracker), ChangeTracker = require(script.changetracker),
Scheduler = require(script.scheduler), Scheduler = scheduler.new(world),
bt = require(script.bt), bt = require(script.bt),
collect = require(script.collect), collect = require(script.collect),
components = require(script.components), components = require(script.components),

View file

@ -1,64 +1,207 @@
local function panic(str) --!native
-- We don't want to interrupt the loop when we error --!optimize 2
task.spawn(error, str) local jecs = require(game:GetService("ReplicatedStorage").ecs)
end local pair = jecs.pair
type World = jecs.World
type Entity<T=nil> = jecs.Entity<T>
type System = {
callback: (world: World) -> ()
}
type Systems = { System }
type Events = {
RenderStepped: Systems,
Heartbeat: Systems
}
export type Scheduler = {
components: {
Disabled: Entity,
System: Entity<System>,
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 system
local dt 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 function run()
local name = systemsNames[system] debug.profilebegin(system.name)
system.callback(dt)
debug.profilebegin(name)
debug.setmemorycategory(name)
system(dt)
debug.profileend() debug.profileend()
end 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) if not event then continue end
debug.profilebegin("loop()") 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 local didNotYield, why = xpcall(function()
system = systems[i] for _ in run do end
end, debug.traceback)
dt = sinceLastFrame if didNotYield then
continue
end
local didNotYield, why = xpcall(function() if string.find(why, "thread is not yieldable") then
for _ in run do end panic("Not allowed to yield in the systems.")
end, debug.traceback) else
panic(why)
end
end
debug.profileend()
end)
end
return connections
end
if didNotYield then local function scheduler_collect_systems_under_phase_recursive(systems, phase)
continue for _, system in world:query(System):with(pair(DependsOn, phase)) do
end 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 local function scheduler_collect_systems_under_event(event)
N -= 1 local systems = {}
local name = table.remove(systems, i) scheduler_collect_systems_under_phase_recursive(systems, event)
panic("Not allowed to yield in the systems." return systems
.. "\n" end
.. `System: {name} has been ejected`
) local function scheduler_collect_systems_all()
else local systems = {}
panic(why) for phase, event in world:query(Event):with(Phase) do
end 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 end
debug.profileend() world:add(Heartbeat, Phase)
debug.resetmemorycategory() world:set(Heartbeat, Event, RunService.Heartbeat)
end
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 end
return Scheduler
return {
new = scheduler_new
}

View file

@ -1,4 +1,8 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = require(ReplicatedStorage.std) local std = require(ReplicatedStorage.std)
local loop = std.Scheduler(unpack(script.Parent.systems:GetChildren())) local scheduler = std.Scheduler
game:GetService("RunService").Heartbeat:Connect(loop) for _, module in script.Parent.systems:GetChildren() do
require(module)(scheduler)
end
local events = scheduler.collect.all()
scheduler.systems.begin(events)

View file

@ -1,4 +1,7 @@
--!optimize 2
--!native
--!strict --!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(game:GetService("ServerScriptService").net) local blink = require(game:GetService("ServerScriptService").net)
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
@ -15,25 +18,22 @@ local Player = cts.Player
local Character = cts.Character local Character = cts.Character
local function mobsMove(dt: number) 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 for mob, cf, v in world:query(Transform, Velocity):with(Mob):iter() do
local p = cf.Position local p = cf.Position
local target local target
local closest
for playerId, character in players:iter() do for _, pos in targets do
local pos = (character.PrimaryPart::Part).Position local distance = (p - pos).Magnitude
if true then if not target or distance < closest then
target = pos
break
end
if not target then
target = pos
elseif (p - pos).Magnitude
< (p - target).Magnitude
then
target = pos target = pos
closest = distance
end end
end end
@ -47,4 +47,7 @@ local function mobsMove(dt: number)
end end
end end
return mobsMove return function(scheduler: std.Scheduler)
return scheduler.systems.new(mobsMove,
scheduler.phases.Heartbeat)
end

View file

@ -39,4 +39,7 @@ local function players()
end end
end end
return players return function(scheduler: std.Scheduler)
return scheduler.systems.new(players,
scheduler.phases.Heartbeat)
end

View file

@ -27,4 +27,7 @@ local function spawnMobs()
end end
end end
return spawnMobs return function(scheduler: std.Scheduler)
return scheduler.systems.new(spawnMobs,
scheduler.phases.Heartbeat)
end

View file

@ -1,6 +1,9 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = require(ReplicatedStorage.std) 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()) scheduler.systems.begin(events)
local loop = std.Scheduler(unpack(script.Parent:WaitForChild("systems"):GetChildren()))
game:GetService("RunService").Heartbeat:Connect(loop)

View file

@ -14,4 +14,7 @@ local function move(dt: number)
end end
end end
return move return function(scheduler: std.Scheduler)
return scheduler.systems.new(move,
scheduler.phases.RenderStepped)
end

View file

@ -22,7 +22,9 @@ local function syncMobs()
:set(cts.Model, model) :set(cts.Model, model)
:add(cts.Mob) :add(cts.Mob)
end end
end end
return syncMobs return function(scheduler: std.Scheduler)
return scheduler.systems.new(syncMobs,
scheduler.phases.RenderStepped)
end

View file

@ -13,4 +13,7 @@ local function syncTransforms()
end end
end end
return syncTransforms return function(scheduler: std.Scheduler)
return scheduler.systems.new(syncTransforms,
scheduler.phases.RenderStepped)
end

View file

@ -1264,11 +1264,12 @@ TEST("scheduler", function()
DependsOn: Entity DependsOn: Entity
}, },
collect: {
under_event: (event: Entity) -> Systems,
all: () -> Events
},
systems: { systems: {
collect: {
under_event: (event: Entity) -> Systems,
all: () -> Events
},
run: (events: Events) -> (), run: (events: Events) -> (),
new: (callback: (world: World) -> (), phase: Entity) -> Entity new: (callback: (world: World) -> (), phase: Entity) -> Entity
}, },
@ -1287,8 +1288,20 @@ TEST("scheduler", function()
local System local System
local DependsOn local DependsOn
local Phase local Phase
local Event
local RenderStepped local RenderStepped
local Heartbeat 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) local function scheduler_collect_systems_under_phase_recursive(systems, phase)
for _, system in world:query(System):with(pair(DependsOn, phase)) do for _, system in world:query(System):with(pair(DependsOn, phase)) do
@ -1306,24 +1319,13 @@ TEST("scheduler", function()
end end
local function scheduler_collect_systems_all() local function scheduler_collect_systems_all()
local systems = { local systems = {}
RenderStepped = scheduler_collect_systems_under_event( for phase in world:query(Phase, Event) do
RenderStepped), systems[phase] = scheduler_collect_systems_under_event(phase)
Heartbeat = scheduler_collect_systems_under_event( end
Heartbeat)
}
return systems return systems
end 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 function scheduler_phase_new(after)
local phase = world:entity() local phase = world:entity()
world:add(phase, Phase) world:add(phase, Phase)
@ -1345,12 +1347,15 @@ TEST("scheduler", function()
System = world:component() System = world:component()
Phase = world:component() Phase = world:component()
DependsOn = world:component() DependsOn = world:component()
Event = world:component()
RenderStepped = world:component() RenderStepped = world:component()
Heartbeat = world:component() Heartbeat = world:component()
world:add(RenderStepped, Phase) world:add(RenderStepped, Phase)
world:add(RenderStepped, Event)
world:add(Heartbeat, Phase) world:add(Heartbeat, Phase)
world:add(Heartbeat, Event)
return { return {
phase = scheduler_phase_new, phase = scheduler_phase_new,
@ -1371,13 +1376,14 @@ TEST("scheduler", function()
System = System, System = System,
}, },
collect = {
under_event = scheduler_collect_systems_under_event,
all = scheduler_collect_systems_all
},
systems = { systems = {
run = scheduler_run_systems,
collect = {
under_event = scheduler_collect_systems_under_event,
all = scheduler_collect_systems_all
},
new = scheduler_systems_new, new = scheduler_systems_new,
run = scheduler_systems_run
} }
} }
end end
@ -1432,7 +1438,7 @@ TEST("scheduler", function()
createSystem(hit, Collisions) createSystem(hit, Collisions)
createSystem(move, Physics) createSystem(move, Physics)
local events = scheduler.systems.collect.all() local events = scheduler.collect.all()
scheduler.systems.run(events) scheduler.systems.run(events)
order ..= "->END" order ..= "->END"
@ -1465,20 +1471,20 @@ TEST("scheduler", function()
createSystem(move, Physics) createSystem(move, Physics)
createSystem(camera, Render) createSystem(camera, Render)
local systems = scheduler.systems.collect.under_event(Collisions) local systems = scheduler.collect.under_event(Collisions)
CHECK(#systems == 1) CHECK(#systems == 1)
CHECK(systems[1].callback == hit) CHECK(systems[1].callback == hit)
systems = scheduler.systems.collect.under_event(Physics) systems = scheduler.collect.under_event(Physics)
CHECK(#systems == 2) CHECK(#systems == 2)
systems = scheduler.systems.collect.under_event(Heartbeat) systems = scheduler.collect.under_event(Heartbeat)
CHECK(#systems == 2) CHECK(#systems == 2)
systems = scheduler.systems.collect.under_event(Render) systems = scheduler.collect.under_event(Render)
CHECK(#systems == 1) CHECK(#systems == 1)
CHECK(systems[1].callback == camera) CHECK(systems[1].callback == camera)