Skip to content

Commit d1d5b1f

Browse files
authored
Add entity ranges (#221)
* Add component registration and metadata API * Fix test case erroring * Initial commit * Simplify entity swap logic in world_entity function * Allow to disconnect signals * Remove appending to array * Add ecs_assert and fix entity range handling * Fix listener array indexing in observers * Only max_id and alive_count if range_begin is larger than built in ranges * No nullable records * Index is not a stable pointer
1 parent 846722b commit d1d5b1f

File tree

9 files changed

+223
-58
lines changed

9 files changed

+223
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
1111
## [Unreleased]
1212

1313
- `[world]`:
14+
- Added `world:range` to allow for creating
1415
- Changed `world:clear` to also look through the component record for the cleared `ID`
1516
- Removes the cleared ID from every entity that has it
1617
- Changed entity ID layouts by putting the index in the lower bits, which should make every world function 1-5 nanoseconds faster

addons/observers.luau

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,6 @@ local function join(world, component)
8989
end
9090
end
9191

92-
local positions_join = join(world, Position)
93-
94-
for e, v in positions_join() do
95-
96-
end
97-
9892
local function query_changed(world, component)
9993
assert(jecs.IS_PAIR(component) == false)
10094
local callerid = debug.info(2, "sl")
@@ -208,54 +202,53 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
208202

209203
world.added = function(_, component, fn)
210204
local listeners = signals.added[component]
211-
local max = #listeners + 1
212205
local component_index = world.component_index :: jecs.ComponentIndex
213206
assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with")
214207
if not listeners then
215208
listeners = {}
216209
signals.added[component] = listeners
210+
217211
local function on_add(entity: number, id: number, value: any)
218212
for _, listener in listeners :: any do
219213
listener(entity, id, value)
220214
end
221215
end
222216
world:set(component, jecs.OnAdd, on_add)
223217
end
224-
listeners[max] = fn
218+
table.insert(listeners, fn)
225219
return function()
226220
local n = #listeners
227-
listeners[max] = listeners[n]
221+
local i = table.find(listeners, fn)
222+
listeners[i] = listeners[n]
228223
listeners[n] = nil
229224
end
230225
end
231226

232227
world.changed = function(_, component, fn)
233228
local listeners = signals.emplaced[component]
234-
local max = 0
235229
local component_index = world.component_index :: jecs.ComponentIndex
236230
assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with")
237231
if not listeners then
238232
listeners = {}
239-
max = 1
240233
signals.emplaced[component] = listeners
241234
local function on_change(entity: number, id: number, value: any)
242235
for _, listener in listeners :: any do
243236
listener(entity, id, value)
244237
end
245238
end
246239
world:set(component, jecs.OnChange, on_change)
247-
else
248-
max = #listeners + 1
249240
end
250-
listeners[max] = fn
241+
table.insert(listeners, fn)
251242
return function()
252-
243+
local n = #listeners
244+
local i = table.find(listeners, fn)
245+
listeners[i] = listeners[n]
246+
listeners[n] = nil
253247
end
254248
end
255249

256250
world.removed = function(_, component, fn)
257251
local listeners = signals.removed[component]
258-
local max = #listeners
259252
local component_index = world.component_index :: jecs.ComponentIndex
260253
assert(component_index[component] == nil, "You cannot use hooks on components you intend to use this signal with")
261254
if not listeners then
@@ -268,10 +261,11 @@ local function observers_add(world: jecs.World & { [string]: any }): PatchedWorl
268261
end
269262
world:set(component, jecs.OnRemove, on_remove)
270263
end
271-
listeners[max] = fn
264+
table.insert(listeners, fn)
272265
return function()
273266
local n = #listeners
274-
listeners[max] = listeners[n]
267+
local i = table.find(listeners, fn)
268+
listeners[i] = listeners[n]
275269
listeners[n] = nil
276270
end
277271
end

benches/visual/insertion.bench.luau

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
local ReplicatedStorage = game:GetService("ReplicatedStorage")
55
local Matter = require(ReplicatedStorage.DevPackages.Matter)
66
local ecr = require(ReplicatedStorage.DevPackages.ecr)
7-
local jecs = require(ReplicatedStorage.Lib)
8-
local newWorld = Matter.World.new()
7+
local jecs = require(ReplicatedStorage.Lib:Clone())
98
local ecs = jecs.World.new()
10-
local mirror = require(ReplicatedStorage.mirror)
9+
local mirror = require(ReplicatedStorage.mirror:Clone())
1110
local mcs = mirror.World.new()
1211

1312
local A1 = Matter.component()

benches/visual/spawn.bench.luau

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,32 @@
22
--!native
33

44
local ReplicatedStorage = game:GetService("ReplicatedStorage")
5-
local Matter = require(ReplicatedStorage.DevPackages.Matter)
6-
local ecr = require(ReplicatedStorage.DevPackages.ecr)
7-
local jecs = require(ReplicatedStorage.Lib)
8-
local newWorld = Matter.World.new()
9-
local ecs = jecs.World.new()
5+
local jecs = require(ReplicatedStorage.Lib:Clone())
6+
local mirror = require(ReplicatedStorage.mirror:Clone())
7+
8+
109

1110
return {
1211
ParameterGenerator = function()
13-
local registry2 = ecr.registry()
14-
15-
return registry2
12+
local ecs = jecs.world()
13+
ecs:range(1000, 20000)
14+
local mcs = mirror.World.new()
15+
return ecs, mcs
1616
end,
1717

1818
Functions = {
19-
Matter = function()
20-
for i = 1, 1000 do
21-
newWorld:spawn()
22-
end
23-
end,
19+
Mirror = function(_, ecs, mcs)
20+
for i = 1, 100 do
2421

25-
ECR = function(_, registry2)
26-
for i = 1, 1000 do
27-
registry2.create()
22+
mcs:entity()
2823
end
2924
end,
3025

31-
Jecs = function()
32-
for i = 1, 1000 do
26+
Jecs = function(_, ecs, mcs)
27+
for i = 1, 100 do
28+
3329
ecs:entity()
3430
end
3531
end,
36-
},
32+
},
3733
}

