Compare commits

..

1 commit

Author SHA1 Message Date
Ajay
3f6c252253
Merge 918231a1ad into 8822be58a9 2025-04-23 21:09:33 -04:00
43 changed files with 1014 additions and 1119 deletions

View file

@ -11,8 +11,6 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
## [Unreleased]
- `[world]`:
- Added `world:range` to restrict entity range
- Changed `world:entity` to accept the overload to create an entity at the desired index
- Changed `world:clear` to also look through the component record for the cleared `ID`
- Removes the cleared ID from every entity that has it
- Changed entity ID layouts by putting the index in the lower bits, which should make every world function 1-5 nanoseconds faster

View file

@ -24,7 +24,7 @@ local function observers_new(world, description)
end
local entity_index = world.entity_index :: any
local function emplaced(entity, id, value)
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
@ -45,106 +45,6 @@ local function observers_new(world, description)
end
end
local function join(world, component)
local sparse_array = {}
local dense_array = {}
local values = {}
local max_id = 0
world:added(component, function(entity, id, value)
max_id += 1
sparse_array[entity] = max_id
dense_array[max_id] = entity
values[max_id] = value
end)
world:removed(component, function(entity, id)
local e_swap = dense_array[max_id]
local v_swap = values[max_id]
local dense = sparse_array[entity]
dense_array[dense] = e_swap
values[dense] = v_swap
sparse_array[entity] = nil
dense_array[max_id] = nil
values[max_id] = nil
end)
world:changed(component, function(entity, id, value)
values[sparse_array[entity]] = value
end)
return function()
local i = max_id
return function(): ...any
i -= 1
if i == 0 then
return nil
end
local e = dense_array[i]
return e, values[i]
end
end
end
local function query_changed(world, component)
assert(jecs.IS_PAIR(component) == false)
local callerid = debug.info(2, "sl")
local tracker = world.trackers[callerid]
if not tracker then
local records = {}
local connections = {}
tracker = {
records = records,
connections = connections
}
world.trackers[callerid] = tracker
table.insert(connections, world:added(component, function(entity, id, v)
tracker[entity] = {
new = v
}
end))
table.insert(connections, world:changed(component, function(entity, id, v)
local record = tracker[entity]
record.old = record.new
record.new = v
end))
table.insert(connections, world:removed(component, function(entity, id)
local record = tracker[entity]
record.old = record.new
record.new = nil
end))
end
local entity = nil
local record = nil
return function()
entity, record = next(tracker, entity)
if entity == nil then
return
end
return entity, record
end
end
local function spy_on_world_delete(world)
local world_delete = world.delete
world.delete = function(world, entity)
world_delete(world, entity)
for _, tracker in world.trackers do
tracker.records[entity] = nil
for _, connection in tracker.connections do
connection()
end
end
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
@ -193,11 +93,10 @@ local function monitors_new(world, description)
end
local function observers_add(world: jecs.World & { [string]: any }): PatchedWorld
type Signal = { [jecs.Entity]: { (...any) -> () } }
local signals = {
added = {} :: Signal,
emplaced = {} :: Signal,
removed = {} :: Signal
added = {},
emplaced = {},
removed = {}
}
world.added = function(_, component, fn)
@ -207,21 +106,13 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
if not listeners then
listeners = {}
signals.added[component] = listeners
local function on_add(entity: number, id: number, value: any)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
world:set(component, jecs.OnAdd, on_add)
end
world:set(component, jecs.OnAdd, on_add) end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world.changed = function(_, component, fn)
@ -239,12 +130,6 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
world:set(component, jecs.OnChange, on_change)
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world.removed = function(_, component, fn)
@ -262,12 +147,6 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
world:set(component, jecs.OnRemove, on_remove)
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)
listeners[i] = listeners[n]
listeners[n] = nil
end
end
world.signals = signals
@ -276,8 +155,6 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
world.monitor = monitors_new
world.trackers = {}
return world :: PatchedWorld
end

View file

@ -4,9 +4,10 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib:Clone())
local jecs = require(ReplicatedStorage.Lib)
local newWorld = Matter.World.new()
local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror:Clone())
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local A1 = Matter.component()

View file

@ -2,32 +2,36 @@
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.Lib:Clone())
local mirror = require(ReplicatedStorage.mirror:Clone())
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local newWorld = Matter.World.new()
local ecs = jecs.World.new()
return {
ParameterGenerator = function()
local ecs = jecs.world()
ecs:range(1000, 20000)
local mcs = mirror.World.new()
return ecs, mcs
local registry2 = ecr.registry()
return registry2
end,
Functions = {
Mirror = function(_, ecs, mcs)
for i = 1, 100 do
mcs:entity()
Matter = function()
for i = 1, 1000 do
newWorld:spawn()
end
end,
Jecs = function(_, ecs, mcs)
for i = 1, 100 do
ECR = function(_, registry2)
for i = 1, 1000 do
registry2.create()
end
end,
Jecs = function()
for i = 1, 1000 do
ecs:entity()
end
end,
},
},
}

View file

