Compare commits

...

9 commits

Author SHA1 Message Date
nonamie
a24db62ca3
Merge e8575a0db6 into ec4fa3ff3e 2024-12-26 07:24:46 +02: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
nonamie
e8575a0db6 infer invariants 2024-12-14 20:22:51 +03:00
nonamie
15d5c44085 fix TData infer issue 2024-12-14 18:46:19 +03:00
nonamie
059a49132b remove commented Id/Pair types 2024-12-14 11:53:11 +03:00
nonamie
e28a6bc819 fix "set" method defition 2024-12-14 11:30:50 +03:00
nonamie
edf2e642c4 type refactors 2024-12-09 14:28:08 +03:00
nonamie
deca401970 refactor types 2024-12-09 14:15:28 +03:00
21 changed files with 4965 additions and 4695 deletions

View file

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

View file

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

View file

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

@ -16,4 +16,4 @@ local function interval(s)
return throttle
end
return interval
return interval

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

View file

@ -6,13 +6,12 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(game:GetService("ServerScriptService").net)
local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local ref = require(std.ref)
local interval = require(std.interval)
local std = require(ReplicatedStorage.std)
local ref = std.ref
local interval = std.interval
local world: std.World = std.world
local cts = std.components
local world = require(std.world)
local cts = require(std.components)
local Mob = cts.Mob
local Transform = cts.Transform
@ -20,13 +19,26 @@ local Velocity = cts.Velocity
local Player = cts.Player
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 targets = {}
for _, character in world:query(Character):with(Player):iter() do
for _, character in characters do
table.insert(targets, (character.PrimaryPart :: Part).Position)
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 p = cf.Position
@ -53,21 +65,24 @@ end
local throttle = interval(5)
local function spawnMobs()
local function spawnMobs()
if throttle() then
local p = Vector3.new(0, 5, 0)
local cf = CFrame.new(p)
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
return function(scheduler: std.Scheduler)
local phases = scheduler.phases
local system_new = scheduler.systems.new
system_new(mobsMove, phases.Heartbeat)
system_new(spawnMobs, phases.Heartbeat)
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(spawnMobs)
scheduler.SYSTEM(mobsMove)
return 0

View file

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

View file

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

View file

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

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

139
jecs.d.ts vendored
View file

