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,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<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()")
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
}

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