@ -1,55 +1,76 @@
{
"name": "demo",
"emitLegacyScripts": false,
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage",
"ecs": {
"$path": "../jecs.luau"
},
"Packages": {
"$path": "Packages"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$path": "src/ServerScriptService"
},
"Workspace": {
"$properties": {
"FilteringEnabled": true
},
"Baseplate": {
"$className": "Part",
"$properties": {
"Anchored": true,
"Color": [0.38823, 0.37254, 0.38823],
"Locked": true,
"Position": [0, -10, 0],
"Size": [512, 20, 512]
}
}
},
"Lighting": {
"$properties": {
"Ambient": [0, 0, 0],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"SoundService": {
"$properties": {
"RespectFilteringEnabled": true
}
},
"StarterPlayer": {
"StarterPlayerScripts": {
"$path": "src/StarterPlayer/StarterPlayerScripts"
}
}
}
"name": "demo",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage",
"ecs": {
"$path": "../jecs.luau"
},
"net": {
"$path": "net/client.luau"
},
"Packages": {
"$path": "Packages"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$path": "src/ServerScriptService",
"net": {
"$path": "net/server.luau"
}
},
"Workspace": {
"$properties": {
"FilteringEnabled": true
},
"Baseplate": {
"$className": "Part",
"$properties": {
"Anchored": true,
"Color": [
0.38823,
0.37254,
0.38823
],
"Locked": true,
"Position": [
0,
-10,
0
],
"Size": [
512,
20,
512
]
}
}
},
"Lighting": {
"$properties": {
"Ambient": [
0,
0,
0
],
"Brightness": 2,
"GlobalShadows": true,
"Outlines": false,
"Technology": "Voxel"
}
},
"SoundService": {
"$properties": {
"RespectFilteringEnabled": true
}
},
"StarterPlayer": {
"StarterPlayerScripts": {
"$path": "src/StarterPlayer/StarterPlayerScripts"
}
}
}
}

View file

@ -1,28 +0,0 @@
local function collect<T...>(
signal: {
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
): () -> (T...)
local enqueued = {}
local i = 0
local connection = (signal :: any):Connect(function(...)
table.insert(enqueued, { ... })
i += 1
end)
return function(): any
if i == 0 then
return
end
i -= 1
local args: any = table.remove(enqueued, 1)
return unpack(args)
end, connection
end
return collect

View file

@ -1,36 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local types = require("./types")
local Networked = jecs.tag()
local NetworkedPair = jecs.tag()
local Renderable = jecs.component() :: jecs.Id<Instance>
jecs.meta(Renderable, Networked)
local Poison = jecs.component() :: jecs.Id<number>
jecs.meta(Poison, Networked)
local Health = jecs.component() :: jecs.Id<number>
jecs.meta(Health, Networked)
local Player = jecs.component() :: jecs.Id<Player>
jecs.meta(Player, Networked)
local components = {
Renderable = Renderable,
Player = Player,
Poison = Poison,
Health = Health,
Networked = Networked,
NetworkedPair = NetworkedPair,
}
for name, component in components do
jecs.meta(component, jecs.Name, name)
end
return components

View file

@ -0,0 +1,4 @@
_G.JECS_DEBUG = true
_G.JECS_HI_COMPONENT_ID = 32
require(game:GetService("ReplicatedStorage").ecs)
return

View file

@ -1,13 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = observers_add(jecs.world())
local systems = ReplicatedStorage.systems
SYSTEM(world, systems.receive_replication)
RUN(world)

View file

@ -1,190 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
type Observer<T...> = {
callback: (jecs.Entity) -> (),
query: jecs.Query<T...>,
}
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
observer: (PatchedWorld, Observer<any>) -> (),
monitor: (PatchedWorld, Observer<any>) -> (),
}
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity)
end
end
for _, term in terms do
world:added(term, emplaced)
world:changed(term, emplaced)
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
query.filter_with = ids
terms = ids
end
local entity_index = world.entity_index :: any
local function emplaced(entity: jecs.Entity)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnAdd)
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any)
if not r then
return
end
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnRemove)
end
end
for _, term in terms do
world:added(term, emplaced)
world:removed(term, removed)
end
end
local function observers_add(world: jecs.World): PatchedWorld
local signals = {
added = {},
emplaced = {},
removed = {},
deleted = {}
}
world = world :: jecs.World & {[string]: any}
world.added = function(_, component, fn)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_add(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnAdd, on_add)
idr.hooks.on_add = on_add :: any
idr_r.hooks.on_add = on_add :: any
end
table.insert(listeners, fn)
end
world.changed = function(_, component, fn)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_change(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnChange, on_change)
idr.hooks.on_change = on_change :: any
idr_r.hooks.on_change = on_change :: any
end
table.insert(listeners, fn)
end
world.removed = function(_, component, fn)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local idr = jecs.id_record_ensure(world :: any, component :: any)
local rw = jecs.pair(component, jecs.Wildcard)
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
local function on_remove(entity: number, id: number, value: any)
for _, listener in listeners do
listener(entity, id, value)
end
end
world:set(component, jecs.OnRemove, on_remove)
idr.hooks.on_remove = on_remove :: any
idr_r.hooks.on_remove = on_remove :: any
end
table.insert(listeners, fn)
end
world.signals = signals
world.observer = observers_new
world.monitor = monitors_new
-- local world_delete = world.delete
-- world.deleted = function(_, fn)
-- local listeners = signals.deleted
-- table.insert(listeners, fn)
-- end
-- world.delete = function(world, entity)
-- world_delete(world, entity)
-- for _, fn in signals.deleted do
-- fn(entity)
-- end
-- end
return world :: PatchedWorld
end
return observers_add

