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 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),

View file

@ -1,38 +1,86 @@
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<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 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()")
for i = N, 1, -1 do
system = systems[i]
dt = sinceLastFrame
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
local didNotYield, why = xpcall(function()
for _ in run do end
@ -43,22 +91,117 @@ local function Scheduler(...)
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`
)
panic("Not allowed to yield in the systems.")
else
panic(why)
end
end
debug.profileend()
debug.resetmemorycategory()
end)
end
return connections
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
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 Scheduler
return {
new = scheduler_new
}

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1264,11 +1264,12 @@ TEST("scheduler", function()
DependsOn: Entity
},
systems: {
collect: {
under_event: (event: Entity) -> Systems,
all: () -> Events
},
systems: {
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,
},
systems = {
run = scheduler_run_systems,
collect = {
under_event = scheduler_collect_systems_under_event,
all = scheduler_collect_systems_all
},
systems = {
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)