mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 01:20:04 +00:00
Add a hooks cache
This commit is contained in:
parent
e234bd82ee
commit
1503d7e462
5 changed files with 657 additions and 483 deletions
76
demo/default.project.json
Normal file
76
demo/default.project.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
demo/src/ReplicatedStorage/std/hooks.luau
Normal file
32
demo/src/ReplicatedStorage/std/hooks.luau
Normal 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
|
|
@ -19,6 +19,7 @@ local std = {
|
||||||
world = world :: World,
|
world = world :: World,
|
||||||
pair = jecs.pair,
|
pair = jecs.pair,
|
||||||
__ = jecs.w,
|
__ = jecs.w,
|
||||||
|
hooks = require(script.hooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
return std
|
return std
|
||||||
|
|
|
@ -4,6 +4,8 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
local jabby = require(ReplicatedStorage.Packages.jabby)
|
local jabby = require(ReplicatedStorage.Packages.jabby)
|
||||||
local jecs = require(ReplicatedStorage.ecs)
|
local jecs = require(ReplicatedStorage.ecs)
|
||||||
local pair = jecs.pair
|
local pair = jecs.pair
|
||||||
|
local Name = jecs.Name
|
||||||
|
|
||||||
type World = jecs.World
|
type World = jecs.World
|
||||||
type Entity<T=nil> = jecs.Entity<T>
|
type Entity<T=nil> = jecs.Entity<T>
|
||||||
|
|
||||||
|
@ -43,20 +45,20 @@ export type Scheduler = {
|
||||||
Heartbeat: Entity,
|
Heartbeat: Entity,
|
||||||
},
|
},
|
||||||
|
|
||||||
phase: (after: Entity) -> Entity
|
phase: (after: Entity) -> Entity,
|
||||||
|
|
||||||
|
debugging: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
local scheduler_new: (w: World, components: { [string]: Entity }) -> Scheduler
|
local scheduler_new: (w: World, components: { [string]: Entity }) -> Scheduler
|
||||||
|
|
||||||
|
|
||||||
do
|
do
|
||||||
local world: World
|
local world: World
|
||||||
local Disabled: Entity
|
local Disabled: Entity
|
||||||
local System: Entity<{}>
|
local System: Entity<System>
|
||||||
local DependsOn
|
local DependsOn: Entity
|
||||||
local Phase
|
local Phase: Entity
|
||||||
local Event
|
local Event: Entity<RBXScriptSignal>
|
||||||
local Name
|
|
||||||
|
|
||||||
local scheduler
|
local scheduler
|
||||||
|
|
||||||
|
@ -65,18 +67,19 @@ do
|
||||||
local PreAnimation
|
local PreAnimation
|
||||||
local PreSimulation
|
local PreSimulation
|
||||||
|
|
||||||
local system: System
|
local sys: System
|
||||||
local dt
|
local dt
|
||||||
|
|
||||||
local function run()
|
local function run()
|
||||||
local id = system.id
|
local id = sys.id
|
||||||
local system_data = scheduler.system_data[id]
|
local system_data = scheduler.system_data[id]
|
||||||
if system_data.paused then return end
|
if system_data.paused then return end
|
||||||
|
|
||||||
scheduler:mark_system_frame_start(id)
|
scheduler:mark_system_frame_start(id)
|
||||||
system.callback(dt)
|
sys.callback(dt)
|
||||||
scheduler:mark_system_frame_end(id)
|
scheduler:mark_system_frame_end(id)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function panic(str)
|
local function panic(str)
|
||||||
-- We don't want to interrupt the loop when we error
|
-- We don't want to interrupt the loop when we error
|
||||||
task.spawn(error, str)
|
task.spawn(error, str)
|
||||||
|
@ -91,10 +94,10 @@ do
|
||||||
dt = event:Wait()
|
dt = event:Wait()
|
||||||
|
|
||||||
debug.profilebegin(event_name)
|
debug.profilebegin(event_name)
|
||||||
for _, sys in systems do
|
for _, s in systems do
|
||||||
system = sys
|
sys = s
|
||||||
local didNotYield, why = xpcall(function()
|
local didNotYield, why = xpcall(function()
|
||||||
for _ in run do end
|
for _ in run do break end
|
||||||
end, debug.traceback)
|
end, debug.traceback)
|
||||||
|
|
||||||
if didNotYield then
|
if didNotYield then
|
||||||
|
@ -105,7 +108,7 @@ do
|
||||||
panic("Not allowed to yield in the systems."
|
panic("Not allowed to yield in the systems."
|
||||||
.. "\n"
|
.. "\n"
|
||||||
.. "System: "
|
.. "System: "
|
||||||
.. debug.info(system.callback, "n")
|
.. debug.info(s.callback, "n")
|
||||||
.. " has been ejected"
|
.. " has been ejected"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
@ -121,13 +124,13 @@ do
|
||||||
|
|
||||||
local function scheduler_collect_systems_under_phase_recursive(systems, phase)
|
local function scheduler_collect_systems_under_phase_recursive(systems, phase)
|
||||||
local phase_name = world:get(phase, Name)
|
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, {
|
table.insert(systems, {
|
||||||
id = scheduler:register_system({
|
id = scheduler:register_system({
|
||||||
name = system.name,
|
name = s.name,
|
||||||
phase = phase_name
|
phase = phase_name
|
||||||
}),
|
}),
|
||||||
callback = system.callback
|
callback = s.callback
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
for after in world:query(Phase):with(pair(DependsOn, phase)) do
|
for after in world:query(Phase):with(pair(DependsOn, phase)) do
|
||||||
|
@ -172,7 +175,6 @@ do
|
||||||
Phase = world:component()
|
Phase = world:component()
|
||||||
DependsOn = world:component()
|
DependsOn = world:component()
|
||||||
Event = world:component()
|
Event = world:component()
|
||||||
Name = world:component()
|
|
||||||
|
|
||||||
RenderStepped = world:component()
|
RenderStepped = world:component()
|
||||||
Heartbeat = world:component()
|
Heartbeat = world:component()
|
||||||
|
@ -193,6 +195,7 @@ do
|
||||||
|
|
||||||
world:add(PreAnimation, Phase)
|
world:add(PreAnimation, Phase)
|
||||||
world:set(PreAnimation, Event, RunService.PreAnimation)
|
world:set(PreAnimation, Event, RunService.PreAnimation)
|
||||||
|
|
||||||
for name, component in components do
|
for name, component in components do
|
||||||
world:set(component, Name, name)
|
world:set(component, Name, name)
|
||||||
end
|
end
|
||||||
|
@ -210,6 +213,7 @@ do
|
||||||
scheduler = jabby.scheduler.create("scheduler")
|
scheduler = jabby.scheduler.create("scheduler")
|
||||||
|
|
||||||
table.insert(jabby.public, scheduler)
|
table.insert(jabby.public, scheduler)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
phase = scheduler_phase_new,
|
phase = scheduler_phase_new,
|
||||||
|
|
||||||
|
|
123
src/init.luau
123
src/init.luau
|
@ -57,7 +57,7 @@ type ArchetypeDiff = {
|
||||||
removed: Ty,
|
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 EcsOnAdd = HI_COMPONENT_ID + 1
|
||||||
local EcsOnRemove = HI_COMPONENT_ID + 2
|
local EcsOnRemove = HI_COMPONENT_ID + 2
|
||||||
|
@ -70,7 +70,8 @@ local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
|
||||||
local EcsDelete = HI_COMPONENT_ID + 9
|
local EcsDelete = HI_COMPONENT_ID + 9
|
||||||
local EcsRemove = HI_COMPONENT_ID + 10
|
local EcsRemove = HI_COMPONENT_ID + 10
|
||||||
local EcsTag = HI_COMPONENT_ID + 11
|
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_PAIR_FLAG = 0x8
|
||||||
local ECS_ID_FLAGS_MASK = 0x10
|
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
|
||||||
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 entityIndex = world.entityIndex
|
||||||
local record = entityIndex.sparse[entity]
|
local record = entityIndex.sparse[entity]
|
||||||
local from = record.archetype
|
local from = record.archetype
|
||||||
|
@ -622,7 +623,7 @@ local function world_add(world: World, entity: i53, id: i53)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Symmetric like `World.add` but idempotent
|
-- 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 entityIndex = world.entityIndex
|
||||||
local record = entityIndex.sparse[entity]
|
local record = entityIndex.sparse[entity]
|
||||||
local from = record.archetype
|
local from = record.archetype
|
||||||
|
@ -914,48 +915,40 @@ local function world_contains(world: World, entity): boolean
|
||||||
return world.entityIndex.sparse[entity] ~= nil
|
return world.entityIndex.sparse[entity] ~= nil
|
||||||
end
|
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
|
return query
|
||||||
end
|
end
|
||||||
|
|
||||||
local world_query
|
local EMPTY_LIST = {}
|
||||||
do
|
local EmptyQuery = {
|
||||||
local empty_list = {}
|
|
||||||
local EmptyQuery = {
|
|
||||||
__iter = function()
|
__iter = function()
|
||||||
return noop
|
return NOOP
|
||||||
end,
|
end,
|
||||||
iter = function()
|
iter = function()
|
||||||
return noop
|
return NOOP
|
||||||
end,
|
end,
|
||||||
drain = Arm,
|
drain = ARM,
|
||||||
next = noop,
|
next = NOOP,
|
||||||
replace = noop,
|
replace = NOOP,
|
||||||
with = Arm,
|
with = ARM,
|
||||||
without = Arm,
|
without = ARM,
|
||||||
archetypes = function()
|
archetypes = function()
|
||||||
return empty_list
|
return EMPTY_LIST
|
||||||
end,
|
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
|
for i, column in columns do
|
||||||
column[row] = select(i, ...)
|
column[row] = select(i, ...)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function world_query(world: World, ...)
|
|
||||||
-- breaking
|
|
||||||
if (...) == nil then
|
|
||||||
error("Missing components")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
local function world_query(world: World, ...)
|
||||||
local compatible_archetypes = {}
|
local compatible_archetypes = {}
|
||||||
local length = 0
|
local length = 0
|
||||||
|
|
||||||
|
@ -1339,7 +1332,7 @@ do
|
||||||
local tr = records[id]
|
local tr = records[id]
|
||||||
queryOutput[j] = columns[tr.column][row]
|
queryOutput[j] = columns[tr.column][row]
|
||||||
end
|
end
|
||||||
world_query_replace_values(row, columns, fn(unpack(queryOutput)))
|
columns_replace_values(row, columns, fn(unpack(queryOutput)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1416,7 +1409,6 @@ do
|
||||||
setmetatable(it, it)
|
setmetatable(it, it)
|
||||||
|
|
||||||
return it
|
return it
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local World = {}
|
local World = {}
|
||||||
|
@ -1436,6 +1428,74 @@ World.target = world_target
|
||||||
World.parent = world_parent
|
World.parent = world_parent
|
||||||
World.contains = world_contains
|
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()
|
function World.new()
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
archetypeIndex = {} :: { [string]: Archetype },
|
archetypeIndex = {} :: { [string]: Archetype },
|
||||||
|
@ -1592,6 +1652,7 @@ return {
|
||||||
Delete = EcsDelete :: Entity,
|
Delete = EcsDelete :: Entity,
|
||||||
Remove = EcsRemove :: Entity,
|
Remove = EcsRemove :: Entity,
|
||||||
Tag = EcsTag :: Entity,
|
Tag = EcsTag :: Entity,
|
||||||
|
Name = EcsName :: Entity<string>,
|
||||||
Rest = EcsRest :: Entity,
|
Rest = EcsRest :: Entity,
|
||||||
|
|
||||||
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,
|
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,
|
||||||
|
|
Loading…
Reference in a new issue