View file

@ -1,50 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types")
type Signal<T...> = {
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
type Remote<T...> = {
FireClient: (Remote<T...>, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>) -> (),
OnServerEvent: {
Connect: (any, fn: (Player, T...) -> () ) -> ()
},
OnClientEvent: {
Connect: (any, fn: (T...) -> () ) -> ()
}
}
local function stream_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("RemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
local function datagram_ensure(name): Remote<any>
local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then
remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name
remote.Parent = ReplicatedStorage
end
return remote :: any
end
return {
input = datagram_ensure("input") :: Remote<string>,
replication = stream_ensure("replication") :: Remote<{
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}>,
}

View file

@ -1,136 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jabby = require(ReplicatedStorage.Packages.jabby)
local jecs = require(ReplicatedStorage.ecs)
jabby.set_check_function(function() return true end)
local scheduler = jabby.scheduler.create("jabby scheduler")
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = scheduler,
},
})
local ContextActionService = game:GetService("ContextActionService")
local function create_widget(_, state: Enum.UserInputState)
local client = jabby.obtain_client()
if state ~= Enum.UserInputState.Begin then return end
client.spawn_app(client.apps.home, nil)
end
local RunService = game:GetService("RunService")
local System = jecs.component() :: jecs.Id<{
fn: () -> (),
name: string,
}>
local DependsOn = jecs.component()
local Phase = jecs.tag()
local Event = jecs.component() :: jecs.Id<RBXScriptSignal>
local pair = jecs.pair
local types = require(ReplicatedStorage.types)
local function ECS_PHASE(world, after: types.Entity)
local phase = world:entity()
world:add(phase, Phase)
if after then
local dependency = pair(DependsOn, after)
world:add(phase, dependency)
end
return phase
end
local Heartbeat = jecs.tag()
jecs.meta(Heartbeat, Phase)
jecs.meta(Heartbeat, Event, RunService.Heartbeat)
local PreSimulation = jecs.tag()
jecs.meta(PreSimulation, Phase)
jecs.meta(PreSimulation, Event, RunService.PreSimulation)
local PreAnimation = jecs.tag()
jecs.meta(PreAnimation, Phase)
jecs.meta(PreAnimation, Event, RunService.PreAnimation)
local PreRender = jecs.tag()
jecs.meta(PreRender, Phase)
jecs.meta(PreRender, Event, RunService.PreRender)
local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?)
local system = world:entity()
local p = phase or Heartbeat
local fn = require(mod) :: (...any) -> ()
world:set(system, System, {
fn = fn(world, 0) or fn,
name = mod.Name,
})
local depends_on = DependsOn :: jecs.Entity
world:add(system, pair(depends_on, p))
end
local function find_systems_w_phase(world: types.World, systems, phase: types.Entity)
local phase_name = world:get(phase, jecs.Name) :: string
for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, {
id = scheduler:register_system({
phase = phase_name,
name = s.name,
}),
fn = s.fn
})
end
for after in world:query(Phase, pair(DependsOn, phase)) do
find_systems_w_phase(world, systems, after)
end
return systems
end
local function ECS_RUN(world: types.World)
jabby.register({
applet = jabby.applets.world,
name = "MyWorld",
configuration = {
world = world,
},
})
if RunService:IsClient() then
ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4)
end
for phase, event in world:query(Event, Phase) do
local systems = find_systems_w_phase(world, {}, phase)
event:Connect(function(...)
for _, system in systems do
scheduler:run(system.id, system.fn, world, ...)
end
end)
end
end
return {
PHASE = ECS_PHASE,
SYSTEM = ECS_SYSTEM,
RUN = ECS_RUN,
phases = {
Heartbeat = Heartbeat,
PreSimulation = PreSimulation,
PreAnimation = PreAnimation,
PreRender = PreRender
},
components = {
System = System,
DependsOn = DependsOn,
Phase = Phase,
Event = Event,
}
}

View file

@ -0,0 +1,34 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local jabby = require(ReplicatedStorage.Packages.jabby)
local std = ReplicatedStorage.std
local scheduler = require(std.scheduler)
local world = require(std.world)
local function start(modules)
for _, module in modules do
require(module)
end
local events = scheduler.COLLECT()
scheduler.BEGIN(events)
jabby.set_check_function(function(player)
return true
end)
if RunService:IsClient() then
local player = game:GetService("Players").LocalPlayer
local playergui = player:WaitForChild("PlayerGui")
local client = jabby.obtain_client()
UserInputService.InputBegan:Connect(function(input)
if input.KeyCode == Enum.KeyCode.F4 then
local home = playergui:FindFirstChild("Home")
if home then
home:Destroy()
end
client.spawn_app(client.apps.home)
end
end)
end
end
return start

View file

@ -0,0 +1,40 @@
--!optimize 2
--!native
-- original author @centau
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 <= RUNNING then
return status
end
end
return SUCCESS
end
end
local function FALLBACK(nodes)
return function(...)
for _, node in nodes do
local status = node(...)
if status > FAILURE then
return status
end
end
return FAILURE
end
end
local bt = {
SEQUENCE = SEQUENCE,
FALLBACK = FALLBACK,
RUNNING = RUNNING,
}
return bt

View file

