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,38 +1,86 @@
--!native
--!optimize 2
local jecs = require(game:GetService("ReplicatedStorage").ecs)
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 system
local dt
local function run()
debug.profilebegin(system.name)
system.callback(dt)
debug.profileend()
end
local function panic(str) local function panic(str)
-- We don't want to interrupt the loop when we error -- We don't want to interrupt the loop when we error
task.spawn(error, str) task.spawn(error, str)
end end
local function begin(events)
local connections = {}
for event, systems in events do
local function Scheduler(...) if not event then continue end
local systems = { ... } local event_name = tostring(event)
local systemsNames = {} connections[event] = event:Connect(function(last)
local N = #systems debug.profilebegin(event_name)
local system for _, sys in systems do
local dt system = sys
dt = last
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.profileend()
end
local function loop(sinceLastFrame)
debug.profilebegin("loop()")
for i = N, 1, -1 do
system = systems[i]
dt = sinceLastFrame
local didNotYield, why = xpcall(function() local didNotYield, why = xpcall(function()
for _ in run do end for _ in run do end
@ -43,22 +91,117 @@ local function Scheduler(...)
end end
if string.find(why, "thread is not yieldable") then if string.find(why, "thread is not yieldable") then
N -= 1 panic("Not allowed to yield in the systems.")
local name = table.remove(systems, i)
panic("Not allowed to yield in the systems."
.. "\n"
.. `System: {name} has been ejected`
)
else else
panic(why) panic(why)
end end
end end
debug.profileend() debug.profileend()
debug.resetmemorycategory() end)
end
return connections
end end
return loop 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 end
return Scheduler 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
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)
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 {
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
}, },
systems: {
collect: { collect: {
under_event: (event: Entity) -> Systems, under_event: (event: Entity) -> Systems,
all: () -> Events all: () -> Events
}, },
systems: {
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,
}, },
systems = {
run = scheduler_run_systems,
collect = { collect = {
under_event = scheduler_collect_systems_under_event, under_event = scheduler_collect_systems_under_event,
all = scheduler_collect_systems_all all = scheduler_collect_systems_all
}, },
systems = {
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)