Add a hooks cache

This commit is contained in:
Ukendio 2024-09-09 03:22:20 +02:00
parent e234bd82ee
commit 1503d7e462
5 changed files with 657 additions and 483 deletions

76
demo/default.project.json Normal file
View file

@ -0,0 +1,76 @@
{
"name": "demo",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage",
"ecs": {
"$path": "../src"
},
"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

@ -0,0 +1,32 @@
--!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

@ -19,6 +19,7 @@ local std = {
world = world :: World,
pair = jecs.pair,
__ = jecs.w,
hooks = require(script.hooks)
}
return std

View file

@ -4,6 +4,8 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
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>
@ -43,20 +45,20 @@ export type Scheduler = {
Heartbeat: Entity,
},
phase: (after: Entity) -> Entity
phase: (after: Entity) -> Entity,
debugging: boolean,
}
local scheduler_new: (w: World, components: { [string]: Entity }) -> Scheduler
do
local world: World
local Disabled: Entity
local System: Entity<{}>
local DependsOn
local Phase
local Event
local Name
local System: Entity<System>
local DependsOn: Entity
local Phase: Entity
local Event: Entity<RBXScriptSignal>
local scheduler
@ -65,18 +67,19 @@ do
local PreAnimation
local PreSimulation
local system: System
local sys: System
local dt
local function run()
local id = system.id
local id = sys.id
local system_data = scheduler.system_data[id]
if system_data.paused then return end
scheduler:mark_system_frame_start(id)
system.callback(dt)
sys.callback(dt)
scheduler:mark_system_frame_end(id)
end
local function panic(str)
-- We don't want to interrupt the loop when we error
task.spawn(error, str)
@ -91,10 +94,10 @@ do
dt = event:Wait()
debug.profilebegin(event_name)
for _, sys in systems do
system = sys
for _, s in systems do
sys = s
local didNotYield, why = xpcall(function()
for _ in run do end
for _ in run do break end
end, debug.traceback)
if didNotYield then
@ -105,7 +108,7 @@ do
panic("Not allowed to yield in the systems."
.. "\n"
.. "System: "
.. debug.info(system.callback, "n")
.. debug.info(s.callback, "n")
.. " has been ejected"
)
continue
@ -121,13 +124,13 @@ do
local function scheduler_collect_systems_under_phase_recursive(systems, phase)
local phase_name = world:get(phase, Name)
for _, system in world:query(System):with(pair(DependsOn, phase)) do
for _, s in world:query(System):with(pair(DependsOn, phase)) do
table.insert(systems, {
id = scheduler:register_system({
name = system.name,
name = s.name,
phase = phase_name
}),
callback = system.callback
callback = s.callback
})
end
for after in world:query(Phase):with(pair(DependsOn, phase)) do
@ -172,7 +175,6 @@ do
Phase = world:component()
DependsOn = world:component()
Event = world:component()
Name = world:component()
RenderStepped = world:component()
Heartbeat = world:component()
@ -193,6 +195,7 @@ do
world:add(PreAnimation, Phase)
world:set(PreAnimation, Event, RunService.PreAnimation)
for name, component in components do
world:set(component, Name, name)
end
@ -210,6 +213,7 @@ do
scheduler = jabby.scheduler.create("scheduler")
table.insert(jabby.public, scheduler)
return {
phase = scheduler_phase_new,

View file

@ -57,7 +57,7 @@ type ArchetypeDiff = {
removed: Ty,
}
local HI_COMPONENT_ID = 256
local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
@ -70,7 +70,8 @@ local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10
local EcsTag = HI_COMPONENT_ID + 11
local EcsRest = HI_COMPONENT_ID + 12
local EcsName = HI_COMPONENT_ID + 12
local EcsRest = HI_COMPONENT_ID + 13
local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
@ -597,7 +598,7 @@ local function invoke_hook(world: World, hook_id: number, id: i53, entity: i53,
end
end
local function world_add(world: World, entity: i53, id: i53)
local function world_add(world: World, entity: i53, id: i53): ()
local entityIndex = world.entityIndex
local record = entityIndex.sparse[entity]
local from = record.archetype
@ -622,7 +623,7 @@ local function world_add(world: World, entity: i53, id: i53)
end
-- Symmetric like `World.add` but idempotent
local function world_set(world: World, entity: i53, id: i53, data: unknown)
local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
local entityIndex = world.entityIndex
local record = entityIndex.sparse[entity]
local from = record.archetype
@ -914,48 +915,40 @@ local function world_contains(world: World, entity): boolean
return world.entityIndex.sparse[entity] ~= nil
end
type CompatibleArchetype = { archetype: Archetype, indices: { number } }
local function noop() end
local function NOOP() end
local function Arm(query, ...)
local function ARM(query, ...)
return query
end
local world_query
do
local empty_list = {}
local EmptyQuery = {
local EMPTY_LIST = {}
local EmptyQuery = {
__iter = function()
return noop
return NOOP
end,
iter = function()
return noop
return NOOP
end,
drain = Arm,
next = noop,
replace = noop,
with = Arm,
without = Arm,
drain = ARM,
next = NOOP,
replace = NOOP,
with = ARM,
without = ARM,
archetypes = function()
return empty_list
return EMPTY_LIST
end,
}
}
setmetatable(EmptyQuery, EmptyQuery)
setmetatable(EmptyQuery, EmptyQuery)
local function world_query_replace_values(row, columns, ...)
local function columns_replace_values(row, columns, ...)
for i, column in columns do
column[row] = select(i, ...)
end
end
function world_query(world: World, ...)
-- breaking
if (...) == nil then
error("Missing components")
end
end
local function world_query(world: World, ...)
local compatible_archetypes = {}
local length = 0
@ -1339,7 +1332,7 @@ do
local tr = records[id]
queryOutput[j] = columns[tr.column][row]
end
world_query_replace_values(row, columns, fn(unpack(queryOutput)))
columns_replace_values(row, columns, fn(unpack(queryOutput)))
end
end
end
@ -1416,7 +1409,6 @@ do
setmetatable(it, it)
return it
end
end
local World = {}
@ -1436,6 +1428,74 @@ World.target = world_target
World.parent = world_parent
World.contains = world_contains
if _G.__JECS_DEBUG == true then
-- taken from https://github.com/centau/ecr/blob/main/src/ecr.luau
-- error but stack trace always starts at first callsite outside of this file
local function throw(msg: string)
local s = 1
repeat s += 1 until debug.info(s, "s") ~= debug.info(1, "s")
error(msg, s)
end
local function ASSERT<T>(v: T, msg: string)
if v then
return
end
throw(msg)
end
World.query = function(world: World, ...)
ASSERT((...), "Requires at least a single component")
return world_query(world, ...)
end
World.set = function(world: World, entity: i53,
id: i53, value: any): ()
local idr = world.componentIndex[id]
local flags = idr.flags
local id_is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
if id_is_tag then
local name = world_get_one_inline(world, id, EcsName) or `${id}`
throw(`({name}) is a tag. Did you mean to use "world:add(entity, {name})"`)
elseif value == nil then
local name = world_get_one_inline(world, id, EcsName) or `${id}`
throw(`cannot set component ({name}) value to nil. If this was intentional, use "world:add(entity, {name})"`)
end
world_set(world, entity, id, value)
end
World.add = function(world: World, entity: i53, id: i53, value: nil)
if value ~= nil then
local name = world_get_one_inline(world, id, EcsName) or `${id}`
throw(`You provided a value when none was expected. Did you mean to use "world:add(entity, {name})"`)
end
world_add(world, entity, id)
end
World.get = function(world: World, entity: i53, id: i53, ...: i53)
local length = select("#", ...)
ASSERT(length > 4, "world:get does not support more than 4 components")
for i = 1, length do
local id = select(i, ...)
local idr = world.componentIndex[id]
local flags = idr.flags
local id_is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
if id_is_tag then
throw(`cannot get component ({name}) value because it is a tag. If this was intentional, use "world:has(entity, {name})"`)
end
end
if value ~= nil then
local name = world_get_one_inline(world, id, EcsName) or `${id}`
throw(`You provided a value when none was expected. Did you mean to use "world:add(entity, {name})"`)
end
return world_get(world, entity, id)
end
end
function World.new()
local self = setmetatable({
archetypeIndex = {} :: { [string]: Archetype },
@ -1592,6 +1652,7 @@ return {
Delete = EcsDelete :: Entity,
Remove = EcsRemove :: Entity,
Tag = EcsTag :: Entity,
Name = EcsName :: Entity<string>,
Rest = EcsRest :: Entity,
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,