@ -0,0 +1,67 @@
--!nonstrict
--[[
local signal = Signal.new() :: Signal.Signal<string, string>
local events = collect(signal)
local function system(world)
for id, str1, str2 in events do
--
end
end
]]
--[[
original author by @memorycode
MIT License
Copyright (c) 2024 Michael
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]
type Signal<T...> = { [any]: any }
local function collect<T...>(event: Signal<T...>)
local storage = {}
local mt = {}
local iter = function()
local n = #storage
return function()
if n <= 0 then
mt.__iter = nil
return nil
end
n -= 1
return n + 1, unpack(table.remove(storage, 1) :: any)
end
end
local disconnect = event:Connect(function(...)
table.insert(storage, { ... })
mt.__iter = iter
end)
setmetatable(storage, mt)
return (storage :: any) :: () -> (number, T...), function()
disconnect()
end
end
return collect

View file

@ -0,0 +1,30 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local world = require(script.Parent.world)
type Entity<T = nil> = jecs.Entity<T>
local components: {
Character: Entity<Model>,
Mob: Entity,
Model: Entity<Model>,
Player: Entity,
Target: Entity,
Transform: Entity<{ new: CFrame, old: CFrame }>,
Velocity: Entity<number>,
Previous: Entity,
} =
{
Character = world:component(),
Mob = world:component(),
Model = world:component(),
Player = world:component(),
Target = world:component(),
Transform = world:component(),
Velocity = 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)

View file

@ -0,0 +1,19 @@
local function interval(s)
local pin
local function throttle()
if not pin then
pin = os.clock()
end
local elapsed = os.clock() - pin > s
if elapsed then
pin = os.clock()
end
return elapsed
end
return throttle
end
return 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

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

View file

@ -0,0 +1,171 @@
--!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
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) -> (),
id: number,
}
type Systems = { System }
type Events = {
RenderStepped: Systems,
Heartbeat: Systems,
}
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()
local PreRender = world:entity()
local Heartbeat = world:entity()
local PreAnimation = world:entity()
local PreSimulation = world:entity()
local sys: System
local dt: number
local jabby_scheduler = jabby.scheduler.create("Scheduler")
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
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)
jabby.register({
applet = jabby.applets.world,
name = "MyWorld",
configuration = {
world = world,
},
})
jabby.register({
applet = jabby.applets.scheduler,
name = "Scheduler",
configuration = {
scheduler = jabby_scheduler,
},
})
if RunService:IsClient() then
world:add(PreRender, Phase)
world:set(PreRender, Event, (RunService :: RunService).PreRender)
end
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 = ...
for _ in run do
break
end
end
debug.profileend()
end)
end
return connections
end
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,
})
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 {
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

@ -0,0 +1,4 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
-- I like the idea of only having the world be a singleton.
return jecs.World.new() :: jecs.World

View file

@ -1,67 +0,0 @@
local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes")
local collect = require("../collect")
local client_ids = {}
local function ecs_map_get(world: types.World, id: types.Entity)
local deserialised_id = 0
if not world:exists(id) or not world:contains(id) then
deserialised_id = world:entity(id)
client_ids[id] = deserialised_id
else
deserialised_id = client_ids[id]
end
return deserialised_id
end
local function ecs_make_alive_id(world: types.World, id: jecs.Id)
local rel = jecs.ECS_PAIR_FIRST(id)
local tgt = jecs.ECS_PAIR_SECOND(id)
ecs_map_get(world, rel)
ecs_map_get(world, tgt)
end
local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: types.World)
return function()
for snapshot in snapshots do
for key, map in snapshot do
local id = (tonumber(key) :: any) :: jecs.Id
if jecs.IS_PAIR(id) then
ecs_make_alive_id(world, id)
end
local set = map.set
if set then
if jecs.is_tag(world, id) then
for _, entity in set do
entity = ecs_map_get(world, entity)
world:add(entity, id)
end
else
local values = map.values :: { any }
for i, entity in set do
entity = ecs_map_get(world, entity)
world:set(entity, id, values[i])
end
end
end
local removed = map.removed
if removed then
for i, e in removed do
if not world:contains(e) then
continue
end
world:remove(e, id)
end
end
end
end
end
end

View file

@ -0,0 +1,48 @@
local events = {}
local function trackers_invoke(event, component, entity, ...)
local trackers = events[event][component]
if not trackers then
return
end
for _, tracker in trackers do
tracker(entity, data)
end
end
local function trackers_init(event, component, fn)
local ob = events[event]
return {
connect = function(component, fn)
local trackers = ob[component]
if not trackers then
trackers = {}
ob[component] = trackers
end
table.insert(trackers, fn)
end,
invoke = function(component, ...)
trackers_invoke(event, component, ...)
end
}
return function(component, fn)
local trackers = ob[component]
if not trackers then
trackers = {}
ob[component] = trackers
end
table.insert(trackers, fn)
end
end
local trackers = {
emplace = trackers_init("emplace"),
add = trackers_init("added"),
remove = trackers_init("removed")
}
return trackers

View file

@ -1,8 +0,0 @@
local jecs = require(game:GetService("ReplicatedStorage").ecs)
local observers_add = require("../ReplicatedStorage/observers_add")
export type World = typeof(observers_add(jecs.world()))
export type Entity = jecs.Entity
export type Id<T> = jecs.Id<T>
return {}

