Skip to content

Releases: YetAnotherClown/planck

v0.3.0-alpha.1

10 Nov 23:05

Choose a tag to compare

v0.3.0-alpha.1 Pre-release
Pre-release

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 renderSystem

You 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 networkSystem

Breaking Changes

System Returns

It is now expected that systems return:

  • Either nil or 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

  • TypeScript: EventLike type which did not work with non-RBXScriptSignal types #25
  • Added missing insertBefore type to the Pipeline interface
  • TypeScript: Condition type did now allow sending tuples #34

v0.2.0

10 Jul 00:47

Choose a tag to compare

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.

→ Documentation

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

  1. Start with the first Phase/Pipeline inserted
  2. If this Phase/Pipeline has any dependency, skip it and move onto the next one.
  3. Add this Phase/Pipeline to the order
  4. If this Phase/Pipeline has any dependents, repeat this process in order of insertion for each dependent Phase/Pipeline.
  5. 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 -> PhaseTwo

This change is desirable because:

  • PhaseThree is not dependent on PhaseTwo (dependencies are defined with :insertAfter())
  • PhaseThree is inserted before PhaseTwo, 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 conditions
  • PlanckRunService Plugin 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 with Scheduler:addRunCondition()
  • Breaking: SystemTable.runCondition has been replaced with SystemTable.runConditions
  • Refactored internals to reuse event logic
  • Replaced Phase.Update as default phase with Default
  • 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 ...
Read more

Planck v0.2.0-rc.3

05 Jul 00:59

Choose a tag to compare

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.

→ Documentation

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

  1. Start with the first Phase/Pipeline inserted
  2. If this Phase/Pipeline has any dependency, skip it and move onto the next one.
  3. Add this Phase/Pipeline to the order
  4. If this Phase/Pipeline has any dependents, repeat this process in order of insertion for each dependent Phase/Pipeline.
  5. 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 -> PhaseTwo

This change is desirable because:

  • PhaseThree is not dependent on PhaseTwo (dependencies are defined with :insertAfter())
  • PhaseThree is inserted before PhaseTwo, 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 conditions
  • PlanckRunService Plugin 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 with Scheduler:addRunCondition()
  • Breaking: SystemTable.runCondition has been replaced with SystemTable.runConditions
  • Refactored internals to reuse event logic
  • Replaced Phase.Update as default phase with Default
  • 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

05 Feb 00:56

Choose a tag to compare

Planck, an ECS Scheduler

GitHub License
Documentation
Wally Package

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 systemA

And 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

03 Feb 23:51

Choose a tag to compare

Planck v0.2.0-alpha.2 Pre-release
Pre-release

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

  1. Start with the first Phase/Pipeline inserted
  2. If this Phase/Pipeline has any dependency, skip it and move onto the next one.
  3. Add this Phase/Pipeline to the order
  4. If this Phase/Pipeline has any dependents, repeat this process in order of insertion for each dependent Phase/Pipeline.
  5. 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 -> PhaseTwo

This change is desirable because:

  • PhaseThree is not dependent on PhaseTwo (dependencies are defined with :insertAfter())
  • PhaseThree is inserted before PhaseTwo, 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 conditions
  • PlanckRunService Plugin 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 with Scheduler:addRunCondition()
  • Breaking: SystemTable.runCondition has been replaced with SystemTable.runConditions
  • Refactored internals to reuse event logic
  • Replaced Phase.Update as default phase with Default

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

03 Feb 23:55

Choose a tag to compare

Planck, an ECS Scheduler

GitHub License
Documentation
Wally Package

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 systemA

And 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.