jecs.luau

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ type ecs_entity_index_t = {
6262
sparse_array: Map<i24, ecs_record_t>,
6363
alive_count: number,
6464
max_id: number,
65+
range_begin: number?,
66+
range_end: number?
6567
}
6668

6769
type ecs_query_data_t = {
@@ -125,6 +127,12 @@ local ECS_INTERNAL_ERROR = [[
125127
https://github.com/Ukendio/jecs/issues/new?template=BUG-REPORT.md
126128
]]
127129

130+
local function ecs_assert(condition, msg: string?)
131+
if not condition then
132+
error(msg)
133+
end
134+
end
135+
128136
local ecs_metadata: Map<i53, Map<i53, any>> = {}
129137
local ecs_max_component_id = 0
130138
local ecs_max_tag_id = EcsRest
@@ -185,6 +193,10 @@ local function ECS_ENTITY_T_LO(e: i53): i24
185193
return e % ECS_ENTITY_MASK
186194
end
187195

196+
local function ECS_ID(e: i53)
197+
return e % ECS_ENTITY_MASK
198+
end
199+
188200
local function ECS_GENERATION(e: i53)
189201
return e // ECS_ENTITY_MASK
190202
end
@@ -249,10 +261,10 @@ local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i
249261
return entity_index_try_get(entity_index, entity) ~= nil
250262
end
251263

252-
local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53?
253-
local r = entity_index_try_get_any(index, entity)
264+
local function entity_index_get_alive(entity_index: ecs_entity_index_t, entity: i53): i53?
265+
local r = entity_index_try_get_any(entity_index, entity)
254266
if r then
255-
return index.dense_array[r.dense]
267+
return entity_index.dense_array[r.dense]
256268
end
257269
return nil
258270
end
@@ -280,23 +292,30 @@ local function ecs_get_alive(world, entity)
280292
return current
281293
end
282294

295+
local ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY = "Entity is outside range"
296+
283297
local function entity_index_new_id(entity_index: ecs_entity_index_t): i53
284298
local dense_array = entity_index.dense_array
285299
local alive_count = entity_index.alive_count
300+
local sparse_array = entity_index.sparse_array
286301
local max_id = entity_index.max_id
287-
if alive_count ~= max_id then
302+
303+
if alive_count < max_id then
288304
alive_count += 1
289305
entity_index.alive_count = alive_count
290306
local id = dense_array[alive_count]
291307
return id
292308
end
293309

294310
local id = max_id + 1
311+
local range_end = entity_index.range_end
312+
ecs_assert(range_end == nil or id < range_end, ECS_INTERNAL_ERROR_INCOMPATIBLE_ENTITY)
313+
295314
entity_index.max_id = id
296315
alive_count += 1
297316
entity_index.alive_count = alive_count
298317
dense_array[alive_count] = id
299-
entity_index.sparse_array[id] = { dense = alive_count } :: ecs_record_t
318+
sparse_array[id] = { dense = alive_count } :: ecs_record_t
300319

301320
return id
302321
end
@@ -583,10 +602,10 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
583602
local is_pair = ECS_IS_PAIR(id)
584603
if is_pair then
585604
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id)) :: i53
586-
assert(relation and entity_index_is_alive(
605+
ecs_assert(relation and entity_index_is_alive(
587606
entity_index, relation), ECS_INTERNAL_ERROR)
588607
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id)) :: i53
589-
assert(target and entity_index_is_alive(
608+
ecs_assert(target and entity_index_is_alive(
590609
entity_index, target), ECS_INTERNAL_ERROR)
591610
end
592611

@@ -719,8 +738,100 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev:
719738
return archetype
720739
end
721740

722-
local function world_entity(world: ecs_world_t): i53
723-
return entity_index_new_id(world.entity_index)
741+
local function world_range(world: ecs_world_t, range_begin: number, range_end: number?)
742+
local entity_index = world.entity_index
743+
744+
entity_index.range_begin = range_begin
745+
entity_index.range_end = range_end
746+
747+
local max_id = entity_index.max_id
748+
749+
if range_begin > max_id then
750+
local dense_array = entity_index.dense_array
751+
local sparse_array = entity_index.sparse_array
752+
753+
for i = max_id, range_begin do
754+
dense_array[i] = i
755+
sparse_array[i] = {
756+
dense = 0
757+
} :: ecs_record_t
758+
end
759+
entity_index.max_id = range_begin - 1
760+
entity_index.alive_count = range_begin - 1
761+
end
762+
end
763+
764+
local function world_entity(world: ecs_world_t, entity: i53?): i53
765+
local entity_index = world.entity_index
766+
if entity then
767+
local index = ECS_ID(entity)
768+
local max_id = entity_index.max_id
769+
local sparse_array = entity_index.sparse_array
770+
local dense_array = entity_index.dense_array
771+
local alive_count = entity_index.alive_count
772+
local r = sparse_array[index]
773+
if r then
774+
local dense = r.dense
775+
if not dense or dense == 0 then
776+
dense = index
777+
end
778+
local any = dense_array[dense]
779+
if any == entity then
780+
if alive_count > dense then
781+
return entity
782+
end
783+
local e_swap = dense_array[alive_count]
784+
local r_swap = sparse_array[alive_count]
785+
r_swap.dense = dense
786+
r.dense = alive_count
787+
dense_array[alive_count] = entity
788+
dense_array[dense] = e_swap
789+
return entity
790+
end
791+
792+
-- assert(any ~= 0) should never happen
793+
794+
local e_swap = dense_array[alive_count]
795+
local r_swap = sparse_array[alive_count]
796+
797+
if dense <= alive_count then
798+
alive_count += 1
799+
entity_index.alive_count = alive_count
800+
end
801+
802+
r_swap.dense = dense
803+
r.dense = alive_count
804+
dense_array[alive_count] = any
805+
dense_array[dense] = e_swap
806+
return any
807+
else
808+
for i = max_id + 1, index do
809+
sparse_array[i] = { dense = i } :: ecs_record_t
810+
dense_array[i] = i
811+
end
812+
entity_index.max_id = index
813+
814+
local e_swap = dense_array[alive_count]
815+
local r_swap = sparse_array[alive_count]
816+
r_swap.dense = index
817+
818+
alive_count += 1
819+
entity_index.alive_count = alive_count
820+
821+
r = sparse_array[index]
822+
823+
r.dense = alive_count
824+
825+
sparse_array[index] = r
826+
827+
dense_array[index] = e_swap
828+
dense_array[alive_count] = entity
829+
830+
831+
return entity
832+
end
833+
end
834+
return entity_index_new_id(entity_index, entity)
724835
end
725836

726837
local function world_parent(world: ecs_world_t, entity: i53)
@@ -2311,6 +2422,8 @@ export type EntityIndex = {
23112422
sparse_array: Map<i24, Record>,
23122423
alive_count: number,
23132424
max_id: number,
2425+
range_begin: number?,
2426+
range_end: number?
23142427
}
23152428

23162429
local World = {}
@@ -2332,6 +2445,7 @@ World.contains = world_contains
23322445
World.cleanup = world_cleanup
23332446
World.each = world_each
23342447
World.children = world_children
2448+
World.range = world_range
23352449

23362450
local function world_new()
23372451
local entity_index = {
@@ -2454,6 +2568,9 @@ export type World = {
24542568

24552569
observable: any,
24562570

2571+
--- Enforce a check on entities to be created within desired range
2572+
range: (self: World, range_begin: number, range_end: number?) -> (),
2573+
24572574
--- Creates a new entity
24582575
entity: (self: World, id: Entity?) -> Entity,
24592576
--- Creates a new entity located in the first 256 ids.
@@ -2558,6 +2675,8 @@ return {
25582675
ECS_META_RESET = ECS_META_RESET,
25592676

25602677
IS_PAIR = (ECS_IS_PAIR :: any) :: <P, O>(pair: Pair<P, O>) -> boolean,
2678+
ECS_PAIR_FIRST = ECS_PAIR_FIRST,
2679+
ECS_PAIR_SECOND = ECS_PAIR_SECOND,
25612680
pair_first = (ecs_pair_first :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>,
25622681
pair_second = (ecs_pair_second :: any) :: <P, O>(world: World, pair: Pair<P, O>) -> Id<O>,
25632682
entity_index_get_alive = entity_index_get_alive,

0 commit comments

Comments
 (0)