View file

@ -1,19 +1,4 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local start = require(ReplicatedStorage.start)
local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN
require(ReplicatedStorage.components)
local world = observers_add(jecs.world())
local systems = ServerScriptService.systems
SYSTEM(world, systems.replication)
SYSTEM(world, systems.players_added)
SYSTEM(world, systems.poison_hurts)
SYSTEM(world, systems.life_is_painful)
RUN(world, 0)
start(script.Parent:WaitForChild("systems"):GetChildren())

View file

@ -1,12 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types)
return function(world: types.World, dt: number)
for e in world:query(ct.Player):without(ct.Health) do
world:set(e, ct.Health, 100)
end
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
world:set(e, ct.Poison, 10)
end
end

View file

@ -0,0 +1,88 @@
--!optimize 2
--!native
--!strict
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 world = require(std.world)
local cts = require(std.components)
local Mob = cts.Mob
local Transform = cts.Transform
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 characters do
table.insert(targets, (character.PrimaryPart :: Part).Position)
end
for mob, transform, v in moving_mobs do
local cf = transform.new
local p = cf.Position
local target
local closest
for _, pos in targets do
local distance = (p - pos).Magnitude
if not target or distance < closest then
target = pos
closest = distance
end
end
if not target then
continue
end
local moving = CFrame.new(p + (target - p).Unit * dt * v)
transform.new = moving
blink.UpdateTransform.FireAll(mob, moving)
end
end
local throttle = interval(5)
local function spawnMobs()
if throttle() then
local p = Vector3.new(0, 5, 0)
local cf = CFrame.new(p)
local v = 5
local e = world:entity()
world:set(e, Velocity, v)
world:set(e, Transform, { new = cf })
world:add(e, Mob)
blink.SpawnMob.FireAll(e, cf, v)
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(spawnMobs)
scheduler.SYSTEM(mobsMove)
return 0

View file

@ -0,0 +1,40 @@
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local std = ReplicatedStorage.std
local ref = require(std.ref)
local collect = require(std.collect)
local cts = require(std.components)
local world = require(std.world)
local Player = cts.Player
local Character = cts.Character
local conn = {}
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
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 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local collect = require(ReplicatedStorage.collect)
local types = require(ReplicatedStorage.types)
local ct = require(ReplicatedStorage.components)
local Players = game:GetService("Players")
local player_added = collect(Players.PlayerAdded)
return function(world: types.World, dt: number)
for player in player_added do
local entity = world:entity()
world:set(entity, ct.Player, player)
end
for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character
if character and character.Parent ~= nil then
world:set(entity, ct.Renderable, character)
end
end
end

View file

@ -1,12 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
return function(world, dt)
for e, poison, health in world:query(ct.Poison, ct.Health) do
local health_after_tick = health - poison * dt * 0.05
if health_after_tick < 0 then
world:remove(e, ct.Health)
continue
end
world:set(e, ct.Health, health_after_tick)
end
end

View file

@ -1,122 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../../ReplicatedStorage/types")
local ct = require("../../ReplicatedStorage/components")
local jecs = require(ReplicatedStorage.ecs)
local remotes = require("../../ReplicatedStorage/remotes")
return function(world: types.World)
local storages = {}
for component in world:query(ct.Networked) do
local is_tag = jecs.is_tag(world, component)
local storage = {} :: { [types.Entity]: any }
storages[component] = storage
if is_tag then
world:added(component, function(entity)
storage[entity] = true
end)
else
world:added(component, function(entity, _, value)
storage[entity] = value
end)
world:changed(component, function(entity, _, value)
storage[entity] = value
end)
end
world:removed(component, function(entity)
storage[entity] = "jecs.Remove"
end)
end
for relation in world:query(ct.NetworkedPair) do
world:added(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
if is_tag then
storage[entity] = true
else
storage[entity] = value
end
end)
world:changed(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id)
if is_tag then
return
end
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = value
end :: <T>(types.Entity, types.Id<T>, T) -> ())
world:removed(relation, function(entity, id)
local storage = storages[id]
if not storage then
storage = {}
storages[id] = storage
end
storage[entity] = "jecs.Remove"
end)
end
return function()
local snapshot = {} :: {
[string]: {
set: { types.Entity }?,
values: { any }?,
removed: { types.Entity }?
}
}
local set_ids = {} :: { types.Entity }
local removed_ids = {} :: { types.Entity }
for component, storage in storages do
local set_values = {}
local set_n = 0
local removed_n = 0
for e, v in storage do
if v ~= "jecs.Remove" then
set_n += 1
set_ids[set_n] = e
set_values[set_n] = v or true
elseif world:contains(e) then
removed_n += 1
removed_ids[removed_n] = e
end
end
table.clear(storage)
local dirty = false
if set_n > 0 or removed_n > 0 then
dirty = true
end
if dirty then
snapshot[tostring(component)] = {
set = if set_n > 0 then table.move(set_ids, 1, set_n, 1, {}) else nil,
values = if set_n > 0 then set_values else nil,
removed = if removed_n > 0 then table.move(removed_ids, 1, removed_n, 1, {} :: { types.Entity }) else nil
} :: any
end
end
if next(snapshot) ~= nil then
remotes.replication:FireAllClients(snapshot)
end
end
end

View file

@ -0,0 +1,4 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local start = require(ReplicatedStorage.start)
start(script.Parent:WaitForChild("systems"):GetChildren())

View file

@ -0,0 +1,67 @@
--!optimize 2
--!native
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local world = require(std.world)
local Position = world:component() :: jecs.Entity<vector>
local Previous = jecs.Rest
local pre = jecs.pair(Position, Previous)
local added = world
:query(Position)
:without(pre)
:cached()
local changed = world
:query(Position, pre)
:cached()
local removed = world
:query(pre)
:without(Position)
:cached()
local children = {}
for i = 1, 10 do
local e = world:entity()
world:set(e, Position, vector.create(i, i, i))
table.insert(children, e)
end
local function flip()
return math.random() > 0.5
end
local function system()
for i, child in children do
world:set(child, Position, vector.create(i,i,i))
end
for e, p in added:iter() do
world:set(e, pre, p)
end
for i, child in children do
if flip() then
world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
end
end
for e, new, old in changed:iter() do
if new ~= old then
world:set(e, pre, new)
end
end
for i, child in children do
world:remove(child, Position)
end
for e in removed:iter() do
world:remove(e, pre)
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(system)
return 0

View file

@ -0,0 +1,90 @@
--!optimize 2
--!native
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs)
local __ = jecs.Wildcard
local std = ReplicatedStorage.std
local world = require(std.world)
local Position = world:component() :: jecs.Entity<vector>
local Previous = jecs.Rest
local pre = jecs.pair(Position, Previous)
local added = world
:query(Position)
:without(pre)
:cached()
local changed = world
:query(Position, pre)
:cached()
local removed = world
:query(pre)
:without(Position)
:cached()
local children = {}
for i = 1, 10 do
local e = world:entity()
world:set(e, Position, vector.create(i, i, i))
table.insert(children, e)
end
local function flip()
return math.random() > 0.5
end
local entity_index = world.entity_index
local function copy(archetypes, id)
for _, archetype in archetypes do
local to = jecs.archetype_traverse_add(world, pre, archetype)
local columns = to.columns
local records = to.records
local old = columns[records[pre].column]
local new = columns[records[id].column]
if to ~= archetype then
for _, entity in archetype.entities do
local r = jecs.entity_index_try_get_fast(entity_index, entity)
jecs.entity_move(entity_index, entity, r, to)
end
end
table.move(new, 1, #new, 1, old)
end
end
local function system2()
for i, child in children do
world:set(child, Position, vector.create(i,i,i))
end
for e, p in added:iter() do
end
copy(added:archetypes(), Position)
for i, child in children do
if flip() then
world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
end
end
for e, new, old in changed:iter() do
if new ~= old then
end
end
copy(changed:archetypes(), Position)
for i, child in children do
world:remove(child, Position)
end
for e in removed:iter() do
world:remove(e, pre)
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(system2)
return 0

View file

@ -0,0 +1,46 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net)
local std = ReplicatedStorage.std
local world = require(std.world)
local ref = require(std.ref)
local cts = require(std.components)
local Model = cts.Model
local Transform = cts.Transform
local moved_models = world:query(Model, Transform):cached()
local updated_models = {}
local i = 0
local function processed(n)
i += 1
if i > n then
i = 0
return true
end
return false
end
local function move(dt: number)
for entity, model in moved_models do
if updated_models[entity] then
updated_models[entity] = nil
model.PrimaryPart.CFrame = transform
end
end
end
local function syncTransforms()
for _, id, cf in blink.UpdateTransform.Iter() do
local e = ref("server-" .. tostring(id))
world:set(e, Transform, cf)
moved_models[e] = true
end
end
local scheduler = require(std.scheduler)
scheduler.SYSTEM(move)
scheduler.SYSTEM(syncTransforms)
return 0

View file

@ -0,0 +1,31 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local blink = require(ReplicatedStorage.net)
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
local part = Instance.new("Part")
part.Size = Vector3.one * 5
part.BrickColor = BrickColor.Red()
part.Anchored = true
local model = Instance.new("Model")
model.PrimaryPart = part
part.Parent = model
model.Parent = workspace
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
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

182
jecs.luau
View file

@ -62,8 +62,6 @@ type ecs_entity_index_t = {
sparse_array: Map<i24, ecs_record_t>,
alive_count: number,
max_id: number,
range_begin: number?,
range_end: number?
}
type ecs_query_data_t = {
@ -127,12 +125,6 @@ local ECS_INTERNAL_ERROR = [[
https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md
]]
local function ecs_assert(condition, msg: string?)
if not condition then
error(msg)
end
end
local ecs_metadata: Map<i53, Map<i53, any>> = {}
local ecs_max_component_id = 0
local ecs_max_tag_id = EcsRest
@ -193,10 +185,6 @@ local function ECS_ENTITY_T_LO(e: i53): i24
return e % ECS_ENTITY_MASK
end
local function ECS_ID(e: i53)
return e % ECS_ENTITY_MASK
end
local function ECS_GENERATION(e: i53)
return e // ECS_ENTITY_MASK
end
@ -261,10 +249,10 @@ local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i
return entity_index_try_get(entity_index, entity) ~= nil
end
local function entity_index_get_alive(entity_index: ecs_entity_index_t, entity: i53): i53?
local r = entity_index_try_get_any(entity_index, entity)
local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53?
local r = entity_index_try_get_any(index, entity)
if r then
return entity_index.dense_array[r.dense]
return index.dense_array[r.dense]
end
return nil
end
@ -292,15 +280,11 @@ local function ecs_get_alive(world, entity)
return current
end
local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range"
local function entity_index_new_id(entity_index: ecs_entity_index_t): i53
local dense_array = entity_index.dense_array
local alive_count = entity_index.alive_count
local sparse_array = entity_index.sparse_array
local max_id = entity_index.max_id
if alive_count < max_id then
if alive_count ~= max_id then
alive_count += 1
entity_index.alive_count = alive_count
local id = dense_array[alive_count]
@ -308,14 +292,11 @@ local function entity_index_new_id(entity_index: ecs_entity_index_t): i53
end
local id = max_id + 1
local range_end = entity_index.range_end
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
entity_index.max_id = id
alive_count += 1
entity_index.alive_count = alive_count
dense_array[alive_count] = id
sparse_array[id] = { dense = alive_count } :: ecs_record_t
entity_index.sparse_array[id] = { dense = alive_count } :: ecs_record_t
return id
end
@ -602,10 +583,10 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
local is_pair = ECS_IS_PAIR(id)
if is_pair then
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53
ecs_assert(relation and entity_index_is_alive(
assert(relation and entity_index_is_alive(
entity_index, relation), ECS_INTERNAL_ERROR)
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53
ecs_assert(target and entity_index_is_alive(
assert(target and entity_index_is_alive(
entity_index, target), ECS_INTERNAL_ERROR)
end
@ -738,89 +719,8 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev:
return archetype
end
local function world_range(world: ecs_world_t, range_begin: number, range_end: number?)
local entity_index = world.entity_index
entity_index.range_begin = range_begin
entity_index.range_end = range_end
local max_id = entity_index.max_id
if range_begin > max_id then
local dense_array = entity_index.dense_array
local sparse_array = entity_index.sparse_array
for i = max_id + 1, range_begin do
dense_array[i] = i
sparse_array[i] = {
dense = 0
} :: ecs_record_t
end
entity_index.max_id = range_begin - 1
entity_index.alive_count = range_begin - 1
end
end
local function world_entity(world: ecs_world_t, entity: i53?): i53
local entity_index = world.entity_index
if entity then
local index = ECS_ID(entity)
local max_id = entity_index.max_id
local sparse_array = entity_index.sparse_array
local dense_array = entity_index.dense_array
local alive_count = entity_index.alive_count
local r = sparse_array[index]
if r then
local dense = r.dense
if not dense or r.dense == 0 then
r.dense = index
dense = index
end
local any = dense_array[dense]
if dense <= alive_count then
return any
end
local e_swap = dense_array[dense]
local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t
alive_count += 1
entity_index.alive_count = alive_count
r_swap.dense = dense
r.dense = alive_count
dense_array[dense] = e_swap
dense_array[alive_count] = any
return any
else
for i = max_id + 1, index do
sparse_array[i] = { dense = i } :: ecs_record_t
dense_array[i] = i
end
entity_index.max_id = index
local e_swap = dense_array[alive_count]
local r_swap = sparse_array[alive_count]
r_swap.dense = index
alive_count += 1
entity_index.alive_count = alive_count
r = sparse_array[index]
r.dense = alive_count
sparse_array[index] = r
dense_array[index] = e_swap
dense_array[alive_count] = entity
return entity
end
end
return entity_index_new_id(entity_index, entity)
local function world_entity(world: ecs_world_t): i53
return entity_index_new_id(world.entity_index)
end
local function world_parent(world: ecs_world_t, entity: i53)
@ -1425,24 +1325,19 @@ local function world_delete(world: ecs_world_t, entity: i53)
end
local dense_array = entity_index.dense_array
local dense = record.dense
local i_swap = entity_index.alive_count
entity_index.alive_count = i_swap - 1
local index_of_deleted_entity = record.dense
local index_of_last_alive_entity = entity_index.alive_count
entity_index.alive_count = index_of_last_alive_entity - 1
local e_swap = dense_array[i_swap]
local r_swap = entity_index_try_get_any(entity_index, e_swap) :: ecs_record_t
r_swap.dense = dense
local last_alive_entity = dense_array[index_of_last_alive_entity]
local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: ecs_record_t
r_swap.dense = index_of_deleted_entity
record.archetype = nil :: any
record.row = nil :: any
record.dense = i_swap
record.dense = index_of_last_alive_entity
dense_array[dense] = e_swap
dense_array[i_swap] = ECS_GENERATION_INC(entity)
end
local function world_exists(world: ecs_world_t, entity): boolean
return entity_index_try_get_any(world.entity_index, entity) ~= nil
dense_array[index_of_deleted_entity] = last_alive_entity
dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity)
end
local function world_contains(world: ecs_world_t, entity): boolean
@ -2416,8 +2311,6 @@ export type EntityIndex = {
sparse_array: Map<i24, Record>,
alive_count: number,
max_id: number,
range_begin: number?,
range_end: number?
}
local World = {}
@ -2436,11 +2329,9 @@ World.has = world_has
World.target = world_target
World.parent = world_parent
World.contains = world_contains
World.exists = world_exists
World.cleanup = world_cleanup
World.each = world_each
World.children = world_children
World.range = world_range
local function world_new()
local entity_index = {
@ -2476,6 +2367,21 @@ local function world_new()
entity_index_new_id(entity_index)
end
for i = EcsRest + 1, ecs_max_tag_id do
-- Initialize built-in components
entity_index_new_id(entity_index)
end
for i, bundle in ecs_metadata do
for ty, value in bundle do
if value == NULL then
world_add(self, i, ty)
else
world_set(self, i, ty, value)
end
end
end
world_add(self, EcsName, EcsComponent)
world_add(self, EcsOnChange, EcsComponent)
world_add(self, EcsOnAdd, EcsComponent)
@ -2498,20 +2404,6 @@ local function world_new()
world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete))
for i = EcsRest + 1, ecs_max_tag_id do
entity_index_new_id(entity_index)
end
for i, bundle in ecs_metadata do
for ty, value in bundle do
if value == NULL then
world_add(self, i, ty)
else
world_set(self, i, ty, value)
end
end
end
return self
end
@ -2562,9 +2454,6 @@ export type World = {
observable: any,
--- Enforce a check on entities to be created within desired range
range: (self: World, range_begin: number, range_end: number?) -> (),
--- Creates a new entity
entity: (self: World, id: Entity?) -> Entity,
--- Creates a new entity located in the first 256 ids.
@ -2604,9 +2493,6 @@ export type World = {
--- Checks if the world contains the given entity
contains:(self: World, entity: Entity) -> boolean,
--- Checks if the entity exists
exists: (self: World, entity: Entity) -> boolean,
each: <T>(self: World, id: Id<T>) -> () -> Entity,
children: <T>(self: World, id: Id<T>) -> () -> Entity,
@ -2672,8 +2558,6 @@ return {
ECS_META_RESET = ECS_META_RESET,
IS_PAIR = (ECS_IS_PAIR :: any) :: <P, O>(pair: Pair<P, O>) -> boolean,
ECS_PAIR_FIRST = ECS_PAIR_FIRST :: <P, O>(pair: Pair<P, O>) -> Id<P>,
ECS_PAIR_SECOND = ECS_PAIR_SECOND :: <P, O>(pair: Pair<P, O>) -> Id<O>,
pair_first = (ecs_pair_first :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>,
pair_second = (ecs_pair_second :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<O>,
entity_index_get_alive = entity_index_get_alive,

View file

@ -3,4 +3,3 @@ wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.4"
stylua = "johnnymorganz/stylua@2.0.1"
Blink = "1Axen/Blink@0.14.1"
wally-package-types = "JohnnyMorganz/wally-package-types@1.4.2"

View file

@ -247,7 +247,6 @@ TEST("world:component()", function()
end)
TEST("world:contains()", function()
local tag = jecs.tag()
local world = jecs.world()
local id = world:entity()
@ -259,7 +258,6 @@ TEST("world:contains()", function()
CHECK(not world:contains(id))
end
CHECK(world:contains(tag))
jecs.ECS_META_RESET()
end)
@ -633,53 +631,7 @@ TEST("world:each()", function()
end
end)
TEST("world:range()", function()
do CASE "under range start"
local world = jecs.world()
world:range(400, 1000)
local id = world:entity()
local e = world:entity(id + 5)
CHECK(e == id + 5)
CHECK(world:contains(e))
local e2 = world:entity(399)
CHECK(world:contains(e2))
world:delete(e2)
CHECK(not world:contains(e2))
local e2v1 = world:entity(399)
CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 399)
CHECK(ECS_GENERATION(e2v1) == 1)
end
do CASE "over range start"
local world = jecs.world()
world:range(400, 1000)
local e2 = world:entity(405)
CHECK(world:contains(e2))
world:delete(e2)
CHECK(not world:contains(e2))
local e2v1 = world:entity(405)
CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 405)
CHECK(ECS_GENERATION(e2v1) == 1)
do
local _e2v1 = world:entity(405)
CHECK(_e2v1 == e2v1)
end
end
end)
TEST("world:entity()", function()
do CASE "desired id"
local world = jecs.world()
local id = world:entity()
local e = world:entity(id + 5)
CHECK(e == id + 5)
CHECK(world:contains(e))
local e2 = world:entity(399)
CHECK(world:contains(e2))
end
local N = 2^8
do CASE "unique IDs"

View file

@ -28,7 +28,7 @@ end
local padding_enabled = false
local function pad()
if padding_enabled then
print("------------------")
print("")
end
end
@ -64,8 +64,7 @@ local function lifetime_tracker_add(world: jecs.World, opt): PatchedWorld
world.print_entity_index = function()
local max_id = entity_index.max_id
local alive_count = entity_index.alive_count
local range_begin = entity_index.range_begin or jecs.Rest + 1
local alive = table.move(dense_array, range_begin :: any, alive_count, 1, {})
local alive = table.move(dense_array, 1 + jecs.Rest :: any, alive_count, 1, {})
local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})
local sep = "|--------|"

View file

@ -229,6 +229,7 @@ end
local test_focused = false
local function TEST(name: string, fn: () -> ())
test = {
name = name,
cases = {},
@ -243,11 +244,12 @@ local function TEST(name: string, fn: () -> ())
test.focus = true
test_focused = true
end
table.insert(tests, t)
end
local function FOCUS()
assert(test, "no active test")
check_for_focused = true
test_focused = false
end