@ -1,40 +1,49 @@
/*
* The base type for entities.
* This type indicates that the entity cannot be used to `tag` other entities
* and cannot be used used as a component to associate any kind of data with itself.
*/
export type Id = number & {
readonly __nominal_Id: unique symbol;
};
/*
* An entity with no associated data when used as a component.
* This entity however could still be used to 'tag' other entities.
*
* You could go further and downcast this type to `Id`
* indicating that the entity is intended to only store other entities.
*/
export type Tag = Id & {
readonly __nominal_Tag: unique symbol;
};
/**
* A unique identifier in the world, entity.
* The generic type T defines the data type when this entity is used as a component
* This identifier is associated with `TData` data when this entity is used as a component.
*/
export type Entity<T = undefined | unknown> = number & { __jecs_value: T };
/**
* An entity with no associated data when used as a component
*/
export type Tag = Entity<undefined>;
/**
* 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)
*/
export type Pair<P = undefined, O = undefined, V = P> = number & {
__jecs_pair_pred: P;
__jecs_pair_obj: O;
__jecs_pair_value: V;
export type Entity<TData = unknown> = Tag & {
readonly __nominal_Entity: unique symbol;
readonly __type_TData: TData;
};
/**
* Either an Entity or a Pair
*/
export type Id<T = unknown> = Entity<T> | Pair<unknown, unknown, T>;
type InferComponent<TValue> = TValue extends Entity<infer TData> ? TData : never;
type InferComponent<E> = E extends Id<infer T> ? T : never;
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 InferComponents<A extends Id[]> = {
[K in keyof A]: InferComponent<A[K]>;
type FlattenTuple<TItems extends any[]> = TItems extends [infer TValue] ? TValue : LuaTuple<TItems>;
type Undefinedable<TItems extends any[]> = {
[TKey in keyof TItems]: TItems[TKey] | undefined;
};
type TupleForWorldGet = [Id] | [Id, Id] | [Id, Id, Id] | [Id, Id, Id, Id];
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
type InferComponents<TComponents extends Entity[]> = {
[TKey in keyof TComponents]: InferComponent<TComponents[TKey]>;
};
export type Query<T extends unknown[]> = {
type TupleForWorldGet = [Entity] | [Entity, Entity] | [Entity, Entity, Entity] | [Entity, Entity, Entity, Entity];
type Iter<T extends any[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
export type Query<T extends any[]> = {
/**
* Returns an iterator that returns a tuple of an entity and queried components
*/
@ -45,14 +54,14 @@ export type Query<T extends unknown[]> = {
* @param components The components to include
* @returns Modified Query
*/
with(...components: Id[]): Query<T>;
with(...components: Tag[]): Query<T>;
/**
* Modifies the Query to exclude specified components
* @param components The components to exclude
* @returns Modified Query
*/
without(...components: Id[]): Query<T>;
without(...components: Tag[]): Query<T>;
} & Iter<T>;
export class World {
@ -62,26 +71,31 @@ export class World {
constructor();
/**
* Creates a new entity
* Creates a new entity.
*
* If your intention is to use this entity as a component associated with some data
* then you should provide the type parameter.
*
* @returns Entity
*/
entity(): Tag;
entity<TData = never>(): [TData] extends [never] ? Id : Entity<TData>;
/**
* Creates a new entity located in the first 256 ids.
*
* These should be used for static components for fast access.
* @returns Entity<T>
* @returns Entity<TData>
*/
component<T = unknown>(): Entity<T>;
component<TData = unknown>(): Entity<TData>;
/**
* Gets the target of a relationship. For example, when a user calls
* `world.target(entity, ChildOf(parent))`, you will obtain the parent entity.
* `world.target(entity, ChildOf)`, you will obtain the parent entity.
* @param entity Entity
* @param relation The Relationship
* @returns The Parent Entity if it exists
*/
target(entity: Entity, relation: Entity): Entity | undefined;
target(entity: Id, relation: Entity): Entity | undefined;
/**
* Gets the target of a relationship at a specific index.
@ -92,26 +106,27 @@ export class World {
* @param index Target index
* @returns The Parent Entity if it exists
*/
target(entity: Entity, relation: Entity, index: number): Entity | undefined;
target(entity: Id, relation: Entity, index: number): Entity | undefined;
/**
* Clears an entity from the world
* Clears an entity from the world.
* @param entity Entity to be cleared
*/
clear(entity: Entity): void;
clear(entity: Id): void;
/**
* Deletes an entity and all its related components and relationships
* Deletes an entity and all its related components and relationships.
* @param entity Entity to be destroyed
*/
delete(entity: Entity): void;
delete(entity: Id): void;
/**
* Adds a component to the entity with no value
* Adds a component to the entity with no value.
*
* @param entity Target Entity
* @param component Component
* @param tag Tag
*/
add(entity: Entity, component: Id): void;
add(entity: Id, tag: Tag): void;
/**
* Assigns a value to a component on the given entity
@ -119,40 +134,41 @@ export class World {
* @param component Target Component
* @param value Component Value
*/
set<E extends Id<unknown>>(entity: Entity, component: E, value: InferComponent<E>): void;
set<TData>(entity: Id, component: Entity<TData>, value: TData): void;
/**
* Removes a component from the given entity
* @param entity Target Entity
* @param component Target Component
*/
remove(entity: Entity, component: Id): void;
remove(entity: Id, component: Tag): void;
/**
* Retrieves the values of specified components for an entity.
* Some values may not exist when called.
* A maximum of 4 components are allowed at a time.
* @param id Target Entity
* @param entity Target Entity
* @param components Target Components
* @returns Data associated with target components if it exists.
*/
get<T extends TupleForWorldGet>(id: Entity, ...components: T): FlattenTuple<Nullable<InferComponents<T>>>;
get<TComponents extends TupleForWorldGet>(entity: Id, ...components: TComponents): FlattenTuple<Undefinedable<InferComponents<TComponents>>>;
/**
* Returns whether the entity has the specified components.
* A maximum of 4 components are allowed at a time.
*
* @param entity Target Entity
* @param components Target Components
* @returns If the entity contains the components
*/
has(entity: Entity, ...components: Id[]): boolean;
has(entity: Id, ...components: Tag[]): boolean;
/**
* Checks if an entity exists in the world
* @param entity Entity to check
* @returns Whether the entity exists in the world
*/
contains(entity: Entity): boolean;
contains(entity: Id): boolean;
/**
* Get parent (target of ChildOf relationship) for entity.
@ -160,48 +176,51 @@ export class World {
* @param entity Target Entity
* @returns Parent Entity or undefined
*/
parent(entity: Entity): Entity | undefined;
parent(entity: Id): Entity | undefined;
/**
* Searches the world for entities that match a given query
* @param components Queried Components
* @returns Query
*/
query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
query<TComponents extends Entity[]>(...components: TComponents): Query<InferComponents<TComponents>>;
}
/**
* Creates a composite key (pair)
* Creates a composite key (pair).
*
* @param pred The first entity (predicate)
* @param obj The second entity (object)
* @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<TPredicate, TObject>(pred: Entity<TPredicate>, obj: Entity<TObject>): Entity<TPredicate>;
/**
* Checks if the entity is a composite key (pair)
* @param value The entity to check
* @returns If the entity is a pair
*/
export function IS_PAIR(value: Id): value is Pair;
export function IS_PAIR(value: Id): value is Entity;
/**
* Gets the first entity (predicate) of a pair
* @param pair The pair to get the first entity from
* @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(world: World, pair: Entity): Entity;
/**
* Gets the second entity (object) of a pair
* @param pair The pair to get the second entity from
* @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(world: World, pair: Entity): Entity;
export const OnAdd: Entity<(e: Entity) => void>;
export const OnRemove: Entity<(e: Entity) => void>;
export const OnSet: Entity<(e: Entity, value: unknown) => void>;
export const Component: Entity;
export const OnAdd: Entity<(entity: Entity) => void>;
export const OnRemove: Entity<(entity: Entity) => void>;
export const OnSet: Entity<(entity: Entity, value: unknown) => void>;
export const ChildOf: Entity;
export const Wildcard: Entity;
export const w: Entity;

678
jecs.luau
View file

@ -78,30 +78,32 @@ type EntityIndex = {
local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
-- stylua: ignore start
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnSet = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6
local EcsOnDelete = HI_COMPONENT_ID + 7
local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11
local EcsRest = HI_COMPONENT_ID + 12
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnSet = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6
local EcsOnDelete = HI_COMPONENT_ID + 7
local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11
local EcsArchetypeCreate = HI_COMPONENT_ID + 12
local EcsArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14
local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_ID_DELETE = 0b0000_0001
local ECS_ID_IS_TAG = 0b0000_0010
local ECS_ID_HAS_ON_ADD = 0b0000_0100
local ECS_ID_HAS_ON_SET = 0b0000_1000
local ECS_ID_HAS_ON_REMOVE = 0b0001_0000
local ECS_ID_MASK = 0b0000_0000
local ECS_ID_DELETE = 0b0000_0001
local ECS_ID_IS_TAG = 0b0000_0010
local ECS_ID_HAS_ON_ADD = 0b0000_0100
local ECS_ID_HAS_ON_SET = 0b0000_1000
local ECS_ID_HAS_ON_REMOVE = 0b0001_0000
local ECS_ID_MASK = 0b0000_0000
-- stylua: ignore end
local NULL_ARRAY = table.freeze({}) :: Column
@ -168,7 +170,7 @@ local function _STRIP_GENERATION(e: i53): i24
end
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
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
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
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
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
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 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
local idr = id_record_ensure(world, componentId)
archetype_append_to_records(idr, archetype_id, records, componentId, i)
@ -572,18 +618,17 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
end
end
local archetype: Archetype = {
columns = columns,
entities = {},
id = archetype_id,
records = records,
type = ty,
types = id_types,
add = {},
remove = {},
refs = {} :: GraphEdge,
}
for _, id in id_types do
local observer_list = find_observers(world, EcsArchetypeCreate, 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
world.archetypeIndex[ty] = archetype
world.archetypes[archetype_id] = archetype
@ -626,13 +671,13 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number
end
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
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
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 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.
@ -644,13 +689,13 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch
end
local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype
local types = node.types
local at = table.find(types, id)
local id_types = node.types
local at = table.find(id_types, id)
if at == nil then
return node
end
local dst = table.clone(types)
local dst = table.clone(id_types)
table.remove(dst, at)
return archetype_ensure(world, dst)
@ -1006,6 +1051,18 @@ local function archetype_destroy(world: World, archetype: Archetype)
world.archetypeIndex[archetype.type] = nil :: any
local records = archetype.records
for id in records do
local observer_list = find_observers(world, EcsArchetypeDelete, 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
local idr = component_index[id]
idr.cache[archetype_id] = nil :: any
@ -1064,23 +1121,30 @@ do
local idr = component_index[delete]
if idr then
local children = {}
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
for i, child in idr_archetype.entities do
table.insert(children, child)
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)
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
world_delete(world, entities[i])
end
end
else
for _, child in children do
world_remove(world, child, delete)
for archetype_id in idr.cache do
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
@ -1167,7 +1231,7 @@ local 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 compatible_archetypes = query.compatible_archetypes
@ -1246,7 +1310,325 @@ local function query_iter_init(query): () -> (number, ...any)
i = #entities
entityId = entities[i]
columns = archetype.columns
local records = archetype.records
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
local records = archetype.records
for j, id in ids do
queryOutput[j] = columns[records[id].column][row]
end
return entityId, unpack(queryOutput)
end
end
query.next = world_query_iter_next
return world_query_iter_next
end
local function query_iter(query): () -> (number, ...any)
local query_next = query.next
if not query_next then
query_next = query_iter_init(query)
end
return query_next
end
local function query_without(query: QueryInner, ...: i53)
local without = { ... }
query.filter_without = without
local compatible_archetypes = query.compatible_archetypes
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local records = archetype.records
local matches = true
for _, id in without do
if records[id] then
matches = false
break
end
end
if matches then
continue
end
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end
return query :: any
end
local function query_with(query: QueryInner, ...: i53)
local compatible_archetypes = query.compatible_archetypes
local with = { ... }
query.filter_with = with
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local records = archetype.records
local matches = true
for _, id in with do
if not records[id] then
matches = false
break
end
end
if matches then
continue
end
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end
return query :: any
end
-- Meant for directly iterating over archetypes to minimize
-- function call overhead. Should not be used unless iterating over
-- hundreds of thousands of entities in bulk.
local function query_archetypes(query)
return query.compatible_archetypes
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[EcsArchetypeCreate]
if not on_create_action then
on_create_action = {}
observerable[EcsArchetypeCreate] = 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[EcsArchetypeDelete]
if not on_delete_action then
on_delete_action = {}
observerable[EcsArchetypeDelete] = 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: { i53 }
local i: number
local archetype: Archetype
local records: { ArchetypeRecord }
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
@ -1402,85 +1784,71 @@ local function query_iter_init(query): () -> (number, ...any)
end
end
query.next = world_query_iter_next
return world_query_iter_next
end
local function query_iter(query): () -> (number, ...any)
local query_next = query.next
if not query_next then
query_next = query_iter_init(query)
end
return query_next
end
local function query_without(query: { compatible_archetypes: { Archetype } }, ...)
local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...)
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local records = archetype.records
local shouldRemove = false
for j = 1, N do
local id = select(j, ...)
if records[id] then
shouldRemove = true
break
end
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
if shouldRemove then
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end
return world_query_iter_next
end
if #compatible_archetypes == 0 then
return EMPTY_QUERY
end
return query :: any
end
local function query_with(query: { compatible_archetypes: { Archetype } }, ...)
local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...)
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local records = archetype.records
local shouldRemove = false
for j = 1, N do
local id = select(j, ...)
if not records[id] then
shouldRemove = true
break
end
end
if shouldRemove then
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end
end
if #compatible_archetypes == 0 then
return EMPTY_QUERY
end
return query :: any
end
-- Meant for directly iterating over archetypes to minimize
-- function call overhead. Should not be used unless iterating over
-- hundreds of thousands of entities in bulk.
local function query_archetypes(query)
return query.compatible_archetypes
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 = {}
@ -1490,6 +1858,7 @@ Query.iter = query_iter_init
Query.without = query_without
Query.with = query_with
Query.archetypes = query_archetypes
Query.cached = query_cached
local function world_query(world: World, ...)
local compatible_archetypes = {}
@ -1502,10 +1871,16 @@ local function world_query(world: World, ...)
local idr: IdRecord?
local componentIndex = world.componentIndex
local q = setmetatable({
ids = ids,
compatible_archetypes = compatible_archetypes,
world = world,
}, Query)
for _, id in ids do
local map = componentIndex[id]
if not map then
return EMPTY_QUERY
return q
end
if idr == nil or map.size < idr.size then
@ -1514,7 +1889,7 @@ local function world_query(world: World, ...)
end
if not idr then
return EMPTY_QUERY
return q
end
for archetype_id in idr.cache do
@ -1542,15 +1917,6 @@ local function world_query(world: World, ...)
compatible_archetypes[length] = compatibleArchetype
end
if length == 0 then
return EMPTY_QUERY
end
local q = setmetatable({
compatible_archetypes = compatible_archetypes,
ids = ids,
}, Query) :: any
return q
end
@ -1736,6 +2102,7 @@ function World.new()
nextComponentId = 0 :: number,
nextEntityId = 0 :: number,
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
observerable = {},
}, World) :: any
self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
@ -1781,7 +2148,7 @@ type function ecs_entity_t(entity)
return entity:components()[2]:readproperty(types.singleton("__T"))
end
export type function Pair(first, second)
type function Pair(first, second)
local thing = first:components()[2]
if thing:readproperty(types.singleton("__T")):is("nil") then
@ -1797,15 +2164,38 @@ export type Entity<T = unknown> = number & { __T: 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: Iter<T...>,
with: (self: Query<T...>, ...i53) -> Query<T...>,
without: (self: Query<T...>, ...i53) -> Query<T...>,
with: (self: Query<T...>, ...Id) -> Query<T...>,
without: (self: Query<T...>, ...Id) -> Query<T...>,
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,
}
type function ecs_partial_t(ty)
local output = types.newtable()
for k, v in ty:properties() do
output:setproperty(k, types.unionof(v.write, types.singleton(nil)))
end
return output
end
export type World = {
archetypeIndex: { [string]: Archetype },
archetypes: Archetypes,
@ -1816,6 +2206,8 @@ export type World = {
nextComponentId: number,
nextEntityId: number,
nextArchetypeId: number,
observerable: { [i53]: { [i53]: { { query: Query<i53> } } } },
} & {
--- Creates a new entity
entity: (self: World) -> Entity,

8004
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -59,9 +59,14 @@ local function debug_world_inspect(world: World)
records = records,
row = row,
tuple = tuple,
columns = columns
}
end
local function name(world, e)
return world:get(e, jecs.Name)
end
TEST("archetype", function()
local archetype_append_to_records = jecs.archetype_append_to_records
local id_record_ensure = jecs.id_record_ensure
@ -358,6 +363,39 @@ TEST("world:add()", function()
end)
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")
local world = jecs.World.new()
local A = world:component()
@ -813,40 +851,6 @@ TEST("world:query()", function()
CHECK(withoutCount == 0)
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
CASE("without")
do
@ -1205,25 +1209,38 @@ TEST("world:delete", function()
end)
TEST("world:target", function()
do
CASE("nth index")
do CASE("nth index")
local world = world_new()
local A = world:component()
world:set(A, jecs.Name, "A")
local B = world:component()
world:set(B, jecs.Name, "B")
local C = world:component()
world:set(C, jecs.Name, "C")
local D = world:component()
world:set(D, jecs.Name, "D")
local E = world:component()
world:set(E, jecs.Name, "E")
local e = world:entity()
world:add(e, pair(A, B))
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, D))
world:add(e, pair(C, D))
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, 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, 1) == D)
CHECK(world:target(e, C, 0) == D)