mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Add scheduler to demo (#106)
* Add example to make scheduler with phases * Add more builtin phases
This commit is contained in:
parent
3d8d71b48d
commit
f220b95d2f
12 changed files with 279 additions and 101 deletions
BIN
demo.rbxl
Normal file
BIN
demo.rbxl
Normal file
Binary file not shown.
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue