Releases: YetAnotherClown/planck
v0.3.0-alpha.1
Planck v0.3.0-alpha.1
Caution
Planck v0.3.0-alpha.1 is an alpha version contains breaking changes from v0.2.0. If you're currently using v0.2.0, read the Breaking Changes section before installing this version so you can migrate your code.
Note
Planck v0.3.0 now has official Roblox-TS typings for the follow packages:
@rbxts/planck@rbxts/planck-matter-hooks@rbxts/planck-runservice@rbxts/planck-jabby@rbxts/planck-matter-debugger
Initializer Systems
Initializer systems allow you to do one-time setup when a system first runs. As an example, you can cache a Jecs query, manage connections, or do other setup before running your system.
local function renderSystem(world)
-- This runs once on first execution
local renderables = world:query(Transform, Model):cached()
-- Return the function that runs on each subsequent execution
return function(world)
for id, transform, model in renderables do
render(transform, model)
end
end
end
return renderSystemYou can also return a 'cleanup function' to handle any cleanup when the system is removed/replaced.
local function networkSystem(world)
local connection = Players.PlayerAdded:Connect(function(player)
-- Handle player joining
end)
return function(world)
-- Runtime logic
end, function(world)
-- Cleanup runs when system is removed
connection:Disconnect()
end
end
return networkSystemBreaking Changes
System Returns
It is now expected that systems return:
- Either
nilor void - Or a valid return for an Initializer System
When creating an Initializer System, you can return one or two functions of type (U...) -> (). The first return will be the actual system that is ran every frame, while the second will be the cleanup function called when the system is added/removed.
You can also return a table of type:
{
system: (U...) -> (),
cleanup: ((U...) -> ())?,
}Changes
Added
- Added 'Initializer Systems' to allow setup and cleanup of systems #30
Changed
- Planck-Jabby: Moved Startup Systems to their own Scheduler Applet #23
- Breaking: It is now expected for systems to return either
nil | ()or to return a system or cleanup function #30
Fixed
v0.2.0
Planck v0.2.0
Caution
Planck v0.2.0 contains a lot of breaking changes from v0.1.0. If you're currently using v0.1.0, read the Breaking Changes section before installing this version so you can migrate your code.
Note
Planck v0.2.0 does not have official Roblox-TS typings. Community maintained typings are available below:
New and Improved Documentation
The Documentation Site now contains a well detailed Getting Started guide for learning all the core concepts of Planck, a Design Guide for designing games with Planck for more advanced users, and Setup Guides for Jecs and Matter.
Built-in Common Conditions
Planck now provides built-in conditions for use within your Systems or as Run Conditions. These common conditions are functions which generate closures to check if a condition is true or false, some of them provide additional uses that you can use within your systems as well.
OnEvent
The onEvent condition checks for new events each time the conditional function is called. It also provides an additional function to collect the new events.
This can be used in your systems similarly to the Matter.useEvent() hook or a simple collect function.
local onEvent = Planck.onEvent
local hasNewEvent, collectEvents = onEvent(Players.PlayerAdded)
local function handlePlayers()
for _, player in collectEvents() do
-- ...
end
end
return {
system = handlePlayers,
runConditions = { hasNewEvent },
-- This also works, if you don't want to collect the events
runConditions = { onEvent(Players.PlayerAdded) },
}TimePassed (Throttle)
The timePassed or throttle condition checks if the given time has passed. This is similar to the Matter.useThrottle() hook or a simple interval function.
local timePassed = Planck.timePassed
local hasTimePassed = timePassed(10)
local function throttled()
if hasTimePassed() then
-- We can use this function in our systems
end
-- Only runs after 10 seconds because of our condition
end
return {
system = throttled,
runConditions = { hasTimePassed },
-- Or if we don't want to use it in systems
runConditions = { timePassed(10) },
}RunOnce
The runOnce condition will only return true once, and then always false afterwards. This is useful for when you need to create startup logic. It is important to note that while Startup Phases use this internally, the Scheduler ensures they always run before other systems.
local runOnce = Planck.runOnce
local hasRanOnce = runOnce()
local function someSystem()
if not hasRanOnce()
-- Some startup logic
end
-- Or we just want the whole system to run only once
end
return {
system = someSystem,
runConditions = { hasRanOnce },
-- If you only want a 'startup' system
runConditions = { runOnce() },
}IsNot
The isNot condition inverses other conditions.
RunService Plugin
This Plugin replaces the built-in Phases from v0.1.0 in an effort to also make Planck runtime agnostic. In the future, any feature which interacts with the Roblox Engine will be a separate Plugin instead of apart of the core library.
Pipelines
Each RunService Event is now it's own Pipeline,
- PreRender
- PreAnimation
- PreSimulation
- PostSimulation
- Heartbeat
Phases
And it's own Phase, with the exception of Heartbeat which has many Phases.
| Event | Phase |
|---|---|
| PreRender | PreRender |
| PreAnimation | PreAnimation |
| PreSimulation | PreSimulation |
| PostSimulation | PostSimulation |
| Heartbeat | Update |
Heartbeat Phases
- First
- PreUpdate
- Update
- PostUpdate
- Last
Installation
With Wally,
[dependencies]
PlanckRunService = "yetanotherclown/planck-runservice@0.2.0"Better Scheduling w/ Dependency Management
The library has been refactored to now create and manage dependencies between Pipelines and Phases using Adjacency Matrices, and it will now use these dependencies in addition to order of insertion to determine the order in which Pipelines and Phases run in Planck,
You can learn more about the new behavior under the breaking changes section.
Scheduler:insertBefore() and Pipeline:insertBefore()
These two new methods now exist for creating dependencies between two phases. :insertBefore() will make the first Phase/Pipeline depend on the other, meaning it cannot run until the other does.
Breaking Changes
Ordering Priorities
Ordering is no longer determinant on a fixed position assigned to each Phase or Pipeline with :insert() or :insertAfter within an ordered list. Instead, the Scheduler will now order Phases and Pipelines based on their dependencies.
To explain how the ordering now works,
A dependency is any Phase/Pipeline another Phase/Pipeline depends on. A dependent if the Phase/Pipeline that depends on another Phase/Pipeline.
This looks like, insertAfter(dependent, dependency) or insertBefore(dependent, dependency).
- Start with the first Phase/Pipeline inserted
- If this Phase/Pipeline has any dependency, skip it and move onto the next one.
- Add this Phase/Pipeline to the order
- If this Phase/Pipeline has any dependents, repeat this process in order of insertion for each dependent Phase/Pipeline.
- Move onto the next node.
insert(dependent) also now works by setting the last added Phase/Pipeline as a dependency of dependent.
Breaking insertAfter
This change affects Scheduler:insertAfter() and Pipeline:insertAfter(). Because Phases/Pipelines no longer have fixed positions in the order of execution, insertAfter no longer inserts a dependent to be immediately after dependency.
To better demonstrate this, consider this code example:
local myScheduler = Scheduler.new()
:insert(PhaseOne)
:insert(PhaseThree)
:insertAfter(PhaseTwo, PhaseOne)
-- Old Behavior:
-- PhaseOne -> PhaseTwo -> PhaseThree
-- New Behavior:
-- PhaseOne -> PhaseThree -> PhaseTwoThis change is desirable because:
PhaseThreeis not dependent onPhaseTwo(dependencies are defined with:insertAfter())PhaseThreeis inserted beforePhaseTwo, so we respect the implicit order of insertion
If you find your systems breaking because of this change, you should use insertAfter and insertBefore to explicitly define the dependencies of your Phases/Pipelines. You should not rely on insert to manage dependencies.
Built-in RunService Phases Removed
The following RunService Phases are no longer included in the core library, instead they are now available as a plugin.
| Event | Phase(s) |
|---|---|
| PreRender | PreRender |
| PreAnimation | PreAnimation |
| PreSimulation | PreSimulation |
| PostSimulation | PostSimulation |
| Heartbeat | First, PreUpdate, Update, PostUpdate, Last |
When the Plugin is added, the default Phase will be set to Update.
[dependencies]
PlanckRunService = "yetanotherclown/planck-runservice@0.2.0"Replacement of Scheduler:setRunCondition
The method Scheduler:setRunCondition has been replaced with a new method Scheduler:addRunCondition which allows for the addition of multiple run conditions.
With this new method, it is no longer possible to overwrite previous run conditions. Run Conditions should ideally be added upon creation of the Scheduler, and not modified afterwards.
Replacement of SystemTable.runCondition
Now that you can add multiple Run Conditions, a System Table now will take SystemTable.runConditions instead which is an array of run conditions.
local function system()
-- ...
end
local function runIf()
-- ...
end
return {
system = system,
runConditions = { throttle(10), runIf }
}Changes
Added
Scheduler:insertBefore()Pipeline:insertBefore()- Conditions (isNot, runOnce, timePassed, onEvent)
Scheduler:addRunCondition()for adding multiple run conditionsPlanckRunServicePlugin which adds built-in Pipelines and Phases for RunService events- Cleanup methods to the Scheduler class and relevant Plugins, allowing you to cleanup events and the scheduler
- Types for all event related APIs
Changed
- Refactored internals to use Adjacency Matrices for managing ordering and dependencies of Phases/Pipelines
- Breaking: The following methods now create an ordering dependency instead of setting the fixed order of Phases/Pipelines
Scheduler:insert()Scheduler:insertAfter()Pipeline:insert()Pipeline:insertAfter()
- Breaking:
Scheduler:runAll()will no longer run in the exact order that Phases were inserted for Phases bound to events. They will be grouped together, and ran together in order of insertion. - Breaking:
Scheduler:setRunCondition()has been replaced withScheduler:addRunCondition() - Breaking:
SystemTable.runConditionhas been replaced withSystemTable.runConditions - Refactored internals to reuse event logic
- Replaced
Phase.Updateas default phase withDefault - Add an error when passing an empty table in
Scheduler:addSystems() - System errors now provide proper stack traces
- Improved types for all plugins
- Planck-Jabby: Bumped Jabby ...
Planck v0.2.0-rc.3
Planck v0.2.0-rc.3
Caution
Planck v0.2.0-rc.3 contains a lot of breaking changes from v0.1.0. If you're currently using v0.1.0, read the Breaking Changes section before installing this version so you can migrate your code.
New and Improved Documentation
The Documentation Site now contains a well detailed Getting Started guide for learning all the core concepts of Planck, a Design Guide for designing games with Planck for more advanced users, and Setup Guides for Jecs and Matter.
Built-in Common Conditions
Planck now provides built-in conditions for use within your Systems or as Run Conditions. These common conditions are functions which generate closures to check if a condition is true or false, some of them provide additional uses that you can use within your systems as well.
OnEvent
The onEvent condition checks for new events each time the conditional function is called. It also provides an additional function to collect the new events.
This can be used in your systems similarly to the Matter.useEvent() hook or a simple collect function.
local onEvent = Planck.onEvent
local hasNewEvent, collectEvents = onEvent(Players.PlayerAdded)
local function handlePlayers()
for _, player in collectEvents() do
-- ...
end
end
return {
system = handlePlayers,
runConditions = { hasNewEvent },
-- This also works, if you don't want to collect the events
runConditions = { onEvent(Players.PlayerAdded) },
}TimePassed (Throttle)
The timePassed or throttle condition checks if the given time has passed. This is similar to the Matter.useThrottle() hook or a simple interval function.
local timePassed = Planck.timePassed
local hasTimePassed = timePassed(10)
local function throttled()
if hasTimePassed() then
-- We can use this function in our systems
end
-- Only runs after 10 seconds because of our condition
end
return {
system = throttled,
runConditions = { hasTimePassed },
-- Or if we don't want to use it in systems
runConditions = { timePassed(10) },
}RunOnce
The runOnce condition will only return true once, and then always false afterwards. This is useful for when you need to create startup logic. It is important to note that while Startup Phases use this internally, the Scheduler ensures they always run before other systems.
local runOnce = Planck.runOnce
local hasRanOnce = runOnce()
local function someSystem()
if not hasRanOnce()
-- Some startup logic
end
-- Or we just want the whole system to run only once
end
return {
system = someSystem,
runConditions = { hasRanOnce },
-- If you only want a 'startup' system
runConditions = { runOnce() },
}IsNot
The isNot condition inverses other conditions.
RunService Plugin
This Plugin replaces the built-in Phases from v0.1.0 in an effort to also make Planck runtime agnostic. In the future, any feature which interacts with the Roblox Engine will be a separate Plugin instead of apart of the core library.
Pipelines
Each RunService Event is now it's own Pipeline,
- PreRender
- PreAnimation
- PreSimulation
- PostSimulation
- Heartbeat
Phases
And it's own Phase, with the exception of Heartbeat which has many Phases.
| Event | Phase |
|---|---|
| PreRender | PreRender |
| PreAnimation | PreAnimation |
| PreSimulation | PreSimulation |
| PostSimulation | PostSimulation |
| Heartbeat | Update |
Heartbeat Phases
- First
- PreUpdate
- Update
- PostUpdate
- Last
Installation
With Wally,
[dependencies]
PlanckRunService = "yetanotherclown/planck-runservice@v0.2.0-rc.3"Better Scheduling w/ Dependency Management
The library has been refactored to now create and manage dependencies between Pipelines and Phases using Adjacency Matrices, and it will now use these dependencies in addition to order of insertion to determine the order in which Pipelines and Phases run in Planck,
You can learn more about the new behavior under the breaking changes section.
Scheduler:insertBefore() and Pipeline:insertBefore()
These two new methods now exist for creating dependencies between two phases. :insertBefore() will make the first Phase/Pipeline depend on the other, meaning it cannot run until the other does.
Breaking Changes
Ordering Priorities
Ordering is no longer determinant on a fixed position assigned to each Phase or Pipeline with :insert() or :insertAfter within an ordered list. Instead, the Scheduler will now order Phases and Pipelines based on their dependencies.
To explain how the ordering now works,
A dependency is any Phase/Pipeline another Phase/Pipeline depends on. A dependent if the Phase/Pipeline that depends on another Phase/Pipeline.
This looks like, insertAfter(dependent, dependency) or insertBefore(dependent, dependency).
- Start with the first Phase/Pipeline inserted
- If this Phase/Pipeline has any dependency, skip it and move onto the next one.
- Add this Phase/Pipeline to the order
- If this Phase/Pipeline has any dependents, repeat this process in order of insertion for each dependent Phase/Pipeline.
- Move onto the next node.
insert(dependent) also now works by setting the last added Phase/Pipeline as a dependency of dependent.
Breaking insertAfter
This change affects Scheduler:insertAfter() and Pipeline:insertAfter(). Because Phases/Pipelines no longer have fixed positions in the order of execution, insertAfter no longer inserts a dependent to be immediately after dependency.
To better demonstrate this, consider this code example:
local myScheduler = Scheduler.new()
:insert(PhaseOne)
:insert(PhaseThree)
:insertAfter(PhaseTwo, PhaseOne)
-- Old Behavior:
-- PhaseOne -> PhaseTwo -> PhaseThree
-- New Behavior:
-- PhaseOne -> PhaseThree -> PhaseTwoThis change is desirable because:
PhaseThreeis not dependent onPhaseTwo(dependencies are defined with:insertAfter())PhaseThreeis inserted beforePhaseTwo, so we respect the implicit order of insertion
If you find your systems breaking because of this change, you should use insertAfter and insertBefore to explicitly define the dependencies of your Phases/Pipelines. You should not rely on insert to manage dependencies.
Built-in RunService Phases Removed
The following RunService Phases are no longer included in the core library, instead they are now available as a plugin.
| Event | Phase(s) |
|---|---|
| PreRender | PreRender |
| PreAnimation | PreAnimation |
| PreSimulation | PreSimulation |
| PostSimulation | PostSimulation |
| Heartbeat | First, PreUpdate, Update, PostUpdate, Last |
When the Plugin is added, the default Phase will be set to Update.
[dependencies]
PlanckRunService = "yetanotherclown/planck-runservice@v0.2.0-rc.3"Replacement of Scheduler:setRunCondition
The method Scheduler:setRunCondition has been replaced with a new method Scheduler:addRunCondition which allows for the addition of multiple run conditions.
With this new method, it is no longer possible to overwrite previous run conditions. Run Conditions should ideally be added upon creation of the Scheduler, and not modified afterwards.
Replacement of SystemTable.runCondition
Now that you can add multiple Run Conditions, a System Table now will take SystemTable.runConditions instead which is an array of run conditions.
local function system()
-- ...
end
local function runIf()
-- ...
end
return {
system = system,
runConditions = { throttle(10), runIf }
}Changes
Added
Scheduler:insertBefore()Pipeline:insertBefore()- Conditions (isNot, runOnce, timePassed, onEvent)
Scheduler:addRunCondition()for adding multiple run conditionsPlanckRunServicePlugin which adds built-in Pipelines and Phases for RunService events- Cleanup methods to the Scheduler class and relevant Plugins, allowing you to cleanup events and the scheduler
- Types for all event related APIs
Changed
- Refactored internals to use Adjacency Matrices for managing ordering and dependencies of Phases/Pipelines
- Breaking: The following methods now create an ordering dependency instead of setting the fixed order of Phases/Pipelines
Scheduler:insert()Scheduler:insertAfter()Pipeline:insert()Pipeline:insertAfter()
- Breaking:
Scheduler:runAll()will no longer run in the exact order that Phases were inserted for Phases bound to events. They will be grouped together, and ran together in order of insertion. - Breaking:
Scheduler:setRunCondition()has been replaced withScheduler:addRunCondition() - Breaking:
SystemTable.runConditionhas been replaced withSystemTable.runConditions - Refactored internals to reuse event logic
- Replaced
Phase.Updateas default phase withDefault - Add an error when passing an empty table in
Scheduler:addSystems() - System errors now provide proper stack traces
- Improved types for all plugins
Removed
- Built-in RunService Pipelines/Phases, these will be available as a separate plugin
Fixed
- Fallback System name does not contain the line of the system
- Conflict between Matter and Matter Hooks libraries
- Fixed several types
Planck v0.1.0
Planck, an ECS Scheduler
An Agnostic Scheduler, inspired by Bevy Schedules and Flecs Pipelines and Phases.
Installation
You can install Planck with Wally
[dependencies]
Planck = "yetanotherclown/planck@0.1.0"What is Planck?
Planck is a standalone scheduler, which allows you to execute code on specific events, with certain conditions, and in a particular order.
This scheduler is library agnostic, which means that it doesn't matter which ECS library your using or if you're even using an ECS.
You can use this with Jecs, Matter, ECR, and other Luau ECS Libraries.
Does any of this really matter?
Yes, and no.
Your ECS code should be able to run in any order, without any conditions, and without concern for which event it's running on, as long as it is running.
The order of execution, and conditions both serve to optimize your code. Some systems don't need to run every frame, which is why we have conditions.
And the actual order of execution is to reduce latency between changes and effects in your ECS world.
Let's say we have systemA and systemB. systemA modifies data in our world which systemB depends on.
If systemA runs after systemB, then systemB will have to wait a whole frame for the modifications to be made.
This is called being off-by-a-frame, and this is why we care about the order of execution.
Quick Overview
While it's highly suggested you read the documentation, here is a quick overview of Planck's API.
The Scheduler
This is the core of Planck, this is where you add your Systems and set your Phases, Pipelines, and Run Conditions.
local Planck = require("@packages/Planck")
local Scheduler = Planck.Scheduler
local Jecs = require("@packages/Jecs")
local World = Jecs.World
local world = World.new()
local state = {}
local scheduler = Scheduler.new(world, state)Systems
Systems are really simple, they are just functions which run on an event or in a loop.
local function systemA(world, state)
-- ...
end
return systemAAnd to add it to our Scheduler,
-- ...
local systemA = require("@shared/systems/systemA")
local scheduler = Scheduler.new(world, state)
:addSystem(systemA)Phases
Phases are used to split up your frame into different sections, this allows us to schedule our systems to run at different moments of a given frame.
local Planck = require("@packages/Planck")
local Scheduler = Planck.Scheduler
local Phase = Planck.Phase
-- ...
local systemA = require("@shared/systems/systemA")
local myPhase = Phase.new("myPhase")
local scheduler = Scheduler.new(world, state)
:insert(myPhase)
:addSystem(systemA, myPhase)Planck has lots of built-in Phases that should work for most cases.
| Event | Phase |
|---|---|
| PreRender | PreRender |
| PreAnimation | PreAnimation |
| PreSimulation | PreSimulation |
| PostSimulation | PostSimulation |
| Heartbeat | Update |
Pipelines
Pipelines are ordered groups of Phases, they make working with larger collections of Phases (which all run on the same event) easier.
local Phase = Planck.Phase
local Pipeline = Planck.Pipeline
local Scheduler = Planck.Scheduler
local PreUpdate = Phase.new()
local Update = Phase.new()
local PostUpdate = Phase.new()
local UpdatePipeline = Pipeline.new()
:insert(PreUpdate)
:insert(Update)
:insert(PostUpdate)
local scheduler = scheduler.new(world)
:insert(UpdatePipeline, RunService, "Heartbeat")Tip
The UpdatePipeline seen here, already exists in Planck! It's a built-in Pipeline that you can use without any setup.
Phases:
- First
- PreUpdate
- Update
- PostUpdate
- Last
Conditions
When we run all our systems every frame, there are a lot of systems that may not actually need to run. Run Conditions allow us to
run our Systems, Phases and Pipelines only sometimes.
local function condition(world)
if someCondition then
return true
else
return false
end
end
local scheduler = Scheduler.new(world)
:setRunCondition(systemA, condition)
:setRunCondition(somePhase, condition)
:setRunCondition(somePipeline, condition)Conditions can be useful, but you should use them carefully. It's suggested that you read our page on
Conditions to see some useful examples and learn when you should use them.
Inspiration
Planck's API design is heavily influenced by the Bevy Engine, with Schedules, RunConditions, and more.
Planck also draws inspiration from Flecs for Pipelines and Phases.
We're combining the simple, and beloved API of Bevy with the concept of Pipelines and Phases.
Planck v0.2.0-alpha.2
Breaking Changes
Ordering Priorities
Ordering is no longer determinant on a fixed position assigned to each Phase or Pipeline with :insert() or :insertAfter within an ordered list. Instead, the Scheduler will now order Phases and Pipelines based on their dependencies.
To explain how the ordering now works,
A dependency is any Phase/Pipeline another Phase/Pipeline depends on. A dependent if the Phase/Pipeline that depends on another Phase/Pipeline.
This looks like, insertAfter(dependent, dependency) or insertBefore(dependent, dependency).
- Start with the first Phase/Pipeline inserted
- If this Phase/Pipeline has any dependency, skip it and move onto the next one.
- Add this Phase/Pipeline to the order
- If this Phase/Pipeline has any dependents, repeat this process in order of insertion for each dependent Phase/Pipeline.
- Move onto the next node.
insert(dependent) also now works by setting the last added Phase/Pipeline as a dependency of dependent.
Breaking insertAfter
This change affects Scheduler:insertAfter() and Pipeline:insertAfter(). Because Phases/Pipelines no longer have fixed positions in the order of execution, insertAfter no longer inserts a dependent to be immediately after dependency.
To better demonstrate this, consider this code example:
local myScheduler = Scheduler.new()
:insert(PhaseOne)
:insert(PhaseThree)
:insertAfter(PhaseTwo, PhaseOne)
-- Old Behavior:
-- PhaseOne -> PhaseTwo -> PhaseThree
-- New Behavior:
-- PhaseOne -> PhaseThree -> PhaseTwoThis change is desirable because:
PhaseThreeis not dependent onPhaseTwo(dependencies are defined with:insertAfter())PhaseThreeis inserted beforePhaseTwo, so we respect the implicit order of insertion
If you find your systems breaking because of this change, you should use insertAfter and insertBefore to explicitly define the dependencies of your Phases/Pipelines. You should not rely on insert to manage dependencies.
Built-in RunService Phases Removed
The following RunService Phases are no longer included in the core library, instead they are now available as a plugin.
| Event | Phase(s) |
|---|---|
| PreRender | PreRender |
| PreAnimation | PreAnimation |
| PreSimulation | PreSimulation |
| PostSimulation | PostSimulation |
| Heartbeat | First, PreUpdate, Update, PostUpdate, Last |
When the Plugin is added, the default Phase will be set to Update.
[dependencies]
PlanckRunService = "yetanotherclown/planck-runservice@0.2.0-alpha.2"Replacement of Scheduler:setRunCondition
The method Scheduler:setRunCondition has been replaced with a new method Scheduler:addRunCondition which allows for the addition of multiple run conditions.
With this new method, it is no longer possible to overwrite previous run conditions. Run Conditions should ideally be added upon creation of the Scheduler, and not modified afterwards.
Replacement of SystemTable.runCondition
Now that you can add multiple Run Conditions, a System Table now will take SystemTable.runConditions instead which is an array of run conditions.
local function system()
-- ...
end
local function runIf()
-- ...
end
return {
system = system,
runConditions = { throttle(10), runIf }
}Changes
Added
Scheduler:insertBefore()Pipeline:insertBefore()- Conditions (isNot, runOnce, timePassed, onEvent)
Scheduler:addRunCondition()for adding multiple run conditionsPlanckRunServicePlugin which adds built-in Pipelines and Phases for RunService events
Changed
- Refactored internals to use Adjacency Matrices for managing ordering and dependencies of Phases/Pipelines
- Breaking: The following methods now create an ordering dependency instead of setting the fixed order of Phases/Pipelines
Scheduler:insert()Scheduler:insertAfter()Pipeline:insert()Pipeline:insertAfter()
- Breaking:
Scheduler:runAll()will no longer run in the exact order that Phases were inserted for Phases bound to events. They will be grouped together, and ran together in order of insertion. - Breaking:
Scheduler:setRunCondition()has been replaced withScheduler:addRunCondition() - Breaking:
SystemTable.runConditionhas been replaced withSystemTable.runConditions - Refactored internals to reuse event logic
- Replaced
Phase.Updateas default phase withDefault
Removed
- Built-in RunService Pipelines/Phases, these will be available as a separate plugin
Fixed
- Fallback System name does not contain the line of the system
Planck v0.1.0-rc.3
Planck, an ECS Scheduler
An Agnostic Scheduler, inspired by Bevy Schedules and Flecs Pipelines and Phases.
Important
The Planck Scheduler and it's plugins are currently in development!
You can find a release candidate under yetanotherclown/planck@0.1.0-rc.3
Installation
You can install Planck with Wally
[dependencies]
Planck = "yetanotherclown/planck@0.1.0-rc.3"What is Planck?
Planck is a standalone scheduler, which allows you to execute code on specific events, with certain conditions, and in a particular order.
This scheduler is library agnostic, which means that it doesn't matter which ECS library your using or if you're even using an ECS.
You can use this with Jecs, Matter, ECR, and other Luau ECS Libraries.
Does any of this really matter?
Yes, and no.
Your ECS code should be able to run in any order, without any conditions, and without concern for which event it's running on, as long as it is running.
The order of execution, and conditions both serve to optimize your code. Some systems don't need to run every frame, which is why we have conditions.
And the actual order of execution is to reduce latency between changes and effects in your ECS world.
Let's say we have systemA and systemB. systemA modifies data in our world which systemB depends on.
If systemA runs after systemB, then systemB will have to wait a whole frame for the modifications to be made.
This is called being off-by-a-frame, and this is why we care about the order of execution.
What's Next?
You may not completely understand what's written above. That's fine.
For now, you should read the Official Documentation on how to get started with Planck. These concepts will be explained more in depth as you read.
Quick Overview
While it's highly suggested you read the documentation, here is a quick overview of Planck's API.
The Scheduler
This is the core of Planck, this is where you add your Systems and set your Phases, Pipelines, and Run Conditions.
local Planck = require("@packages/Planck")
local Scheduler = Planck.Scheduler
local Jecs = require("@packages/Jecs")
local World = Jecs.World
local world = World.new()
local state = {}
local scheduler = Scheduler.new(world, state)Systems
Systems are really simple, they are just functions which run on an event or in a loop.
local function systemA(world, state)
-- ...
end
return systemAAnd to add it to our Scheduler,
-- ...
local systemA = require("@shared/systems/systemA")
local scheduler = Scheduler.new(world, state)
:addSystem(systemA)Phases
Phases are used to split up your frame into different sections, this allows us to schedule our systems to run at different moments of a given frame.
local Planck = require("@packages/Planck")
local Scheduler = Planck.Scheduler
local Phase = Planck.Phase
-- ...
local systemA = require("@shared/systems/systemA")
local myPhase = Phase.new("myPhase")
local scheduler = Scheduler.new(world, state)
:insert(myPhase)
:addSystem(systemA, myPhase)Planck has lots of built-in Phases that should work for most cases,
→ Built-in Phases
Pipelines
Pipelines are ordered groups of Phases, they make working with larger collections of Phases (which all run on the same event) easier.
local Phase = Planck.Phase
local Pipeline = Planck.Pipeline
local Scheduler = Planck.Scheduler
local PreUpdate = Phase.new()
local Update = Phase.new()
local PostUpdate = Phase.new()
local UpdatePipeline = Pipeline.new()
:insert(PreUpdate)
:insert(Update)
:insert(PostUpdate)
local scheduler = scheduler.new(world)
:insert(UpdatePipeline, RunService, "Heartbeat")Tip
The UpdatePipeline seen here, already exists in Planck! It's a built-in Pipeline that you can use without any setup.
See all Built-in Phases.
Conditions
When we run all our systems every frame, there are a lot of systems that may not actually need to run. Run Conditions allow us to
run our Systems, Phases and Pipelines only sometimes.
local function condition(world)
if someCondition then
return true
else
return false
end
end
local scheduler = Scheduler.new(world)
:setRunCondition(systemA, condition)
:setRunCondition(somePhase, condition)
:setRunCondition(somePipeline, condition)Conditions can be useful, but you should use them carefully. It's suggested that you read our page on
Conditions to see some useful examples and learn when you should use them.
Inspiration
Planck's API design is heavily influenced by the Bevy Engine, with Schedules, RunConditions, and more.
Planck also draws inspiration from Flecs for Pipelines and Phases.
We're combining the simple, and beloved API of Bevy with the concept of Pipelines and Phases.