Compare commits

...

20 commits

Author SHA1 Message Date
EncodedVenom
7f37a478a8
Merge acc6e40aed into c8884c8eac 2024-12-28 21:59:30 +01:00
EncodedVenom
c8884c8eac
bump ver
Some checks failed
Analysis / Run Luau Analyze (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Unit Testing / Run Luau Tests (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
2024-12-27 14:20:06 -05:00
EncodedVenom
500c494812
Fix unit test 2024-12-27 14:09:39 -05:00
EncodedVenom
0243efe4ba
Remove pin on luau version 2024-12-27 14:05:10 -05:00
Ukendio
f8b3772bce Fix spelling error
Some checks are pending
Analysis / Run Luau Analyze (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Unit Testing / Run Luau Tests (push) Waiting to run
2024-12-27 04:50:15 +01:00
Ukendio
fad88419a3 Fix type errors 2024-12-27 04:34:17 +01:00
Ukendio
1c8a4967e3 Bump patch 2024-12-27 03:47:43 +01:00
Ukendio
b14e984c66 Fix shadowed variable 2024-12-27 03:47:02 +01:00
Ukendio
ed3b2ae35d Bump
Some checks are pending
Analysis / Run Luau Analyze (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Unit Testing / Run Luau Tests (push) Waiting to run
2024-12-26 07:05:20 +01:00
Ukendio
b2fc046ef0 Changed Pair type 2024-12-26 07:04:40 +01:00
Marcus
ec4fa3ff3e
Add cached queries (#166)
* Initial commit

* Add tests

* Dedup observers

* Handle filters on table creation

* Handle Archetype deletion

* Remove print

* Fix type errors

* Cleanup code

* Manually inline code

* Build terms for cached queries

* Specialized cached query iterator

* Remove shadowed variable

* Inverse statement

* Rework demo

* Fix metatable

* Use generalized iteration
2024-12-26 06:15:41 +01:00
Marcus
0f2e0eba76
Initial commit (#167)
Some checks are pending
Analysis / Run Luau Analyze (push) Waiting to run
Deploy VitePress site to Pages / build (push) Waiting to run
Deploy VitePress site to Pages / Deploy (push) Blocked by required conditions
Unit Testing / Run Luau Tests (push) Waiting to run
2024-12-26 01:06:14 +01:00
EncodedVenom
acc6e40aed Simplify script maybe? 2024-10-19 20:04:29 -04:00
EncodedVenom
9878df20ba Use bash entirely 2024-10-19 20:02:05 -04:00
EncodedVenom
b365fc8c1c Parenthesis 2024-10-19 19:58:09 -04:00
EncodedVenom
a8004011cc change to file 2024-10-19 19:55:54 -04:00
EncodedVenom
c697030ec4 debug 2024-10-19 19:53:43 -04:00
EncodedVenom
fefbd19b38 get around permissions 2024-10-19 19:52:21 -04:00
EncodedVenom
d5dc64be1b change location 2024-10-19 19:49:13 -04:00
EncodedVenom
04ef154193 Unit testing init PR 2024-10-19 19:47:26 -04:00
25 changed files with 906 additions and 632 deletions

View file

@ -16,4 +16,5 @@ jobs:
- name: Analyze - name: Analyze
run: | run: |
output=$(luau-analyze src || true) # Suppress errors for now. (luau-analyze src || true) > analyze-log.txt # Suppress errors for now.
bash ./scripts/gh-warn-luau-analyze.sh analyze-log.txt

View file

@ -13,9 +13,9 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Luau - name: Install Luau
uses: encodedvenom/install-luau@v4.2 uses: encodedvenom/install-luau@v4.3
with: with:
version: '0.651' version: 'latest'
verbose: 'true' verbose: 'true'
- name: Run Unit Tests - name: Run Unit Tests

View file

@ -2,18 +2,16 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService") local UserInputService = game:GetService("UserInputService")
local jabby = require(ReplicatedStorage.Packages.jabby) local jabby = require(ReplicatedStorage.Packages.jabby)
local std = require(ReplicatedStorage.std) local std = ReplicatedStorage.std
local Scheduler = std.Scheduler local scheduler = require(std.scheduler)
local world = std.world local world = require(std.world)
local function start(modules) local function start(modules)
local scheduler = Scheduler.new(world, require(ReplicatedStorage.std.components))
for _, module in modules do for _, module in modules do
require(module)(scheduler) require(module)
end end
local events = scheduler.collect.all() local events = scheduler.COLLECT()
scheduler.systems.begin(events) scheduler.BEGIN(events)
jabby.set_check_function(function(player) jabby.set_check_function(function(player)
return true return true
end) end)

View file

@ -3,15 +3,15 @@
-- original author @centau -- original author @centau
local SUCCESS = 0 local FAILURE = -1
local FAILURE = 1 local RUNNING = 0
local RUNNING = 2 local SUCCESS = 1
local function SEQUENCE(nodes) local function SEQUENCE(nodes)
return function(...) return function(...)
for _, node in nodes do for _, node in nodes do
local status = node(...) local status = node(...)
if status == FAILURE or status == RUNNING then if status <= RUNNING then
return status return status
end end
end end
@ -23,7 +23,7 @@ local function FALLBACK(nodes)
return function(...) return function(...)
for _, node in nodes do for _, node in nodes do
local status = node(...) local status = node(...)
if status == SUCCESS or status == RUNNING then if status > FAILURE then
return status return status
end end
end end

View file

@ -8,7 +8,7 @@ local components: {
Model: Entity<Model>, Model: Entity<Model>,
Player: Entity, Player: Entity,
Target: Entity, Target: Entity,
Transform: Entity<CFrame>, Transform: Entity<{ new: CFrame, old: CFrame }>,
Velocity: Entity<number>, Velocity: Entity<number>,
Previous: Entity, Previous: Entity,
} = } =
@ -23,4 +23,8 @@ local components: {
Previous = world:component(), Previous = world:component(),
} }
for name, component in components :: {[string]: jecs.Entity} do
world:set(component, jecs.Name, name)
end
return table.freeze(components) return table.freeze(components)

View file

@ -1,11 +0,0 @@
local handle = require(script.Parent.handle)
local world = require(script.Parent.world)
local singleton = world:entity()
local function ctx()
-- Cannot cache handles because they will get invalidated
return handle(singleton)
end
return ctx

View file

@ -1,56 +0,0 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local world = require(script.Parent.world)
type Handle = {
has: (self: Handle, id: jecs.Entity) -> boolean,
get: <T>(self: Handle, id: jecs.Entity<T>) -> T?,
add: <T>(self: Handle, id: jecs.Entity<T>) -> Handle,
set: <T>(self: Handle, id: jecs.Entity<T>, value: T) -> Handle,
id: (self: Handle?) -> jecs.Entity,
}
local handle: (e: jecs.Entity) -> Handle
do
local e
local function has(_, id)
return world:has(e, id)
end
local function get(_, id)
return world:get(e, id)
end
local function set(self, id, value)
world:set(e, id, value)
return self
end
local function add(self, id)
world:add(e, id)
return self
end
local function clear(self)
world:clear(e)
return self
end
local function delete(self)
world:delete(e)
end
local function id()
return e
end
local entity = {
has = has,
get = get,
set = set,
add = add,
clear = clear,
id = id,
}
function handle(id)
e = id
return entity
end
end
return handle

View file

@ -1,32 +0,0 @@
--!native
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local function create_cache(hook)
local columns = setmetatable({}, {
__index = function(self, component)
local column = {}
self[component] = column
return column
end,
})
return function(world, component, fn)
local column = columns[component]
table.insert(column, fn)
world:set(component, hook, function(entity, value)
for _, callback in column do
callback(entity, value)
end
end)
end
end
local hooks = {
OnSet = create_cache(jecs.OnSet),
OnAdd = create_cache(jecs.OnAdd),
OnRemove = create_cache(jecs.OnRemove),
}
return hooks

View file

@ -1,25 +0,0 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local world = require(script.world) :: jecs.World
export type World = jecs.World
local Scheduler = require(script.scheduler)
export type Scheduler = Scheduler.Scheduler
local std = {
ChangeTracker = require(script.changetracker),
Scheduler = Scheduler,
bt = require(script.bt),
collect = require(script.collect),
components = require(script.components),
ctx = require(script.ctx),
handle = require(script.handle),
interval = require(script.interval),
ref = require(script.ref),
world = world :: World,
pair = jecs.pair,
__ = jecs.w,
hooks = require(script.hooks),
}
return std

View file

@ -0,0 +1,14 @@
local std = game:GetService("ReplicatedStorage").std
local Players = game:GetService("Players")
local scheduler = require(std.scheduler)
local PHASE = scheduler.PHASE
return {
PlayerAdded = PHASE({
event = Players.PlayerAdded
}),
PlayerRemoved = PHASE({
event = Players.PlayerRemoving
})
}

View file

@ -1,16 +1,18 @@
local handle = require(script.Parent.handle)
local world = require(script.Parent.world) local world = require(script.Parent.world)
local refs = {} local jecs = require(game:GetService("ReplicatedStorage").ecs)
local refs: {[any]: jecs.Entity} = {}
local function fini(key) local function fini(key): () -> ()
return function() return function()
refs[key] = nil refs[key] = nil
end end
end end
local function ref(key): (handle.Handle, (() -> ())?) local function noop() end
local function ref(key): (jecs.Entity, () -> ())
if not key then if not key then
return handle(world:entity()) return world:entity(), noop
end end
local e = refs[key] local e = refs[key]
if not e then if not e then
@ -18,7 +20,7 @@ local function ref(key): (handle.Handle, (() -> ())?)
refs[key] = e refs[key] = e
end end
-- Cannot cache handles because they will get invalidated -- Cannot cache handles because they will get invalidated
return handle(e), fini(key) return e, fini(key)
end end
return ref return ref

View file

@ -1,31 +0,0 @@
local reserved = 0
local function reserve()
reserved += 1
return reserved
end
-- If you don't like passing around a world singleton
-- and you need to register component IDs, just register them.
-- I dont use this because I like adding component traits
--[[
local components = {
Model = registry.reserve(),
Transform = registry.reserve(),
}
local world = registry.register(jecs.World.new())
local e = world:entity()
world:set(e, components.Transform, CFrame)
]]
local function register(world)
for _ = 1, reserved do
world:component()
end
return world
end
return {
reserve = reserve,
register = register,
}

View file

@ -1,6 +1,7 @@
--!native --!native
--!optimize 2 --!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local jabby = require(ReplicatedStorage.Packages.jabby) local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local pair = jecs.pair local pair = jecs.pair
@ -8,6 +9,7 @@ local Name = jecs.Name
type World = jecs.World type World = jecs.World
type Entity<T = nil> = jecs.Entity<T> type Entity<T = nil> = jecs.Entity<T>
type Id<T = unknown> = jecs.Id<T>
type System = { type System = {
callback: (world: World) -> (), callback: (world: World) -> (),
@ -21,115 +23,92 @@ type Events = {
Heartbeat: Systems, Heartbeat: Systems,
} }
export type Scheduler = { local world = require(script.Parent.world)
components: { local Disabled = world:entity()
Disabled: Entity, local System = world:component() :: Id<{ callback: (any) -> (), name: string}>
System: Entity<System>, local DependsOn = world:entity()
Phase: Entity, local Event = world:component() :: Id<RBXScriptSignal>
DependsOn: Entity, local Phase = world:entity()
},
collect: { local PreRender = world:entity()
under_event: (event: Entity) -> Systems, local Heartbeat = world:entity()
all: () -> Events, local PreAnimation = world:entity()
}, local PreSimulation = world:entity()
systems: {
begin: (events: Events) -> { [Entity]: thread },
new: (callback: (dt: number) -> (), phase: Entity) -> Entity,
},
phases: {
RenderStepped: Entity,
Heartbeat: Entity,
},
phase: (after: Entity) -> Entity,
debugging: boolean,
}
local scheduler_new: (w: World, components: { [string]: Entity }) -> Scheduler
do
local world: World
local Disabled: Entity
local System: Entity<System>
local DependsOn: Entity
local Phase: Entity
local Event: Entity<RBXScriptSignal>
local scheduler
local RenderStepped
local Heartbeat
local PreAnimation
local PreSimulation
local sys: System local sys: System
local dt local dt: number
local jabby_scheduler = jabby.scheduler.create("Scheduler")
local a, b, c, d
local function run() local function run()
local id = sys.id local id = sys.id
scheduler:run(id, sys.callback, dt) jabby_scheduler:run(id, sys.callback, a, b, c, d)
return nil
end end
local function panic(str) world:add(Heartbeat, Phase)
-- We don't want to interrupt the loop when we error world:set(Heartbeat, Event, RunService.Heartbeat)
task.spawn(error, str)
world:add(PreSimulation, Phase)
world:set(PreSimulation, Event, RunService.PreSimulation)
world:add(PreAnimation, Phase)
world:set(PreAnimation, Event, RunService.PreAnimation)
table.insert(jabby.public, {
class_name = "World",
name = "MyWorld",
world = world,
debug = Name,
entities = {},
})
jabby.public.updated = true
table.insert(jabby.public, jabby_scheduler)
if RunService:IsClient() then
world:add(PreRender, Phase)
world:set(PreRender, Event, (RunService :: RunService).PreRender)
end end
local function begin(events: { Systems })
local function begin(events: { [RBXScriptSignal]: Systems })
local connections = {} local connections = {}
for event, systems in events do for event, systems in events do
if not event then if not event then
continue continue
end end
local event_name = tostring(event) local event_name = tostring(event)
connections[event] = event:Connect(function(delta) connections[event] = event:Connect(function(...)
debug.profilebegin(event_name) debug.profilebegin(event_name)
for _, s in systems do for _, s in systems do
sys = s sys = s
dt = delta a, b, c, d = ...
local didNotYield, why = xpcall(function()
for _ in run do for _ in run do
break break
end end
end, debug.traceback)
if didNotYield then
continue
end
if string.find(why, "thread is not yieldable") then
panic(
"Not allowed to yield in the systems."
.. "\n"
.. "System: "
.. debug.info(s.callback, "n")
.. " has been ejected"
)
continue
end
panic(why)
end end
debug.profileend() debug.profileend()
end) end)
end end
return threads return connections
end end
local function scheduler_collect_systems_under_phase_recursive(systems, phase) local function scheduler_collect_systems_under_phase_recursive(systems, phase: Entity)
local phase_name = world:get(phase, Name) local phase_name = world:get(phase, Name)
for _, s in world:query(System):with(pair(DependsOn, phase)) do for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, { table.insert(systems, {
id = scheduler:register_system({ id = jabby_scheduler:register_system({
name = s.name, name = s.name,
phase = phase_name, phase = phase_name,
}), } :: any),
callback = s.callback, callback = s.callback,
}) })
end end
for after in world:query(Phase):with(pair(DependsOn, phase)) do for after in world:query(Phase):with(pair(DependsOn, phase)):iter() do
scheduler_collect_systems_under_phase_recursive(systems, after) scheduler_collect_systems_under_phase_recursive(systems, after)
end end
end end
@ -148,99 +127,41 @@ do
return events return events
end end
local function scheduler_phase_new(after) local function scheduler_phase_new(d: { after: Entity?, event: RBXScriptSignal? })
local phase = world:entity() local phase = world:entity()
world:add(phase, Phase) world:add(phase, Phase)
local dependency = pair(DependsOn, after) local after = d.after
if after then
local dependency = pair(DependsOn, after :: Entity)
world:add(phase, dependency) world:add(phase, dependency)
end
local event = d.event
if event then
world:set(phase, Event, event)
end
return phase return phase
end end
local function scheduler_systems_new(callback, phase) local function scheduler_systems_new(callback: (any) -> (), phase: Entity?)
local system = world:entity() local system = world:entity()
local name = debug.info(callback, "n") world:set(system, System, { callback = callback, name = debug.info(callback, "n") })
world:set(system, System, { callback = callback, name = name }) local depends_on = DependsOn :: jecs.Entity
world:add(system, pair(DependsOn, phase)) local p: Entity = phase or Heartbeat
world:add(system, pair(depends_on, p))
return system return system
end end
function scheduler_new(w: World, components: { [string]: Entity })
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
world:add(Heartbeat, Phase)
world:set(Heartbeat, Event, RunService.Heartbeat)
world:add(PreSimulation, Phase)
world:set(PreSimulation, Event, RunService.PreSimulation)
world:add(PreAnimation, Phase)
world:set(PreAnimation, Event, RunService.PreAnimation)
for name, component in components do
world:set(component, Name, name)
end
table.insert(jabby.public, {
class_name = "World",
name = "MyWorld",
world = world,
debug = Name,
entities = {},
})
jabby.public.updated = true
scheduler = jabby.scheduler.create("scheduler")
table.insert(jabby.public, scheduler)
return { return {
phase = scheduler_phase_new, SYSTEM = scheduler_systems_new,
BEGIN = begin,
PHASE = scheduler_phase_new,
COLLECT = scheduler_collect_systems_all,
phases = { phases = {
RenderStepped = RenderStepped,
PreSimulation = PreSimulation,
Heartbeat = Heartbeat, Heartbeat = Heartbeat,
PreSimulation = PreSimulation,
PreAnimation = PreAnimation, PreAnimation = PreAnimation,
}, PreRender = PreRender
world = world,
components = {
DependsOn = DependsOn,
Disabled = Disabled,
Phase = Phase,
System = System,
},
collect = {
under_event = scheduler_collect_systems_under_event,
all = scheduler_collect_systems_all,
},
systems = {
new = scheduler_systems_new,
begin = begin,
},
} }
end
end
return {
new = scheduler_new,
} }

View file

@ -6,13 +6,12 @@ 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)
local __ = jecs.Wildcard local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local ref = require(std.ref)
local interval = require(std.interval)
local std = require(ReplicatedStorage.std) local world = require(std.world)
local ref = std.ref local cts = require(std.components)
local interval = std.interval
local world: std.World = std.world
local cts = std.components
local Mob = cts.Mob local Mob = cts.Mob
local Transform = cts.Transform local Transform = cts.Transform
@ -20,13 +19,26 @@ local Velocity = cts.Velocity
local Player = cts.Player local Player = cts.Player
local Character = cts.Character local Character = cts.Character
local characters = world
:query(Character)
:with(Player)
:cached()
local moving_mobs = world
:query(Transform, Velocity)
:with(Mob)
:cached()
local function mobsMove(dt: number) local function mobsMove(dt: number)
local targets = {} local targets = {}
for _, character in world:query(Character):with(Player):iter() do
for _, character in characters do
table.insert(targets, (character.PrimaryPart :: Part).Position) table.insert(targets, (character.PrimaryPart :: Part).Position)
end end
for mob, transform, v in world:query(Transform, Velocity):with(Mob):iter() do for mob, transform, v in moving_mobs do
local cf = transform.new local cf = transform.new
local p = cf.Position local p = cf.Position
@ -59,15 +71,18 @@ local function spawnMobs()
local cf = CFrame.new(p) local cf = CFrame.new(p)
local v = 5 local v = 5
local id = ref():set(Velocity, v):set(Transform, { new = cf }):add(Mob).id() local e = world:entity()
world:set(e, Velocity, v)
world:set(e, Transform, { new = cf })
world:add(e, Mob)
blink.SpawnMob.FireAll(id, cf, v) blink.SpawnMob.FireAll(e, cf, v)
end end
end end
return function(scheduler: std.Scheduler) local scheduler = require(std.scheduler)
local phases = scheduler.phases
local system_new = scheduler.systems.new scheduler.SYSTEM(spawnMobs)
system_new(mobsMove, phases.Heartbeat) scheduler.SYSTEM(mobsMove)
system_new(spawnMobs, phases.Heartbeat)
end return 0

View file

@ -1,43 +1,40 @@
local Players = game:GetService("Players") local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = require(ReplicatedStorage.std) local std = ReplicatedStorage.std
local ref = std.ref local ref = require(std.ref)
local collect = std.collect local collect = require(std.collect)
local cts = std.components local cts = require(std.components)
local world = require(std.world)
local Player = cts.Player local Player = cts.Player
local Character = cts.Character local Character = cts.Character
local playersAdded = collect(Players.PlayerAdded)
local playersRemoved = collect(Players.PlayerRemoving)
local world: std.World = std.world
local conn = {} local conn = {}
local function players() local function playersAdded(player: Player)
for _, player in playersAdded do local e = ref(player.UserId)
world:set(std.world:entity(), cts.Transform) world:set(e, Player, player)
local e = ref(player.UserId):set(Player, player)
local characterAdd = player.CharacterAdded local characterAdd = player.CharacterAdded
conn[e.id()] = characterAdd:Connect(function(rig) conn[e] = characterAdd:Connect(function(rig)
while rig.Parent ~= workspace do while rig.Parent ~= workspace do
task.wait() task.wait()
end end
e:set(Character, rig) world:set(e, Character, rig)
end) end)
end end
for _, player in playersRemoved do local function playersRemoved(player: Player)
local id = ref(player.UserId):clear().id() local e = ref(player.UserId)
conn[id]:Disconnect() world:clear(e)
conn[id] = nil local connection = conn[e]
end connection:Disconnect()
conn[e] = nil
end end
return function(scheduler: std.Scheduler) local scheduler = require(std.scheduler)
local phases = scheduler.phases local phases = require(std.phases)
local system_new = scheduler.systems.new scheduler.SYSTEM(playersAdded, phases.PlayerAdded)
system_new(players, phases.Heartbeat) scheduler.SYSTEM(playersRemoved, phases.PlayerRemoved)
end
return 0

View file

@ -1,20 +1,22 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net) local blink = require(ReplicatedStorage.net)
local std = require(ReplicatedStorage.std) local std = ReplicatedStorage.std
local world = std.world local world = require(std.world)
local ref = std.ref local ref = require(std.ref)
local cts = std.components local cts = require(std.components)
local Model = cts.Model local Model = cts.Model
local Transform = cts.Transform local Transform = cts.Transform
local moving_models = world:query(Transform, Model):cached()
local function move(dt: number) local function move(dt: number)
for _, transform, model in world:query(Transform, Model):iter() do for _, transform, model in moving_models do
local cf = transform.new local cf = transform.new
if cf ~= transform.old then if cf ~= transform.old then
local origo = model.PrimaryPart.CFrame local part = model.PrimaryPart :: BasePart
model.PrimaryPart.CFrame = origo:Lerp(cf, 1) local origo = part.CFrame
part.CFrame = origo:Lerp(cf, 1)
transform.old = cf transform.old = cf
end end
end end
@ -22,8 +24,8 @@ end
local function syncTransforms() local function syncTransforms()
for _, id, cf in blink.UpdateTransform.Iter() do for _, id, cf in blink.UpdateTransform.Iter() do
local e = ref("server-" .. id) local e = ref("server-" .. tostring(id))
local transform = e:get(cts.Transform) local transform = world:get(e, Transform)
if not transform then if not transform then
continue continue
end end
@ -31,9 +33,9 @@ local function syncTransforms()
end end
end end
return function(scheduler: std.Scheduler) local scheduler = require(std.scheduler)
local phases = scheduler.phases
local system_new = scheduler.systems.new scheduler.SYSTEM(move)
system_new(move, phases.Heartbeat) scheduler.SYSTEM(syncTransforms)
system_new(syncTransforms, phases.RenderStepped)
end return 0

View file

@ -1,9 +1,9 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net) local blink = require(ReplicatedStorage.net)
local std = require(ReplicatedStorage.std) local std = ReplicatedStorage.std
local ref = std.ref local ref = require(std.ref)
local world = std.world local world = require(std.world)
local cts = std.components local cts = require(std.components)
local function syncMobs() local function syncMobs()
for _, id, cf, vel in blink.SpawnMob.Iter() do for _, id, cf, vel in blink.SpawnMob.Iter() do
@ -16,16 +16,16 @@ local function syncMobs()
part.Parent = model part.Parent = model
model.Parent = workspace model.Parent = workspace
ref("server-" .. id) local e = ref("server-" .. tostring(id))
:set(cts.Transform, { new = cf, old = cf }) world:set(e, cts.Transform, { new = cf, old = cf })
:set(cts.Velocity, vel) world:set(e, cts.Velocity, vel)
:set(cts.Model, model) world:set(e, cts.Model, model)
:add(cts.Mob) world:add(e, cts.Mob)
end end
end end
return function(scheduler: std.Scheduler) local scheduler = require(std.scheduler)
local phases = scheduler.phases scheduler.SYSTEM(syncMobs)
local system_new = scheduler.systems.new
system_new(syncMobs, phases.RenderStepped) return 0
end

View file

@ -0,0 +1,44 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
local world = require(std.world)
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local function flip()
return math.random() >= 0.15
end
for i = 1, 2^8 do
local e = world:entity()
if flip() then
world:set(e, A, true)
end
if flip() then
world:set(e, B, true)
end
if flip() then
world:set(e, C, true)
end
if flip() then
world:set(e, D, true)
end
end
local function uncached()
for _ in world:query(A, B, C, D) do
end
end
local q = world:query(A, B, C, D):cached()
local function cached()
for _ in q do
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(uncached)
scheduler.SYSTEM(cached)
return 0

26
jecs.d.ts vendored
View file

@ -13,24 +13,32 @@ export type Tag = Entity<undefined>;
* A pair of entities * A pair of entities
* P is the type of the predicate, O is the type of the object, and V is the type of the value (defaults to P) * P is the type of the predicate, O is the type of the object, and V is the type of the value (defaults to P)
*/ */
export type Pair<P = undefined, O = undefined, V = P> = number & { export type Pair<P = Entity, O = Entity> = number & {
__jecs_pair_pred: P; __jecs_pair_pred: P;
__jecs_pair_obj: O; __jecs_pair_obj: O;
__jecs_pair_value: V; __jecs_pair_value: P extends Entity<undefined>
? O extends Entity<infer V>
? V
: never
: P extends Entity<infer V> ? V : never
}; };
/** /**
* Either an Entity or a Pair * Either an Entity or a Pair
*/ */
export type Id<T = unknown> = Entity<T> | Pair<unknown, unknown, T>; export type Id<T = unknown> = Entity<T> | Pair<Entity<T>, Entity<T>>;
type InferComponent<E> = E extends Entity<infer T>
? T
: E extends Pair
? E["__jecs_pair_value"]
: never;
type InferComponent<E> = E extends Id<infer T> ? T : never;
type FlattenTuple<T extends any[]> = T extends [infer U] ? U : LuaTuple<T>; type FlattenTuple<T extends any[]> = T extends [infer U] ? U : LuaTuple<T>;
type Nullable<T extends unknown[]> = { [K in keyof T]: T[K] | undefined }; type Nullable<T extends unknown[]> = { [K in keyof T]: T[K] | undefined };
type InferComponents<A extends Id[]> = { type InferComponents<A extends Id[]> = {
[K in keyof A]: InferComponent<A[K]>; [K in keyof A]: InferComponent<A[K]>;
}; };
type TupleForWorldGet = [Id] | [Id, Id] | [Id, Id, Id] | [Id, Id, Id, Id];
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>; type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
@ -136,7 +144,7 @@ export class World {
* @param components Target Components * @param components Target Components
* @returns Data associated with target components if it exists. * @returns Data associated with target components if it exists.
*/ */
get<T extends TupleForWorldGet>(id: Entity, ...components: T): FlattenTuple<Nullable<InferComponents<T>>>; get<T extends [Id] | [Id, Id] | [Id, Id, Id] | [Id, Id, Id, Id]>(id: Entity, ...components: T): FlattenTuple<Nullable<InferComponents<T>>>;
/** /**
* Returns whether the entity has the specified components. * Returns whether the entity has the specified components.
@ -176,7 +184,7 @@ export class World {
* @param obj The second entity (object) * @param obj The second entity (object)
* @returns The composite key (pair) * @returns The composite key (pair)
*/ */
export function pair<P, O, V = P>(pred: Entity<P>, obj: Entity<O>): Pair<P, O, V>; export function pair<P, O>(pred: Entity<P>, obj: Entity<O>): Pair<Entity<P>, Entity<O>>;
/** /**
* Checks if the entity is a composite key (pair) * Checks if the entity is a composite key (pair)
@ -190,14 +198,14 @@ export function IS_PAIR(value: Id): value is Pair;
* @param pair The pair to get the first entity from * @param pair The pair to get the first entity from
* @returns The first entity (predicate) of the pair * @returns The first entity (predicate) of the pair
*/ */
export function pair_first<P, O, V = P>(pair: Pair<P, O, V>): Entity<P>; export function pair_first<P, O>(pair: Pair<P, O>): Entity<P>;
/** /**
* Gets the second entity (object) of a pair * Gets the second entity (object) of a pair
* @param pair The pair to get the second entity from * @param pair The pair to get the second entity from
* @returns The second entity (object) of the pair * @returns The second entity (object) of the pair
*/ */
export function pair_second<P, O, V = P>(pair: Pair<P, O, V>): Entity<O>; export function pair_second<P, O>(pair: Pair<P, O>): Entity<O>;
export const OnAdd: Entity<(e: Entity) => void>; export const OnAdd: Entity<(e: Entity) => void>;
export const OnRemove: Entity<(e: Entity) => void>; export const OnRemove: Entity<(e: Entity) => void>;

577
jecs.luau
View file

@ -89,7 +89,9 @@ local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9 local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10 local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11 local EcsName = HI_COMPONENT_ID + 11
local EcsRest = HI_COMPONENT_ID + 12 local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14
local ECS_PAIR_FLAG = 0x8 local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10 local ECS_ID_FLAGS_MASK = 0x10
@ -168,7 +170,7 @@ local function _STRIP_GENERATION(e: i53): i24
end end
local function ECS_PAIR(pred: i53, obj: i53): i53 local function ECS_PAIR(pred: i53, obj: i53): i53
return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + FLAGS_ADD(--[[isPair]] true) :: i53 return ECS_COMBINE(ECS_ENTITY_T_LO(pred), ECS_ENTITY_T_LO(obj)) + FLAGS_ADD(--[[isPair]] true) :: i53
end end
local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record? local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record?
@ -242,12 +244,42 @@ end
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
local function ecs_pair_first(world, e) local function ecs_pair_first(world, e)
return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e)) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e))
end end
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
local function ecs_pair_second(world, e) local function ecs_pair_second(world, e)
return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e))
end
local function query_match(query, archetype: Archetype)
local records = archetype.records
local with = query.filter_with
for _, id in with do
if not records[id] then
return false
end
end
local without = query.filter_without
if without then
for _, id in without do
if records[id] then
return false
end
end
end
return true
end
local function find_observers(world: World, event, component): { Observer }?
local cache = world.observerable[event]
if not cache then
return nil
end
return cache[component] :: any
end end
local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24)
@ -549,6 +581,20 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
local columns = (table.create(length) :: any) :: { Column } local columns = (table.create(length) :: any) :: { Column }
local records: { ArchetypeRecord } = {} local records: { ArchetypeRecord } = {}
local archetype: Archetype = {
columns = columns,
entities = {},
id = archetype_id,
records = records,
type = ty,
types = id_types,
add = {},
remove = {},
refs = {} :: GraphEdge,
}
for i, componentId in id_types do for i, componentId in id_types do
local idr = id_record_ensure(world, componentId) local idr = id_record_ensure(world, componentId)
archetype_append_to_records(idr, archetype_id, records, componentId, i) archetype_append_to_records(idr, archetype_id, records, componentId, i)
@ -565,6 +611,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
local idr_t = id_record_ensure(world, t) local idr_t = id_record_ensure(world, t)
archetype_append_to_records(idr_t, archetype_id, records, t, i) archetype_append_to_records(idr_t, archetype_id, records, t, i)
end end
if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then
columns[i] = {} columns[i] = {}
else else
@ -572,18 +619,17 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
end end
end end
local archetype: Archetype = { for _, id in id_types do
columns = columns, local observer_list = find_observers(world, EcsOnArchetypeCreate, id)
entities = {}, if not observer_list then
id = archetype_id, continue
records = records, end
type = ty, for _, observer in observer_list do
types = id_types, if query_match(observer.query, archetype) then
observer.callback(archetype)
add = {}, end
remove = {}, end
refs = {} :: GraphEdge, end
}
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[archetype_id] = archetype world.archetypes[archetype_id] = archetype
@ -626,13 +672,13 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number
end end
local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype
local types = node.types local id_types = node.types
-- Component IDs are added incrementally, so inserting and sorting -- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion -- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array. -- point in the types array.
local dst = table.clone(node.types) :: { i53 } local dst = table.clone(node.types) :: { i53 }
local at = find_insert(types, id) local at = find_insert(id_types, id)
if at == -1 then if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it -- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype. -- directly instead of needing to hash types for a lookup to the archetype.
@ -644,13 +690,13 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch
end end
local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype
local types = node.types local id_types = node.types
local at = table.find(types, id) local at = table.find(id_types, id)
if at == nil then if at == nil then
return node return node
end end
local dst = table.clone(types) local dst = table.clone(id_types)
table.remove(dst, at) table.remove(dst, at)
return archetype_ensure(world, dst) return archetype_ensure(world, dst)
@ -1006,6 +1052,18 @@ local function archetype_destroy(world: World, archetype: Archetype)
world.archetypeIndex[archetype.type] = nil :: any world.archetypeIndex[archetype.type] = nil :: any
local records = archetype.records local records = archetype.records
for id in records do
local observer_list = find_observers(world, EcsOnArchetypeDelete, id)
if not observer_list then
continue
end
for _, observer in observer_list do
if query_match(observer.query, archetype) then
observer.callback(archetype)
end
end
end
for id in records do for id in records do
local idr = component_index[id] local idr = component_index[id]
idr.cache[archetype_id] = nil :: any idr.cache[archetype_id] = nil :: any
@ -1064,23 +1122,30 @@ do
local idr = component_index[delete] local idr = component_index[delete]
if idr then if idr then
local children = {} local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for archetype_id in idr.cache do for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id] local idr_archetype = archetypes[archetype_id]
for i, child in idr_archetype.entities do local entities = idr_archetype.entities
table.insert(children, child) local n = #entities
for i = n, 1, -1 do
world_delete(world, entities[i])
end end
end end
local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do
-- Cascade deletion to children
world_delete(world, child)
end
else else
for _, child in children do for archetype_id in idr.cache do
world_remove(world, child, delete) local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
world_remove(world, entities[i], delete)
end
end
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
archetype_destroy(world, idr_archetype)
end end
end end
end end
@ -1167,7 +1232,7 @@ local EMPTY_QUERY = {
setmetatable(EMPTY_QUERY, EMPTY_QUERY) setmetatable(EMPTY_QUERY, EMPTY_QUERY)
local function query_iter_init(query): () -> (number, ...any) local function query_iter_init(query: QueryInner): () -> (number, ...any)
local world_query_iter_next local world_query_iter_next
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
@ -1246,7 +1311,7 @@ local function query_iter_init(query): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
local records = archetype.records records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
end end
@ -1269,7 +1334,7 @@ local function query_iter_init(query): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
local records = archetype.records records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
b = columns[records[B].column] b = columns[records[B].column]
end end
@ -1293,7 +1358,7 @@ local function query_iter_init(query): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
local records = archetype.records records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
b = columns[records[B].column] b = columns[records[B].column]
c = columns[records[C].column] c = columns[records[C].column]
@ -1318,7 +1383,7 @@ local function query_iter_init(query): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
local records = archetype.records records = archetype.records
a = columns[records[A].column] a = columns[records[A].column]
b = columns[records[B].column] b = columns[records[B].column]
c = columns[records[C].column] c = columns[records[C].column]
@ -1345,7 +1410,7 @@ local function query_iter_init(query): () -> (number, ...any)
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
columns = archetype.columns columns = archetype.columns
local records = archetype.records records = archetype.records
if not F then if not F then
a = columns[records[A].column] a = columns[records[A].column]
@ -1393,7 +1458,6 @@ local function query_iter_init(query): () -> (number, ...any)
return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
end end
local records = archetype.records
for j, id in ids do for j, id in ids do
queryOutput[j] = columns[records[id].column][row] queryOutput[j] = columns[records[id].column][row]
end end
@ -1414,65 +1478,64 @@ local function query_iter(query): () -> (number, ...any)
return query_next return query_next
end end
local function query_without(query: { compatible_archetypes: { Archetype } }, ...) local function query_without(query: QueryInner, ...: i53)
local without = { ... }
query.filter_without = without
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...)
for i = #compatible_archetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local shouldRemove = false local matches = true
for j = 1, N do for _, id in without do
local id = select(j, ...)
if records[id] then if records[id] then
shouldRemove = true matches = false
break break
end end
end end
if shouldRemove then if matches then
continue
end
local last = #compatible_archetypes local last = #compatible_archetypes
if last ~= i then if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last] compatible_archetypes[i] = compatible_archetypes[last]
end end
compatible_archetypes[last] = nil :: any compatible_archetypes[last] = nil :: any
end end
end
if #compatible_archetypes == 0 then
return EMPTY_QUERY
end
return query :: any return query :: any
end end
local function query_with(query: { compatible_archetypes: { Archetype } }, ...) local function query_with(query: QueryInner, ...: i53)
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...) local with = { ... }
query.filter_with = with
for i = #compatible_archetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local shouldRemove = false local matches = true
for j = 1, N do for _, id in with do
local id = select(j, ...)
if not records[id] then if not records[id] then
shouldRemove = true matches = false
break break
end end
end end
if shouldRemove then if matches then
continue
end
local last = #compatible_archetypes local last = #compatible_archetypes
if last ~= i then if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last] compatible_archetypes[i] = compatible_archetypes[last]
end end
compatible_archetypes[last] = nil :: any compatible_archetypes[last] = nil :: any
end end
end
if #compatible_archetypes == 0 then
return EMPTY_QUERY
end
return query :: any return query :: any
end end
@ -1483,6 +1546,310 @@ local function query_archetypes(query)
return query.compatible_archetypes return query.compatible_archetypes
end end
local function query_cached(query: QueryInner)
local archetypes = query.compatible_archetypes
local world = query.world :: World
-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
-- because the event will be emitted for all components of that Archetype.
local first = query.ids[1]
local observerable = world.observerable
local on_create_action = observerable[EcsOnArchetypeCreate]
if not on_create_action then
on_create_action = {}
observerable[EcsOnArchetypeCreate] = on_create_action
end
local query_cache_on_create = on_create_action[first]
if not query_cache_on_create then
query_cache_on_create = {}
on_create_action[first] = query_cache_on_create
end
local on_delete_action = observerable[EcsOnArchetypeDelete]
if not on_delete_action then
on_delete_action = {}
observerable[EcsOnArchetypeDelete] = on_delete_action
end
local query_cache_on_delete = on_delete_action[first]
if not query_cache_on_delete then
query_cache_on_delete = {}
on_delete_action[first] = query_cache_on_delete
end
local function on_create_callback(archetype)
table.insert(archetypes, archetype)
end
local function on_delete_callback(archetype)
local i = table.find(archetypes, archetype) :: number
local n = #archetypes
archetypes[i] = archetypes[n]
archetypes[n] = nil
end
local with = query.filter_with
local ids = query.ids
if with then
table.move(ids, 1, #ids, #with, with)
else
query.filter_with = ids
end
local observer_for_create = { query = query, callback = on_create_callback }
local observer_for_delete = { query = query, callback = on_delete_callback }
table.insert(query_cache_on_create, observer_for_create)
table.insert(query_cache_on_delete, observer_for_delete)
local compatible_archetypes = query.compatible_archetypes
local lastArchetype = 1
local A, B, C, D, E, F, G, H, I = unpack(ids)
local a: Column, b: Column, c: Column, d: Column
local e: Column, f: Column, g: Column, h: Column
local world_query_iter_next
local columns: { Column }
local entities: { number }
local i: number
local archetype: Archetype
local records: { ArchetypeRecord }
local function cached_query_iter()
lastArchetype = 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return NOOP
end
entities = archetype.entities
i = #entities
records = archetype.records
columns = archetype.columns
if not B then
a = columns[records[A].column]
elseif not C then
a = columns[records[A].column]
b = columns[records[B].column]
elseif not D then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
elseif not E then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
elseif not F then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
elseif not G then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
elseif not H then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
elseif not I then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
h = columns[records[H].column]
end
return world_query_iter_next
end
if not B then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
records = archetype.records
a = columns[records[A].column]
end
local row = i
i -= 1
return entityId, a[row]
end
elseif not C then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
end
local row = i
i -= 1
return entityId, a[row], b[row]
end
elseif not D then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
end
local row = i
i -= 1
return entityId, a[row], b[row], c[row]
end
elseif not E then
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
records = archetype.records
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
end
local row = i
i -= 1
return entityId, a[row], b[row], c[row], d[row]
end
else
local queryOutput = {}
function world_query_iter_next(): any
local entityId = entities[i]
while entityId == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
entityId = entities[i]
columns = archetype.columns
records = archetype.records
if not F then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
elseif not G then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
elseif not H then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
elseif not I then
a = columns[records[A].column]
b = columns[records[B].column]
c = columns[records[C].column]
d = columns[records[D].column]
e = columns[records[E].column]
f = columns[records[F].column]
g = columns[records[G].column]
h = columns[records[H].column]
end
end
local row = i
i -= 1
if not F then
return entityId, a[row], b[row], c[row], d[row], e[row]
elseif not G then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row]
elseif not H then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row]
elseif not I then
return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row]
end
for j, id in ids do
queryOutput[j] = columns[records[id].column][row]
end
return entityId, unpack(queryOutput)
end
end
local cached_query = query :: any
cached_query.archetypes = query_archetypes
cached_query.__iter = cached_query_iter
cached_query.iter = cached_query_iter
setmetatable(cached_query, cached_query)
return cached_query
end
local Query = {} local Query = {}
Query.__index = Query Query.__index = Query
Query.__iter = query_iter Query.__iter = query_iter
@ -1490,6 +1857,7 @@ Query.iter = query_iter_init
Query.without = query_without Query.without = query_without
Query.with = query_with Query.with = query_with
Query.archetypes = query_archetypes Query.archetypes = query_archetypes
Query.cached = query_cached
local function world_query(world: World, ...) local function world_query(world: World, ...)
local compatible_archetypes = {} local compatible_archetypes = {}
@ -1502,10 +1870,16 @@ local function world_query(world: World, ...)
local idr: IdRecord? local idr: IdRecord?
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
local q = setmetatable({
ids = ids,
compatible_archetypes = compatible_archetypes,
world = world,
}, Query)
for _, id in ids do for _, id in ids do
local map = componentIndex[id] local map = componentIndex[id]
if not map then if not map then
return EMPTY_QUERY return q
end end
if idr == nil or map.size < idr.size then if idr == nil or map.size < idr.size then
@ -1514,7 +1888,7 @@ local function world_query(world: World, ...)
end end
if not idr then if not idr then
return EMPTY_QUERY return q
end end
for archetype_id in idr.cache do for archetype_id in idr.cache do
@ -1542,15 +1916,6 @@ local function world_query(world: World, ...)
compatible_archetypes[length] = compatibleArchetype compatible_archetypes[length] = compatibleArchetype
end end
if length == 0 then
return EMPTY_QUERY
end
local q = setmetatable({
compatible_archetypes = compatible_archetypes,
ids = ids,
}, Query) :: any
return q return q
end end
@ -1736,6 +2101,7 @@ function World.new()
nextComponentId = 0 :: number, nextComponentId = 0 :: number,
nextEntityId = 0 :: number, nextEntityId = 0 :: number,
ROOT_ARCHETYPE = (nil :: any) :: Archetype, ROOT_ARCHETYPE = (nil :: any) :: Archetype,
observerable = {},
}, World) :: any }, World) :: any
self.ROOT_ARCHETYPE = archetype_create(self, {}, "") self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
@ -1775,16 +2141,26 @@ function World.new()
return self return self
end end
export type Id<T = unknown> = Entity<T> type Id<T = unknown> =
| (number & { __jecs_pair_value: T })
| (number & { __T: T })
type function ecs_entity_t(entity) export type Pair<P = Entity, O = Entity> = number & {
return entity:components()[2]:readproperty(types.singleton("__T")) __jecs_pair_value: ecs_id_t<ecs_pair_t<P, O>>
}
type function ecs_id_t(entity)
local ty = entity:components()[2]
local __T = ty:readproperty(types.singleton("__T"))
if not __T then
return ty:readproperty(types.singleton("__jecs_pair_value"))
end
return __T
end end
export type function Pair(first, second) type function ecs_pair_t(first, second)
local thing = first:components()[2] local ty = first:components()[2]
if ty:readproperty(types.singleton("__T")):is("nil") then
if thing:readproperty(types.singleton("__T")):is("nil") then
return second return second
else else
return first return first
@ -1793,17 +2169,32 @@ end
type Item<T...> = (self: Query<T...>) -> (Entity, T...) type Item<T...> = (self: Query<T...>) -> (Entity, T...)
export type Entity<T = unknown> = number & { __T: T } export type Entity<T = nil> = number & { __T: T }
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...) type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
type Query<T...> = typeof(setmetatable({}, { export type Query<T...> = typeof(setmetatable({}, {
__iter = (nil :: any) :: Iter<T...>, __iter = (nil :: any) :: Iter<T...>,
})) & { })) & {
iter: Iter<T...>, iter: Iter<T...>,
with: (self: Query<T...>, ...i53) -> Query<T...>, with: (self: Query<T...>, ...Id) -> Query<T...>,
without: (self: Query<T...>, ...i53) -> Query<T...>, without: (self: Query<T...>, ...Id) -> Query<T...>,
archetypes: (self: Query<T...>) -> { Archetype }, archetypes: (self: Query<T...>) -> { Archetype },
cached: (self: Query<T...>) -> Query<T...>,
}
type QueryInner = {
compatible_archetypes: { Archetype },
filter_with: { i53 }?,
filter_without: { i53 }?,
ids: { i53 },
world: {}, -- Downcasted to be serializable by the analyzer
next: () -> Item<any>
}
type Observer = {
callback: (archetype: Archetype) -> (),
query: QueryInner,
} }
export type World = { export type World = {
@ -1816,6 +2207,8 @@ export type World = {
nextComponentId: number, nextComponentId: number,
nextEntityId: number, nextEntityId: number,
nextArchetypeId: number, nextArchetypeId: number,
observerable: { [i53]: { [i53]: { { query: Query<i53> } } } },
} & { } & {
--- Creates a new entity --- Creates a new entity
entity: (self: World) -> Entity, entity: (self: World) -> Entity,
@ -1858,14 +2251,14 @@ export type World = {
children: (self: World, id: Id) -> () -> Entity, children: (self: World, id: Id) -> () -> Entity,
--- Searches the world for entities that match a given query --- Searches the world for entities that match a given query
query: (<A>(World, A) -> Query<ecs_entity_t<A>>) query: (<A>(World, A) -> Query<ecs_id_t<A>>)
& (<A, B>(World, A, B) -> Query<ecs_entity_t<A>, ecs_entity_t<B>>) & (<A, B>(World, A, B) -> Query<ecs_id_t<A>, ecs_id_t<B>>)
& (<A, B, C>(World, A, B, C) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>>) & (<A, B, C>(World, A, B, C) -> Query<ecs_id_t<A>, ecs_id_t<B>, ecs_id_t<C>>)
& (<A, B, C, D>(World, A, B, C, D) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>>) & (<A, B, C, D>(World, A, B, C, D) -> Query<ecs_id_t<A>, ecs_id_t<B>, ecs_id_t<C>, ecs_id_t<D>>)
& (<A, B, C, D, E>(World, A, B, C, D, E) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>>) & (<A, B, C, D, E>(World, A, B, C, D, E) -> Query<ecs_id_t<A>, ecs_id_t<B>, ecs_id_t<C>, ecs_id_t<D>, ecs_id_t<E>>)
& (<A, B, C, D, E, F>(World, A, B, C, D, E, F) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>, ecs_entity_t<F>>) & (<A, B, C, D, E, F>(World, A, B, C, D, E, F) -> Query<ecs_id_t<A>, ecs_id_t<B>, ecs_id_t<C>, ecs_id_t<D>, ecs_id_t<E>, ecs_id_t<F>>)
& (<A, B, C, D, E, F, G>(World, A, B, C, D, E, F, G) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>, ecs_entity_t<F>, ecs_entity_t<G>>) & (<A, B, C, D, E, F, G>(World, A, B, C, D, E, F, G) -> Query<ecs_id_t<A>, ecs_id_t<B>, ecs_id_t<C>, ecs_id_t<D>, ecs_id_t<E>, ecs_id_t<F>, ecs_id_t<G>>)
& (<A, B, C, D, E, F, G, H>(World, A, B, C, D, E, F, G, H) -> Query<ecs_entity_t<A>, ecs_entity_t<B>, ecs_entity_t<C>, ecs_entity_t<D>, ecs_entity_t<E>, ecs_entity_t<F>, ecs_entity_t<G>, ecs_entity_t<H>>) & (<A, B, C, D, E, F, G, H>(World, A, B, C, D, E, F, G, H) -> Query<ecs_id_t<A>, ecs_id_t<B>, ecs_id_t<C>, ecs_id_t<D>, ecs_id_t<E>, ecs_id_t<F>, ecs_id_t<G>, ecs_id_t<H>>)
} }
return { return {

View file

@ -1,6 +1,6 @@
{ {
"name": "@rbxts/jecs", "name": "@rbxts/jecs",
"version": "0.4.1-rc.0", "version": "0.5.0",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "repository": {

View file

@ -0,0 +1,13 @@
# Read the input file line by line
while IFS= read -r line; do
# Use regex to capture file name, line number, column number, and message
if [[ $line =~ ^(.+)\(([0-9]+),([0-9]+)\):\ (.+)$ ]]; then
file="${BASH_REMATCH[1]}"
line_number="${BASH_REMATCH[2]}"
column_number="${BASH_REMATCH[3]}"
message="${BASH_REMATCH[4]}"
# Format output for GitHub Actions
echo "::warning file=$file,line=$line_number,col=$column_number::${message}"
fi
done < "$1"

View file

@ -59,9 +59,14 @@ local function debug_world_inspect(world: World)
records = records, records = records,
row = row, row = row,
tuple = tuple, tuple = tuple,
columns = columns
} }
end end
local function name(world, e)
return world:get(e, jecs.Name)
end
TEST("archetype", function() TEST("archetype", function()
local archetype_append_to_records = jecs.archetype_append_to_records local archetype_append_to_records = jecs.archetype_append_to_records
local id_record_ensure = jecs.id_record_ensure local id_record_ensure = jecs.id_record_ensure
@ -358,6 +363,39 @@ TEST("world:add()", function()
end) end)
TEST("world:query()", function() TEST("world:query()", function()
do CASE "cached"
local world = world_new()
local Foo = world:component()
local Bar = world:component()
local Baz = world:component()
local e = world:entity()
local q = world:query(Foo, Bar):without(Baz):cached()
world:set(e, Foo, true)
world:set(e, Bar, false)
local i = 0
for _, e in q:iter() do
i=1
end
CHECK(i == 1)
for _, e in q:iter() do
i=2
end
CHECK(i == 2)
for _, e in q do
i=3
end
CHECK(i == 3)
for _, e in q do
i=4
end
CHECK(i == 4)
CHECK(#q:archetypes() == 1)
CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")]))
world:delete(Foo)
CHECK(#q:archetypes() == 0)
end
do CASE("multiple iter") do CASE("multiple iter")
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
@ -813,40 +851,6 @@ TEST("world:query()", function()
CHECK(withoutCount == 0) CHECK(withoutCount == 0)
end end
do
CASE("Empty Query")
do
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:add(e1, A)
local query = world:query(B)
CHECK(query:without() == query)
CHECK(query:with() == query)
-- They always return the same EMPTY_LIST
CHECK(query:archetypes() == world:query(B):archetypes())
end
do
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:add(e1, A)
local count = 0
for id in world:query(B) do
count += 1
end
CHECK(count == 0)
end
end
do do
CASE("without") CASE("without")
do do
@ -1205,25 +1209,38 @@ TEST("world:delete", function()
end) end)
TEST("world:target", function() TEST("world:target", function()
do do CASE("nth index")
CASE("nth index")
local world = world_new() local world = world_new()
local A = world:component() local A = world:component()
world:set(A, jecs.Name, "A")
local B = world:component() local B = world:component()
world:set(B, jecs.Name, "B")
local C = world:component() local C = world:component()
world:set(C, jecs.Name, "C")
local D = world:component() local D = world:component()
world:set(D, jecs.Name, "D")
local E = world:component()
world:set(E, jecs.Name, "E")
local e = world:entity() local e = world:entity()
world:add(e, pair(A, B)) world:add(e, pair(A, B))
world:add(e, pair(A, C)) world:add(e, pair(A, C))
world:add(e, pair(A, D))
world:add(e, pair(A, E))
world:add(e, pair(B, C)) world:add(e, pair(B, C))
world:add(e, pair(B, D)) world:add(e, pair(B, D))
world:add(e, pair(C, D)) world:add(e, pair(C, D))
CHECK(pair(A, B) < pair(A, C)) CHECK(pair(A, B) < pair(A, C))
CHECK(pair(A, E) < pair(B, C))
local records = debug_world_inspect(world).records(e)
CHECK(jecs.pair_first(world, pair(B, C)) == B)
CHECK(records[pair(B, C)].column > records[pair(A, E)].column)
CHECK(world:target(e, A, 0) == B) CHECK(world:target(e, A, 0) == B)
CHECK(world:target(e, A, 1) == C) CHECK(world:target(e, A, 1) == C)
CHECK(world:target(e, A, 2) == D)
CHECK(world:target(e, A, 3) == E)
CHECK(world:target(e, B, 0) == C) CHECK(world:target(e, B, 0) == C)
CHECK(world:target(e, B, 1) == D) CHECK(world:target(e, B, 1) == D)
CHECK(world:target(e, C, 0) == D) CHECK(world:target(e, C, 0) == D)

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ukendio/jecs" name = "ukendio/jecs"
version = "0.4.1-rc.0" version = "0.5.1"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"