From a9f449b3fbf862fab462d11d8b8151ff63ce4c10 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sat, 3 Aug 2024 05:48:48 +0200 Subject: [PATCH] Example (#93) * Initial commit * Return static function * Fix upvalues conflict * Add examples to luau * rename example folder * Add queries example * Add changetracking example * Add wildcards example * Delete example.project.json --- demo.project.json | 71 ++++++ demo/.gitignore | 6 + demo/README.md | 17 ++ demo/src/client/init.client.luau | 1 + demo/src/server/init.server.luau | 1 + demo/src/shared/common.luau | 258 ++++++++++++++++++++++ examples/README.md | 11 + examples/luau/entities/basics.luau | 45 ++++ examples/luau/entities/hierarchy.luau | 125 +++++++++++ examples/luau/queries/basics.luau | 75 +++++++ examples/luau/queries/changetracking.luau | 242 ++++++++++++++++++++ examples/luau/queries/wildcards.luau | 37 ++++ test/btree.luau | 152 +++++++++++++ 13 files changed, 1041 insertions(+) create mode 100644 demo.project.json create mode 100644 demo/.gitignore create mode 100644 demo/README.md create mode 100644 demo/src/client/init.client.luau create mode 100644 demo/src/server/init.server.luau create mode 100644 demo/src/shared/common.luau create mode 100644 examples/README.md create mode 100644 examples/luau/entities/basics.luau create mode 100644 examples/luau/entities/hierarchy.luau create mode 100644 examples/luau/queries/basics.luau create mode 100644 examples/luau/queries/changetracking.luau create mode 100644 examples/luau/queries/wildcards.luau create mode 100644 test/btree.luau diff --git a/demo.project.json b/demo.project.json new file mode 100644 index 0000000..54314ef --- /dev/null +++ b/demo.project.json @@ -0,0 +1,71 @@ +{ + "name": "demo", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Shared": { + "$path": "demo/src/shared" + }, + "ecs": { + "$path": "src" + } + }, + "ServerScriptService": { + "Server": { + "$path": "demo/src/server" + } + }, + "StarterPlayer": { + "StarterPlayerScripts": { + "Client": { + "$path": "demo/src/client" + } + } + }, + "Workspace": { + "$properties": { + "FilteringEnabled": true + }, + "Baseplate": { + "$className": "Part", + "$properties": { + "Anchored": true, + "Color": [ + 0.38823, + 0.37254, + 0.38823 + ], + "Locked": true, + "Position": [ + 0, + -10, + 0 + ], + "Size": [ + 512, + 20, + 512 + ] + } + } + }, + "Lighting": { + "$properties": { + "Ambient": [ + 0, + 0, + 0 + ], + "Brightness": 2, + "GlobalShadows": true, + "Outlines": false, + "Technology": "Voxel" + } + }, + "SoundService": { + "$properties": { + "RespectFilteringEnabled": true + } + } + } +} diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..cf9d94d --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,6 @@ +# Project place file +/example.rbxlx + +# Roblox Studio lock files +/*.rbxlx.lock +/*.rbxl.lock \ No newline at end of file diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..5223a3e --- /dev/null +++ b/demo/README.md @@ -0,0 +1,17 @@ +# example +Generated by [Rojo](https://github.com/rojo-rbx/rojo) 7.4.1. + +## Getting Started +To build the place from scratch, use: + +```bash +rojo build -o "example.rbxlx" +``` + +Next, open `example.rbxlx` in Roblox Studio and start the Rojo server: + +```bash +rojo serve +``` + +For more help, check out [the Rojo documentation](https://rojo.space/docs). \ No newline at end of file diff --git a/demo/src/client/init.client.luau b/demo/src/client/init.client.luau new file mode 100644 index 0000000..505f71c --- /dev/null +++ b/demo/src/client/init.client.luau @@ -0,0 +1 @@ +print("Hello world, from client!") \ No newline at end of file diff --git a/demo/src/server/init.server.luau b/demo/src/server/init.server.luau new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/demo/src/server/init.server.luau @@ -0,0 +1 @@ + diff --git a/demo/src/shared/common.luau b/demo/src/shared/common.luau new file mode 100644 index 0000000..38b1ff3 --- /dev/null +++ b/demo/src/shared/common.luau @@ -0,0 +1,258 @@ +--!optimize 2 +--!native + +local jecs = require(game:GetService("ReplicatedStorage").ecs) + +type World = jecs.WorldShim +type Entity = jecs.Entity + +local function panic(str) + -- We don't want to interrupt the loop when we error + task.spawn(error, str) +end + +local function Scheduler(world, ...) + 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(world, dt) + debug.profileend() + end + + local function loop(sinceLastFrame) + debug.profilebegin("loop()") + + for i = N, 1, -1 do + system = systems[i] + + dt = sinceLastFrame + + local didNotYield, why = xpcall(function() + for _ in run do end + end, debug.traceback) + + if didNotYield then + continue + 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 + end + + debug.profileend() + debug.resetmemorycategory() + end + + return loop +end + +type Tracker = { track: (world: World, fn: (changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T) + }) -> ()) -> () +} + +type Entity = number & { __nominal_type_dont_use: T } + +local function diff(a, b) + local size = 0 + for k, v in a do + if b[k] ~= v then + return true + end + size += 1 + end + for k, v in b do + size -= 1 + end + + if size ~= 0 then + return true + end + + return false +end + +local function ChangeTracker(world, T: Entity): Tracker + local PreviousT = jecs.pair(jecs.Rest, T) + local add = {} + local added + local removed + local is_trivial + + local function changes_added() + added = true + local q = world:query(T):without(PreviousT):drain() + return function() + local id, data = q.next() + if not id then + return nil + end + + is_trivial = typeof(data) ~= "table" + + add[id] = data + + return id, data + end + end + + local function changes_changed() + local q = world:query(T, PreviousT):drain() + + return function() + local id, new, old = q.next() + while true do + if not id then + return nil + end + + if not is_trivial then + if diff(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = q.next() + end + + local record = world.entityIndex.sparse[id] + local archetype = record.archetype + local column = archetype.records[PreviousT].column + local data = if is_trivial then new else table.clone(new) + archetype.columns[column][record.row] = data + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local q = world:query(PreviousT):without(T):drain() + return function() + local id = q.next() + if id then + world:remove(id, PreviousT) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = false + removed = false + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in add do + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) + end + end + + local tracker = { track = track } + + return tracker +end + +local bt +do + local SUCCESS = 0 + local FAILURE = 1 + local RUNNING = 2 + + local function SEQUENCE(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == FAILURE or status == RUNNING then + return status + end + end + return SUCCESS + end + end + local function FALLBACK(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == SUCCESS or status == RUNNING then + return status + end + end + return FAILURE + end + end + bt = { + SEQUENCE = SEQUENCE, + FALLBACK = FALLBACK, + RUNNING = RUNNING + } +end + +local function interval(s) + local pin + + local function throttle() + if not pin then + pin = os.clock() + end + + local elapsed = os.clock() - pin > s + if elapsed then + pin = os.clock() + end + + return elapsed + end + return throttle +end + +return { + Scheduler = Scheduler, + ChangeTracker = ChangeTracker, + interval = interval, + BehaviorTree = bt +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..68b04aa --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples + +This folder contains code examples for the Luau/Typescript APIs. + +## Run with Luau +To run the examples with Luau, run the following commands from the root of the repository: + +```sh +cd examples/luau +luau path/to/file.luau +``` diff --git a/examples/luau/entities/basics.luau b/examples/luau/entities/basics.luau new file mode 100644 index 0000000..cfebf6d --- /dev/null +++ b/examples/luau/entities/basics.luau @@ -0,0 +1,45 @@ +local jecs = require("@jecs") +local world = jecs.World.new() + +local Position = world:component() +local Walking = world:component() +local Name = world:component() + +-- Create an entity with name Bob +local bob = world:entity() + +-- The set operation finds or creates a component, and sets it. +world:set(bob, Position, Vector3.new(10, 20, 30)) +-- Name the entity Bob +world:set(bob, Name, "Bob") +-- The add operation adds a component without setting a value. This is +-- useful for tags, or when adding a component with its default value. +world:add(bob, Walking) + +-- Get the value for the Position component +local pos = world:get(bob, Position) +print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`) + +-- Overwrite the value of the Position component +world:set(bob, Position, Vector3.new(40, 50, 60)) + + +local alice = world:entity() +-- Create another named entity +world:set(alice, Name, "Alice") +world:set(alice, Position, Vector3.new(10, 20, 30)) +world:add(alice, Walking) + + +-- Remove tag +world:remove(alice, Walking) + +-- Iterate all entities with Position +for entity, p in world:query(Position) do + print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) +end + +-- Output: +-- {10, 20, 30} +-- Alice: {10, 20, 30} +-- Bob: {40, 50, 60} diff --git a/examples/luau/entities/hierarchy.luau b/examples/luau/entities/hierarchy.luau new file mode 100644 index 0000000..d872509 --- /dev/null +++ b/examples/luau/entities/hierarchy.luau @@ -0,0 +1,125 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local ChildOf = jecs.ChildOf +local world = jecs.World.new() + +local Name = world:component() +local Position = world:component() +local Star = world:component() +local Planet = world:component() +local Moon = world:component() + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new( + left.X + right.X, + left.Y + right.Y, + left.Z + right.Z + ) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new( + left.X * right, + left.Y * right, + left.Z * right + ) + end + return Vector3.new( + left.X * right.X, + left.Y * right.Y, + left.Z * right.Z + ) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +local function path(entity) + local str = world:get(entity, Name) + local parent + while true do + parent = world:parent(entity) + if not parent then + break + end + entity = parent + str = world:get(parent, Name) .. "/" .. str + end + return str +end + +local function iterate(entity, parent) + local p = world:get(entity, Position) + local actual = p + parent + print(path(entity)) + print(`\{{actual.X}, {actual.Y}, {actual.Z}}`) + + for child in world:query(pair(ChildOf, entity)) do + --print(world:get(child, Name)) + iterate(child, actual) + end +end + +local sun = world:entity() +world:add(sun, Star) +world:set(sun, Position, Vector3.one) +world:set(sun, Name, "Sun") +do + local earth = world:entity() + world:set(earth, Name, "Earth") + world:add(earth, pair(ChildOf, sun)) + world:add(earth, Planet) + world:set(earth, Position, Vector3.one * 3) + + do + local moon = world:entity() + world:set(moon, Name, "Moon") + world:add(moon, pair(ChildOf, earth)) + world:add(moon, Moon) + world:set(moon, Position, Vector3.one * 0.1) + + print(`Child of Earth? {world:has(moon, pair(ChildOf, earth))}`) + end + + local venus = world:entity() + world:set(venus, Name, "Venus") + world:add(venus, pair(ChildOf, sun)) + world:add(venus, Planet) + world:set(venus, Position, Vector3.one * 2) + + local mercury = world:entity() + world:set(mercury, Name, "Mercury") + world:add(mercury, pair(ChildOf, sun)) + world:add(mercury, Planet) + world:set(mercury, Position, Vector3.one) + + + iterate(sun, Vector3.zero) +end + +-- Output: +-- Child of Earth? true +-- Sun +-- {1, 1, 1} +-- Sun/Mercury +-- {2, 2, 2} +-- Sun/Venus +-- {3, 3, 3} +-- Sun/Earth +-- {4, 4, 4} +-- Sun/Earth/Moon +-- {4.1, 4.1, 4.1} diff --git a/examples/luau/queries/basics.luau b/examples/luau/queries/basics.luau new file mode 100644 index 0000000..8dd9a35 --- /dev/null +++ b/examples/luau/queries/basics.luau @@ -0,0 +1,75 @@ +local jecs = require("@jecs") +local world = jecs.World.new() + +local Position = world:component() +local Velocity = world:component() +local Name = world:component() + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new( + left.X + right.X, + left.Y + right.Y, + left.Z + right.Z + ) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new( + left.X * right, + left.Y * right, + left.Z * right + ) + end + return Vector3.new( + left.X * right.X, + left.Y * right.Y, + left.Z * right.Z + ) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +-- Create a few test entities for a Position, Velocity query +local e1 = world:entity() +world:set(e1, Name, "e1") +world:set(e1, Position, Vector3.new(10, 20, 30)) +world:set(e1, Velocity, Vector3.new(1, 2, 3)) + +local e2 = world:entity() +world:set(e2, Name, "e2") +world:set(e2, Position, Vector3.new(10, 20, 30)) +world:set(e2, Velocity, Vector3.new(4, 5, 6)) + +-- This entity will not match as it does not have Position, Velocity +local e3 = world:entity() +world:set(e3, Name, "e3") +world:set(e3, Position, Vector3.new(10, 20, 30)) + +-- Create an uncached query for Position, Velocity. +for entity, p, v in world:query(Position, Velocity) do + -- Iterate entities matching the query + p.X += v.X + p.Y += v.Y + p.Z += v.Z + + print(`{world:get(entity, Name)}: \{{p.X}, {p.Y}, {p.Z}}`) +end + +-- Output: +-- e2: {14, 25, 36} +-- e1: {11, 22, 33} diff --git a/examples/luau/queries/changetracking.luau b/examples/luau/queries/changetracking.luau new file mode 100644 index 0000000..20ed888 --- /dev/null +++ b/examples/luau/queries/changetracking.luau @@ -0,0 +1,242 @@ +local jecs = require("@jecs") + +type World = jecs.WorldShim + +type Tracker = { track: (world: World, fn: (changes: { + added: () -> () -> (number, T), + removed: () -> () -> number, + changed: () -> () -> (number, T, T) + }) -> ()) -> () +} + +local function diff(a, b) + local size = 0 + for k, v in a do + if b[k] ~= v then + return true + end + size += 1 + end + for k, v in b do + size -= 1 + end + + if size ~= 0 then + return true + end + + return false +end + +type Entity = number & { __nominal_type_dont_use: T } + +local function ChangeTracker(world, T: Entity): Tracker + local PreviousT = jecs.pair(jecs.Rest, T) + local add = {} + local added + local removed + local is_trivial + + local function changes_added() + added = true + local q = world:query(T):without(PreviousT):drain() + return function() + local id, data = q.next() + if not id then + return nil + end + + is_trivial = typeof(data) ~= "table" + + add[id] = data + + return id, data + end + end + + local function changes_changed() + local q = world:query(T, PreviousT):drain() + + return function() + local id, new, old = q.next() + while true do + if not id then + return nil + end + + if not is_trivial then + if diff(new, old) then + break + end + elseif new ~= old then + break + end + + id, new, old = q.next() + end + + add[id] = new + + return id, old, new + end + end + + local function changes_removed() + removed = true + + local q = world:query(PreviousT):without(T):drain() + return function() + local id = q.next() + if id then + world:remove(id, PreviousT) + end + return id + end + end + + local changes = { + added = changes_added, + changed = changes_changed, + removed = changes_removed, + } + + local function track(fn) + added = false + removed = false + + fn(changes) + + if not added then + for _ in changes_added() do + end + end + + if not removed then + for _ in changes_removed() do + end + end + + for e, data in add do + world:set(e, PreviousT, if is_trivial then data else table.clone(data)) + end + end + + local tracker = { track = track } + + return tracker +end + +local Vector3 +do + Vector3 = {} + Vector3.__index = Vector3 + + function Vector3.new(x, y, z) + x = x or 0 + y = y or 0 + z = z or 0 + return setmetatable({ X = x, Y = y, Z = z }, Vector3) + end + + function Vector3.__add(left, right) + return Vector3.new( + left.X + right.X, + left.Y + right.Y, + left.Z + right.Z + ) + end + + function Vector3.__mul(left, right) + if typeof(right) == "number" then + return Vector3.new( + left.X * right, + left.Y * right, + left.Z * right + ) + end + return Vector3.new( + left.X * right.X, + left.Y * right.Y, + left.Z * right.Z + ) + end + + Vector3.one = Vector3.new(1, 1, 1) + Vector3.zero = Vector3.new() +end + +local world = jecs.World.new() +local Name = world:component() + +local function named(ctr, name) + local e = ctr(world) + world:set(e, Name, name) + return e +end +local function name(e) + return world:get(e, Name) +end + +local Position = named(world.component, "Position") + +-- Create the ChangeTracker with the component type to track +local PositionTracker = ChangeTracker(world, Position) + +local e1 = named(world.entity, "e1") +world:set(e1, Position, Vector3.new(10, 20, 30)) + +local e2 = named(world.entity, "e2") +world:set(e2, Position, Vector3.new(10, 20, 30)) + +PositionTracker.track(function(changes) + -- You can iterate over different types of changes: Added, Changed, Removed + + -- added queries for every entity with a new Position component + for e, p in changes.added() do + print(`Added {e}: \{{p.X}, {p.Y}, {p.Z}}`) + end + + -- changed queries for entities who's changed their data since + -- last was it tracked + for _ in changes.changed() do + print([[This won't print because it is the first time + we are tracking the Position component]]) + end + + -- removed queries for entities who's removed their Position component + -- since last it was tracked + for _ in changes.removed() do + print([[This won't print because it is the first time + we are tracking the Position component]]) + end +end) + +world:set(e1, Position, Vector3.new(1, 1, 2) * 999) + +PositionTracker.track(function(changes) + for e, p in changes.added() do + print([[This won't never print no Position component was added + since last time we tracked]]) + end + + for e, old, new in changes.changed() do + print(`{name(e)}'s Position changed from \{{old.X}, {old.Y}, {old.Z}\} to \{{new.X}, {new.Y}, {new.Z}\}`) + end + + -- If you don't call e.g. changes.removed() then it will automatically drain its iterator and stage their changes. + -- This ensures you will not have any off-by-one frame errors. +end) + +world:remove(e2, Position) + +PositionTracker.track(function(changes) + for e in changes.removed() do + print(`Position was removed from {name(e)}`) + end +end) + +-- Output: +-- Added 265: {10, 20, 30} +-- Added 264: {10, 20, 30} +-- e1's Position changed from {10, 20, 30} to {999, 999, 1998} +-- Position was removed from e2 diff --git a/examples/luau/queries/wildcards.luau b/examples/luau/queries/wildcards.luau new file mode 100644 index 0000000..87bef3d --- /dev/null +++ b/examples/luau/queries/wildcards.luau @@ -0,0 +1,37 @@ +local jecs = require("@jecs") +local pair = jecs.pair +local world = jecs.World.new() +local Name = world:component() + +local function named(ctr, name) + local e = ctr(world) + world:set(e, Name, name) + return e +end +local function name(e) + return world:get(e, Name) +end + +local Eats = world:component() +local Apples = named(world.entity, "Apples") +local Oranges = named(world.entity, "Oranges") + +local bob = named(world.entity, "Bob") +world:set(bob, pair(Eats, Apples), 10) + +local alice = named(world.entity, "Alice") +world:set(alice, pair(Eats, Oranges), 5) + +-- Aliasing the wildcard to symbols improves readability and ease of writing +local __ = jecs.Wildcard + +-- Create a query that matches edible components +for entity, amount in world:query(pair(Eats, __)) do + -- Iterate the query + local food = world:target(entity, Eats) + print(`{name(entity)} eats {amount} {name(food)}`) +end + +-- Output: +-- Alice eats 5 Oranges +-- Bob eats 10 Apples diff --git a/test/btree.luau b/test/btree.luau new file mode 100644 index 0000000..e6f1957 --- /dev/null +++ b/test/btree.luau @@ -0,0 +1,152 @@ +-- original author @centauri +local bt +do + + local FAILURE = 0 + local SUCCESS = 1 + local RUNNING = 2 + + local function SEQUENCE(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == FAILURE or status == RUNNING then + return status + end + end + return SUCCESS + end + end + local function FALLBACK(nodes) + return function(...) + for _, node in nodes do + local status = node(...) + if status == SUCCESS or status == RUNNING then + return status + end + end + return FAILURE + end + end + bt = { + SEQUENCE = SEQUENCE, + FALLBACK = FALLBACK, + RUNNING = RUNNING, + SUCCESS = SUCCESS, + FAILURE = FAILURE, + } +end + +local SEQUENCE, FALLBACK = bt.SEQUENCE, bt.FALLBACK +local RUNNING, SUCCESS, FAILURE = bt.FAILURE, bt.SUCCESS, bt.FAILURE + +local btree = FALLBACK { + SEQUENCE { + function() + return 1 + end, + + function() + return 0 + end + }, + SEQUENCE { + function() + print(3) + local start = os.clock() + local now = os.clock() + while os.clock() - now < 4 do + print("yielding") + coroutine.yield() + end + return 0 + end + }, + function() + return 1 + end +} + +function wait(seconds) + local start = os.clock() + while os.clock() - start < seconds do end + return os.clock() - start +end + +local function panic(str) + -- We don't want to interrupt the loop when we error + coroutine.resume(coroutine.create(function() error(str) end)) +end + +local jecs = require("@jecs") +local world = jecs.World.new() + +local function Scheduler(world, ...) + local systems = { ... } + local systemsNames = {} + local N = #systems + local system + local dt + + for i, module in systems do + local sys = if typeof(module) == "function" then module else 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(world, dt) + --debug.profileend() + end + + local function loop(sinceLastFrame) + --debug.profilebegin("loop()") + local start = os.clock() + for i = N, 1, -1 do + system = systems[i] + + dt = sinceLastFrame + + local didNotYield, why = xpcall(function() + for _ in run do end + end, debug.traceback) + + if didNotYield then + continue + 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 + end + + --debug.profileend() + --debug.resetmemorycategory() + return os.clock() - start + end + + return loop +end + +local co = coroutine.create(btree) +local function ai(world, dt) + coroutine.resume(co) +end + +local loop = Scheduler(world, ai) + +while wait(0.2) do + print("frame time: ", loop(0.2)) +end