Compare commits

...

76 commits
v0.7.2 ... main

Author SHA1 Message Date
Ukendio
0b6bfea5c8 Return nil if nth is over count
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-08-02 23:57:50 +02:00
Ukendio
3cfce10a4a Increment component records after registering
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-08-02 18:58:19 +02:00
Ukendio
add9ad3939 Support setting signal on cached Relation
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-08-02 06:20:53 +02:00
Ukendio
4153a7cdfe Monitors and observers need to able to accept pair terms
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-07-29 21:08:23 +02:00
Ukendio
4230a0a797 Version 2025-07-29 21:07:59 +02:00
dai
499afc20cd
Add rbxts typings for signals (#260)
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
* Add rbxts typings for signals

* Correct arguments
2025-07-27 17:31:31 +02:00
Ukendio
f284de6ec1 Simplying 2025-07-27 17:07:41 +02:00
Ukendio
abc0b1ec22 Fix typos 2025-07-27 17:05:15 +02:00
Marcus
b521fe750a
Bump versions (#259) 2025-07-27 14:39:43 +02:00
EncodedVenom
998b1d3528
Update query.md - formatting
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-26 18:55:38 -04:00
EncodedVenom
0606bf70f0
Merge pull request #258 from daimond113/fix/remove-entity-type-generic
Remove data generic from entity in hooks
2025-07-26 18:48:23 -04:00
daimond113
f2a803c0d8
Remove data generic from entity in hooks 2025-07-27 00:13:14 +02:00
Ukendio
3e46b723e9 Clarify exists usage
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-26 02:50:24 +02:00
Ukendio
3ab1d970e2 Remove dead links 2025-07-26 02:45:13 +02:00
Ukendio
3e2d40e706 Make a table for cleanup 2025-07-26 02:41:40 +02:00
Ukendio
1c2dee57d3 Add some examples 2025-07-26 02:41:18 +02:00
Ukendio
3777585677 Address docs issues 2025-07-26 02:09:28 +02:00
Ukendio
f792c98585 Bump
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-07-24 04:57:06 +02:00
Ukendio
7b43748f18 Fix types issues 2025-07-24 02:25:56 +02:00
Ukendio
666a3ef6de Fix inner types 2025-07-24 02:04:36 +02:00
Ukendio
69911093a3 No need to check for 0 anymore
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-23 00:38:16 +02:00
Ukendio
59df0bf2a3 Bump
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-07-19 14:30:48 +02:00
Ukendio
3e995c9d7d Fix iterator not returning correct column for 8+ overloads 2025-07-19 14:29:36 +02:00
Ukendio
78fe5338cf index component record after archetype gets created
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-18 16:53:32 +02:00
Ukendio
ca0689c92b Bump 2025-07-18 15:14:39 +02:00
Ukendio
117a5e0ca7 Fix on_remove hook not called on cached edge 2025-07-18 15:13:59 +02:00
Ukendio
d99088ea1e Bump rc
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-18 00:38:39 +02:00
Ukendio
012b5e2bfa Make cleanup conditions 2025-07-18 00:37:58 +02:00
Ukendio
54b21001ab Bump versions 2025-07-17 23:45:56 +02:00
Ukendio
9c09686a69 always check OnDelete condition 2025-07-17 23:45:04 +02:00
Ukendio
ebc39c8b28 Remove debug msgs
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-17 19:38:24 +02:00
Ukendio
7b86084b94 Bump versions 2025-07-17 19:34:29 +02:00
Ukendio
25ceda5cee Remove alive count upvalue 2025-07-17 18:50:21 +02:00
Ukendio
fc56b6f716 Retrieve updated max_id 2025-07-17 18:47:05 +02:00
dai
c30328527a
Add typings for bulk operations (#257)
* Add typings for bulk operations

* Use Id
2025-07-17 18:25:37 +02:00
Ukendio
c3853023d0 Allow creating an entity with a non-zero generation below range 2025-07-17 18:24:44 +02:00
Ukendio
7b253e1c2a Remove archetype recycling 2025-07-17 18:07:11 +02:00
Ukendio
210d62d463 Add tests for archetype edges cleanup
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-07-14 15:58:14 +02:00
Ukendio
3f6f8c1739 Export jecs.record for retrieving entity records 2025-07-14 15:57:47 +02:00
Ukendio
7f66d21e6d Recycle component records 2025-07-14 14:17:18 +02:00
Ukendio
5334d8734d Export jecs.Component in roblox-ts types 2025-07-14 13:12:41 +02:00
Ukendio
a6ae67250c Use old interface
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-14 00:47:20 +02:00
Ukendio
9ae32bcce8 Update hook retrieval 2025-07-14 00:45:01 +02:00
Ukendio
f6731069aa Add backwards relation to edges
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-13 05:15:55 +02:00
Ukendio
c67dfcbd24 Bump versions
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-07-10 12:57:51 +02:00
Ukendio
e1545710db LF 2025-07-10 12:52:33 +02:00
Ukendio
aa178981dc Hotfix archetype not being marked dead 2025-07-10 12:51:04 +02:00
Ukendio
ad5ed3b5ea Increment alive count if under dense
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-07-06 19:42:10 +02:00
Ukendio
362490d25e Always swap entity 2025-07-06 18:49:28 +02:00
Ukendio
6c1793f853 Remove indirections to entity_index 2025-07-06 17:57:35 +02:00
Ukendio
6b6f6fb961 Bump versions 2025-07-06 17:52:59 +02:00
Ukendio
29350e6ec3 Fix bug to allow deletion outside partitioned range 2025-07-06 17:44:02 +02:00
Ukendio
eed1b6179e Export ArchetypeOnCreate and ArchetypeOnDelete events 2025-07-06 17:08:29 +02:00
Ukendio
cf94a48a40 Optimized observers 2025-07-06 17:07:43 +02:00
Ukendio
23540e5919 Make iterators simple functions
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-06 09:52:09 +02:00
Ukendio
169ec09ed5 Change monitor to be iterable 2025-07-06 09:48:13 +02:00
Ukendio
a9891abf6d Make callback optional 2025-07-06 09:42:49 +02:00
Ukendio
6dfb428296 Updated observers 2025-07-06 09:33:31 +02:00
Ukendio
b92cf9ab76 Bump versions 2025-07-06 08:52:17 +02:00
Ukendio
5aedb5e730 Bump wally 2025-07-06 08:51:10 +02:00
Ukendio
1c524f1587 Update TS types 2025-07-06 08:49:10 +02:00
Ukendio
6053038cc1 Move only once during removal of invalidated pair
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-06 08:45:19 +02:00
Ukendio
29305cac5d Remove second loop in archetype destroy 2025-07-06 07:50:03 +02:00
Ukendio
8fd32978b4 Fix changelog
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-07-04 22:00:23 +02:00
Ukendio
c9d888aeb9 Export try_get_fast
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-04 04:58:20 +02:00
Marcus
155d51a080 Add exclusive relations (#250)
* Add exclusive relationship

* Remove focus

* Remove whitespace

* Make ChildOf exclusive

* Test exclusive relation perf

* Inline into world:add

* Inline into world:set

* Fix benchmark of remove
2025-07-04 04:24:14 +02:00
Marcus
b425150b0c Add exclusive relations (#250)
* Add exclusive relationship

* Remove focus

* Remove whitespace

* Make ChildOf exclusive

* Test exclusive relation perf

* Inline into world:add

* Inline into world:set

* Fix benchmark of remove
2025-07-04 04:23:28 +02:00
Ukendio
13facc3719 Add inference for IDs in methods
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-03 01:41:06 +02:00
Ukendio
a6ba9f4bd5 Update networking example
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-02 20:26:57 +02:00
Ukendio
53f705ac2e Compare archetype move performance boost
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-06-30 23:11:05 +02:00
Ukendio
ff4b0bf612 Use btest instead of band 2025-06-30 22:53:40 +02:00
renyang19910211
9b57189c3a
Fix receive_replication.luau removed issue (#243)
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-06-30 22:41:29 +02:00
Ukendio
4ff492ceaf Optimize moving archetype 2025-06-30 22:37:30 +02:00
Ukendio
7c8358656a unsafe get
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-06-30 01:44:25 +02:00
Marcus
d6e720f200
Optimize removal path (#248)
* Optimize removal path

* Replace eindex_get implementation
2025-06-30 01:06:31 +02:00
Marcus
3c7f3b4eb3
0.7.3 (#247)
* 0.7.3

* Remove print

* fix jecs.meta for adding values
2025-06-30 00:40:03 +02:00
35 changed files with 4273 additions and 2494 deletions

View file

@ -2,6 +2,20 @@
## Unreleased ## Unreleased
### Added
- `jecs.Exclusive` trait for making exclusive relationships.
### Changed
- `jecs.ChildOf` to be an exclusive relationship, which means you can only have one `ChildOf` pair on an entity.
## 0.7.2
### Added
- `jecs.entity_index_try_get_fast` back as to not break the observer addon.
### Fixed
- A linting problem with the types for `quer:with` and `query:without`.
## 0.7.0 ## 0.7.0
### Added ### Added
@ -10,7 +24,7 @@
- `bulk_insert` and `bulk_remove` respectively for moving an entity to an archetype without intermediate steps. - `bulk_insert` and `bulk_remove` respectively for moving an entity to an archetype without intermediate steps.
### Changed ### Changed
- The fields `archetype.records[id]` and `archetype.counts[id` have been removed from the archetype struct and been moved to the component record `component_index[id].records[archetype.id]` and `component_index[id].counts[archetype.id]` respectively. - The fields `archetype.records[id]` and `archetype.counts[id]` have been removed from the archetype struct and been moved to the component record `component_index[id].records[archetype.id]` and `component_index[id].counts[archetype.id]` respectively.
- Removed the metatable `jecs.World`. Use `jecs.world()` to create your World. - Removed the metatable `jecs.World`. Use `jecs.world()` to create your World.
- Archetypes will no longer be garbage collected when invalidated, allowing them to be recycled to save a lot of performance during frequent deletion. - Archetypes will no longer be garbage collected when invalidated, allowing them to be recycled to save a lot of performance during frequent deletion.
- Removed `jecs.entity_index_try_get_fast`. Use `jecs.entity_index_try_get` instead. - Removed `jecs.entity_index_try_get_fast`. Use `jecs.entity_index_try_get` instead.

217
addons/ob.luau Executable file
View file

@ -0,0 +1,217 @@
--!strict
local jecs = require("@jecs")
type World = jecs.World
type Query<T...> = jecs.Query<T...>
type Id<T=any> = jecs.Id<T>
type Entity<T> = jecs.Entity<T>
export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
export type Observer<T...> = typeof(setmetatable(
{} :: {
iter: Iter<T...>,
entities: { Entity<nil> },
disconnect: (Observer<T...>) -> ()
},
{} :: {
__iter: Iter<T...>,
}
))
local function observers_new<T...>(
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
query:cached()
local world = (query :: Query<T...> & { world: World }).world
callback = callback
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
local observer_on_create = observers_on_create[#observers_on_create]
observer_on_create.callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
local observer_on_delete = observers_on_delete[#observers_on_delete]
observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>(
entity: jecs.Entity<T>,
id: jecs.Id<a>,
value: a?
)
local r = entity_index.sparse_array[jecs.ECS_ID(entity)]
local archetype = r.archetype
if archetypes[archetype.id] then
i += 1
entities[i] = entity
if callback ~= nil then
callback(entity, id, value)
end
end
end
for _, term in terms do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
end
world:added(term, emplaced)
world:changed(term, emplaced)
end
local function disconnect()
table.remove(observers_on_create, table.find(
observers_on_create,
observer_on_create
))
table.remove(observers_on_delete, table.find(
observers_on_delete,
observer_on_delete
))
end
local function iter()
local row = i
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end
end
local observer = {
disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
}
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...>
end
local function monitors_new<T...>(
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
query:cached()
local world = (query :: Query<T...> & { world: World }).world
local archetypes = {}
local terms = query.ids
local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
local observer_on_create = observers_on_create[#observers_on_create]
observer_on_create.callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
local observer_on_delete = observers_on_delete[#observers_on_delete]
observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>(
entity: jecs.Entity<T>,
id: jecs.Id<a>,
value: a?
)
local r = jecs.entity_index_try_get_fast(
entity_index, entity :: any) :: jecs.Record
local archetype = r.archetype
if archetypes[archetype.id] then
i += 1
entities[i] = entity
if callback ~= nil then
callback(entity, jecs.OnAdd)
end
end
end
local function removed(entity: jecs.Entity, component: jecs.Id)
local EcsOnRemove = jecs.OnRemove :: jecs.Id
if callback ~= nil then
callback(entity, EcsOnRemove)
end
end
for _, term in terms do
if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term)
end
world:added(term, emplaced)
world:removed(term, removed)
end
local function disconnect()
table.remove(observers_on_create, table.find(
observers_on_create,
observer_on_create
))
table.remove(observers_on_delete, table.find(
observers_on_delete,
observer_on_delete
))
end
local function iter()
local row = i
return function()
if row == 0 then
i = 0
table.clear(entities)
end
local entity = entities[row]
row -= 1
return entity
end
end
local observer = {
disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
}
setmetatable(observer, observer)
return (observer :: any) :: Observer<T...>
end
return {
monitor = monitors_new,
observer = observers_new
}

View file

@ -1,268 +0,0 @@
local jecs = require("@jecs")
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
observer: (
PatchedWorld,
any,
(jecs.Entity) -> ()
) -> (),
monitor: (
PatchedWorld,
any,
(jecs.Entity, jecs.Id) -> ()
) -> ()
}
local function observers_new(world, query, 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, id, value)
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 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
max_id -= 1
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 monitors_new(world, query, 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
local EcsOnRemove = jecs.OnRemove :: jecs.Id
callback(entity, EcsOnRemove)
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
type Signal = { [jecs.Entity]: { (...any) -> () } }
local world_mut = world :: jecs.World & {[string]: any}
local signals = {
added = {} :: Signal,
emplaced = {} :: Signal,
removed = {} :: Signal
}
world_mut.added = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.added[component]
if not listeners then
listeners = {}
signals.added[component] = listeners
local function on_add(entity, id, value)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnAdd)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.hooks.on_add = on_add
else
world:set(component, jecs.OnAdd, on_add)
end
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_mut.changed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
)
local listeners = signals.emplaced[component]
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local function on_change(entity, id, value: any)
for _, listener in listeners :: any do
listener(entity, id, value)
end
end
local existing_hook = world:get(component, jecs.OnChange)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.hooks.on_change = on_change
else
world:set(component, jecs.OnChange, on_change)
end
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_mut.removed = function<T>(
_: jecs.World,
component: jecs.Id<T>,
fn: (e: jecs.Entity, id: jecs.Id) -> ()
)
local listeners = signals.removed[component]
if not listeners then
listeners = {}
signals.removed[component] = listeners
local function on_remove(entity, id)
for _, listener in listeners :: any do
listener(entity, id)
end
end
local existing_hook = world:get(component, jecs.OnRemove)
if existing_hook then
table.insert(listeners, existing_hook)
end
local idr = world.component_index[component]
if idr then
idr.hooks.on_remove = on_remove
else
world:set(component, jecs.OnRemove, on_remove)
end
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_mut.signals = signals
world_mut.observer = observers_new
world_mut.monitor = monitors_new
world_mut.trackers = {}
return world_mut :: PatchedWorld
end
return observers_add

View file

@ -1,61 +1,65 @@
--!optimize 2 --!optimize 2
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter) local jecs = require(ReplicatedStorage.Lib:Clone())
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair local pair = jecs.pair
local ecs = jecs.world() local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror) local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.World.new() local mcs = mirror.world()
local C1 = ecs:component() local C1 = ecs:component()
local C2 = ecs:entity() local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
local C3 = ecs:entity() local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity() local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component() local E1 = mcs:component()
local E2 = mcs:entity() local E2 = mcs:entity()
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity() local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity() local E4 = mcs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
local m = mcs:entity()
local j = ecs:entity()
return { return {
ParameterGenerator = function() ParameterGenerator = function()
local j = ecs:entity()
ecs:set(j, C1, true)
local m = mcs:entity()
mcs:set(m, E1, true)
for i = 1, 1000 do
local friend1 = ecs:entity()
local friend2 = mcs:entity()
ecs:add(friend1, pair(C2, j))
ecs:add(friend1, pair(C3, j))
ecs:add(friend1, pair(C4, j))
mcs:add(friend2, pair(E2, m))
mcs:add(friend2, pair(E3, m))
mcs:add(friend2, pair(E4, m))
end
return {
m = m,
j = j,
}
end, end,
Functions = { Functions = {
Mirror = function(_, a) Mirror = function()
mcs:delete(a.m) for i = 1, 10 do
local friend2 = mcs:entity()
mcs:add(friend2, pair(E2, m))
mcs:add(friend2, pair(E3, m))
mcs:add(friend2, pair(E4, m))
-- local r = mirror.entity_index_try_get_fast(mcs.entity_index, friend2)
-- local archetype = r.archetype
-- mirror.archetype_destroy(mcs, archetype)
mcs:delete(m)
m = mcs:entity(m)
end
end, end,
Jecs = function(_, a) Jecs = function()
ecs:delete(a.j) for i = 1, 10 do
local friend1 = ecs:entity()
ecs:add(friend1, pair(C2, j))
ecs:add(friend1, pair(C3, j))
ecs:add(friend1, pair(C4, j))
-- local r = jecs.entity_index_try_get_fast(ecs.entity_index, friend1)
-- local archetype = r.archetype
-- jecs.archetype_destroy(ecs, archetype)
ecs:delete(j)
j = ecs:entity()
end
end, end,
}, },
} }

View file

@ -5,8 +5,7 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.Lib:Clone()) local jecs = require(ReplicatedStorage.Lib:Clone())
local ecs = jecs.world() local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror:Clone()) local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.World.new() local mcs = mirror.world()
local C1 = ecs:component() local C1 = ecs:component()
local C2 = ecs:component() local C2 = ecs:component()

View file

@ -2,7 +2,7 @@
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.matter) local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local newWorld = Matter.World.new() local newWorld = Matter.World.new()

View file

@ -4,15 +4,17 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter) local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib) local jecs = require(ReplicatedStorage.Lib:Clone())
local pair = jecs.pair local pair = jecs.pair
local ecs = jecs.world() local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror) local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.World.new() local mcs = mirror.world()
local C1 = ecs:component() local C1 = ecs:component()
local C2 = ecs:entity() local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
ecs:add(C2, jecs.Exclusive)
local C3 = ecs:entity() local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity() local C4 = ecs:entity()
@ -33,16 +35,17 @@ return {
Mirror = function() Mirror = function()
local m = mcs:entity() local m = mcs:entity()
for i = 1, 100 do for i = 1, 100 do
mcs:add(m, E3) mcs:add(m, pair(E2, E3))
mcs:remove(m, E3) mcs:remove(m, pair(E2, E3))
mcs:add(m, pair(E2, E4))
end end
end, end,
Jecs = function() Jecs = function()
local j = ecs:entity() local j = ecs:entity()
for i = 1, 100 do for i = 1, 100 do
ecs:add(j, C3) ecs:add(j, pair(C2, C3))
ecs:remove(j, C3) ecs:add(j, pair(C2, C4))
end end
end, end,
}, },

View file

@ -1,13 +1,11 @@
local function collect<T...>(
signal: { --!strict
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection local function collect(signal)
}
): () -> (T...)
local enqueued = {} local enqueued = {}
local i = 0 local i = 0
local connection = (signal :: any):Connect(function(...) local connection = signal:Connect(function(...)
table.insert(enqueued, { ... }) table.insert(enqueued, { ... })
i += 1 i += 1
end) end)
@ -25,4 +23,11 @@ local function collect<T...>(
end, connection end, connection
end end
return collect type Signal<T... = ...any> = {
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
ConnectParallel: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Once: (self: Signal<T...>, callback: (T...) -> ()) -> RBXScriptConnection,
Wait: (self: Signal<T...>) -> (T...)
}
return collect :: <T...>(Signal<T...>) -> (() -> (T...), RBXScriptConnection)

View file

@ -1,3 +1,5 @@
--!strict
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local types = require("./types") local types = require("./types")
@ -5,31 +7,58 @@ local types = require("./types")
local Networked = jecs.tag() local Networked = jecs.tag()
local NetworkedPair = jecs.tag() local NetworkedPair = jecs.tag()
local Renderable = jecs.component() :: jecs.Id<Instance> local InstanceMapping = jecs.component() :: jecs.Id<Instance>
jecs.meta(Renderable, Networked) jecs.meta(InstanceMapping, jecs.OnAdd, function(component)
jecs.meta(component, jecs.OnAdd, function(entity, _, instance)
if RunService:IsServer() then
instance:SetAttribute("entity_server")
else
instance:SetAttribute("entity_client")
end
end)
end)
local function networked_id(ct)
jecs.meta(ct, Networked)
return ct
end
local function networked_pair(ct)
jecs.meta(ct, NetworkedPair)
return ct
end
local function instance_mapping_id(ct)
jecs.meta(ct, InstanceMapping)
return ct
end
local Poison = jecs.component() :: jecs.Id<number> local Renderable = jecs.component() :: types.Id<Instance>
jecs.meta(Poison, Networked) local Poison = jecs.component() :: types.Id<number>
local Health = jecs.component() :: types.Id<number>
local Health = jecs.component() :: jecs.Id<number> local Player = jecs.component() :: types.Id<Player>
jecs.meta(Health, Networked) local Debuff = jecs.tag() :: types.Entity
local Lifetime = jecs.component() :: types.Id<{
local Player = jecs.component() :: jecs.Id<Player> duration: number,
jecs.meta(Player, Networked) created: number
}>
local Destroy = jecs.tag()
local components = { local components = {
Renderable = Renderable, Renderable = networked_id(instance_mapping_id(Renderable)),
Player = Player, Player = networked_id(Player),
Poison = Poison, Poison = networked_id(Poison),
Health = Health, Health = networked_id(Health),
Lifetime = networked_id(Lifetime),
Debuff = networked_id(Debuff),
Destroy = networked_id(Destroy),
-- We have to define that some builtin IDs can also be networked
ChildOf = networked_pair(jecs.ChildOf),
Networked = Networked, Networked = Networked,
NetworkedPair = NetworkedPair, NetworkedPair = NetworkedPair,
} }
for name, component in components do for name, component in components :: {[string]: types.Id<any> } do
jecs.meta(component, jecs.Name, name) jecs.meta(component, jecs.Name, name)
end end

View file

@ -1,13 +1,13 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule) local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN local RUN = schedule.RUN
require(ReplicatedStorage.components) require(ReplicatedStorage.components)
local world = observers_add(jecs.world()) local world = jecs.world()
local systems = ReplicatedStorage.systems local systems = ReplicatedStorage.systems
SYSTEM(world, systems.receive_replication) SYSTEM(world, systems.receive_replication)
SYSTEM(world, systems.entities_delete)
RUN(world) 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,40 +1,32 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../ReplicatedStorage/types") local types = require("../ReplicatedStorage/types")
type Signal<T...> = {
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
}
type Remote<T...> = { type Remote<T...> = {
FireClient: (Remote<T...>, T...) -> (), FireClient: (Remote<T...>, Player, T...) -> (),
FireAllClients: (Remote<T...>, T...) -> (), FireAllClients: (Remote<T...>, T...) -> (),
FireServer: (Remote<T...>) -> (), FireServer: (Remote<T...>, T...) -> (),
OnServerEvent: { OnServerEvent: RBXScriptSignal<(Player, T...)>,
Connect: (any, fn: (Player, T...) -> () ) -> () OnClientEvent: RBXScriptSignal<T...>
},
OnClientEvent: {
Connect: (any, fn: (T...) -> () ) -> ()
}
} }
local function stream_ensure(name): Remote<any> local function stream_ensure(name)
local remote = ReplicatedStorage:FindFirstChild(name) local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then if not remote then
remote = Instance.new("RemoteEvent") remote = Instance.new("RemoteEvent")
remote.Name = name remote.Name = name
remote.Parent = ReplicatedStorage remote.Parent = ReplicatedStorage
end end
return remote :: any return remote
end end
local function datagram_ensure(name): Remote<any> local function datagram_ensure(name)
local remote = ReplicatedStorage:FindFirstChild(name) local remote = ReplicatedStorage:FindFirstChild(name)
if not remote then if not remote then
remote = Instance.new("UnreliableRemoteEvent") remote = Instance.new("UnreliableRemoteEvent")
remote.Name = name remote.Name = name
remote.Parent = ReplicatedStorage remote.Parent = ReplicatedStorage
end end
return remote :: any return remote
end end
return { return {

View file

@ -0,0 +1,13 @@
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require(ReplicatedStorage.types)
local ct = require(ReplicatedStorage.components)
local function entities_delete(world: types.World, dt: number)
for e in world:each(ct.Destroy) do
world:delete(e)
end
end
return entities_delete

View file

@ -2,45 +2,51 @@ local types = require("../types")
local jecs = require(game:GetService("ReplicatedStorage").ecs) local jecs = require(game:GetService("ReplicatedStorage").ecs)
local remotes = require("../remotes") local remotes = require("../remotes")
local collect = require("../collect") local collect = require("../collect")
local client_ids = {} local components = require("../components")
local function ecs_map_get(world, id) local client_ids: {[jecs.Entity]: jecs.Entity } = {}
local deserialised_id = client_ids[id]
if not deserialised_id then local function ecs_ensure_entity(world: jecs.World, id: jecs.Entity)
if world:has(id, jecs.Name) then local e = 0
deserialised_id = world:entity(id)
else local ser_id = id
deserialised_id = world:entity() local deser_id = client_ids[ser_id]
if deser_id then
if deser_id == 0 then
local new_id = world:entity()
client_ids[ser_id] = new_id
deser_id = new_id
end end
else
client_ids[id] = deserialised_id if not world:exists(ser_id)
or (world:contains(ser_id) and not world:get(ser_id, jecs.Name))
then
deser_id = world:entity()
else
if world:contains(ser_id) and world:get(ser_id, jecs.Name) then
deser_id = ser_id
else
deser_id = world:entity()
end
end
client_ids[ser_id] = deser_id
end end
-- local deserialised_id = client_ids[id] e = deser_id
-- if not deserialised_id then
-- if world:has(id, jecs.Name) then
-- deserialised_id = world:entity(id)
-- else
-- if world:exists(id) then
-- deserialised_id = world:entity()
-- else
-- deserialised_id = world:entity(id)
-- end
-- end
-- client_ids[id] = deserialised_id
-- end
return deserialised_id return e
end end
local function ecs_make_alive_id(world, id) -- local rel_render = `e{jecs.ECS_ID(rel)}v{jecs.ECS_GENERATION(rel)}`
local rel = jecs.ECS_PAIR_FIRST(id) -- local tgt_render = `e{jecs.ECS_ID(tgt)}v{jecs.ECS_GENERATION(tgt)}`
local tgt = jecs.ECS_PAIR_SECOND(id) local function ecs_deser_pairs(world, token)
local tokens = string.split(token, ",")
local rel = tonumber(tokens[1])
local tgt = tonumber(tokens[2])
rel = ecs_map_get(world, rel) rel = ecs_ensure_entity(world, rel)
tgt = ecs_map_get(world, tgt) tgt = ecs_ensure_entity(world, tgt)
return jecs.pair(rel, tgt) return jecs.pair(rel, tgt)
end end
@ -48,25 +54,31 @@ end
local snapshots = collect(remotes.replication.OnClientEvent) local snapshots = collect(remotes.replication.OnClientEvent)
return function(world: types.World) return function(world: types.World)
for entity in world:each(components.Destroy) do
client_ids[entity] = nil
end
for snapshot in snapshots do for snapshot in snapshots do
for id, map in snapshot do for ser_id, map in snapshot do
id = tonumber(id) local id = tonumber(ser_id)
if jecs.IS_PAIR(id) then if not id then
id = ecs_make_alive_id(world, id) id = ecs_deser_pairs(world, ser_id)
else
id = ecs_ensure_entity(world, id)
end end
local set = map.set local set = map.set
if set then if set then
if jecs.is_tag(world, id) then if jecs.is_tag(world, id) then
for _, entity in set do for _, entity in set do
entity = ecs_map_get(world, entity) entity = ecs_ensure_entity(world, entity)
world:add(entity, id) world:add(entity, id)
end end
else else
local t = os.clock()
local values = map.values local values = map.values
for i, entity in set do for i, entity in set do
entity = ecs_map_get(world, entity) entity = ecs_ensure_entity(world, entity)
world:set(entity, id, values[i]) world:set(entity, id, values[i])
end end
end end
end end
@ -74,11 +86,9 @@ return function(world: types.World)
local removed = map.removed local removed = map.removed
if removed then if removed then
for i, e in removed do for _, entity in removed do
if not world:contains(e) then entity = ecs_ensure_entity(world, entity)
continue world:remove(entity, id)
end
world:remove(e, id)
end end
end end
end end

View file

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

View file

@ -2,18 +2,20 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService") local ServerScriptService = game:GetService("ServerScriptService")
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local schedule = require(ReplicatedStorage.schedule) local schedule = require(ReplicatedStorage.schedule)
local observers_add = require(ReplicatedStorage.observers_add)
local SYSTEM = schedule.SYSTEM local SYSTEM = schedule.SYSTEM
local RUN = schedule.RUN local RUN = schedule.RUN
require(ReplicatedStorage.components) require(ReplicatedStorage.components)
local world = observers_add(jecs.world()) local world = jecs.world()
local systems = ServerScriptService.systems local systems = ServerScriptService.systems
SYSTEM(world, systems.replication) SYSTEM(world, systems.replication)
SYSTEM(world, systems.players_added) SYSTEM(world, systems.players_added)
SYSTEM(world, systems.poison_hurts) SYSTEM(world, systems.poison_hurts)
SYSTEM(world, systems.health_regen)
SYSTEM(world, systems.lifetimes_expire)
SYSTEM(world, systems.life_is_painful) SYSTEM(world, systems.life_is_painful)
SYSTEM(world, ReplicatedStorage.systems.entities_delete)
RUN(world, 0) RUN(world, 0)

View file

@ -0,0 +1,15 @@
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, health in world:query(ct.Health) do
if math.random() < 1 / 60 / 30 then
world:set(e, ct.Health, 100)
end
end
end

View file

@ -1,12 +1,19 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components) local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types) local types = require(ReplicatedStorage.types)
local jecs = require(ReplicatedStorage.ecs)
return function(world: types.World, dt: number) return function(world: types.World, dt: number)
for e in world:query(ct.Player):without(ct.Health) do if math.random() < (1 / 60 / 7) then
world:set(e, ct.Health, 100) for e in world:each(ct.Health) do
end local poison = world:entity()
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do world:add(poison, ct.Debuff)
world:set(e, ct.Poison, 10) world:add(poison, jecs.pair(jecs.ChildOf, e))
world:set(poison, ct.Poison, 10)
world:set(poison, ct.Lifetime, {
duration = 3,
created = os.clock()
})
end
end end
end end

View file

@ -0,0 +1,12 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components)
local types = require(ReplicatedStorage.types)
return function(world: types.World, dt: number)
for e, lifetime in world:query(ct.Lifetime) do
if os.clock() > lifetime.created + lifetime.duration then
world:add(e, ct.Destroy)
end
end
end

View file

@ -12,9 +12,13 @@ return function(world: types.World, dt: number)
for entity, player in world:query(ct.Player):without(ct.Renderable) do for entity, player in world:query(ct.Player):without(ct.Renderable) do
local character = player.Character local character = player.Character
if character then if not character then
if not character.Parent then continue
world:set(entity, ct.Renderable, character)
end end
if not character.Parent then
continue
end
world:set(entity, ct.Renderable, character)
end end
end end

View file

@ -1,12 +1,18 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ct = require(ReplicatedStorage.components) local ct = require(ReplicatedStorage.components)
return function(world, dt) local types = require(ReplicatedStorage.types)
for e, poison, health in world:query(ct.Poison, ct.Health) do local jecs = require(ReplicatedStorage.ecs)
local health_after_tick = health - poison * dt * 0.05
if health_after_tick < 0 then return function(world: types.World, dt: number)
world:remove(e, ct.Health) for e, poison_tick in world:query(ct.Poison, jecs.pair(jecs.ChildOf, jecs.w)) do
local tgt = world:target(e, jecs.ChildOf)
local health = world:get(tgt, ct.Health)
if not health then
continue continue
end end
world:set(e, ct.Health, health_after_tick)
if math.random() < 1 / 60 / 1 and health > 1 then
world:set(tgt, ct.Health, health - 1)
end
end end
end end

View file

@ -1,9 +1,12 @@
--!strict
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local types = require("../../ReplicatedStorage/types") local ct = require(ReplicatedStorage.components)
local ct = require("../../ReplicatedStorage/components") local components = ct :: { [string]: jecs.Entity }
local remotes = require(ReplicatedStorage.remotes)
local jecs = require(ReplicatedStorage.ecs) local jecs = require(ReplicatedStorage.ecs)
local remotes = require("../../ReplicatedStorage/remotes") local collect = require(ReplicatedStorage.collect)
local components = ct :: {[string]: jecs.Entity } local ty = require(ReplicatedStorage.types)
return function(world: ty.World) return function(world: ty.World)
@ -19,9 +22,10 @@ return function(world: ty.World)
local networked_pairs = {} local networked_pairs = {}
for component in world:each(ct.Networked) do for component in world:each(ct.Networked) do
local name = world:get(component, jecs.Name) :: string local name = assert(world:get(component, jecs.Name), "Invalid component")
if components[name] == nil then if components[name] == nil then
continue error("Invalid component:"..name)
end end
storages[component] = {} storages[component] = {}
@ -32,29 +36,30 @@ return function(world: ty.World)
for relation in world:each(ct.NetworkedPair) do for relation in world:each(ct.NetworkedPair) do
local name = world:get(relation, jecs.Name) :: string local name = world:get(relation, jecs.Name) :: string
if not components[name] then if not components[name] then
continue error("Invalid component")
end end
table.insert(networked_pairs, relation) table.insert(networked_pairs, relation)
end end
for _, component in networked_components do for _, component in networked_components do
local name = world:get(component, jecs.Name) :: string local name = world:get(component, jecs.Name)
if not components[name] then if not components[name] then
-- error("Invalid component")
error(`Networked Component (%id{component}%name{name})`) error(`Networked Component (%id{component}%name{name})`)
end end
local is_tag = jecs.is_tag(world, component) local is_tag = jecs.is_tag(world, component)
local storage = storages[component] local storage = storages[component]
if is_tag then if is_tag then
world:added(component, function(entity) world:added(component, function(entity)
storage[entity] = true storage[entity] = true
end) end)
else else
world:added(component, function(entity, _, value) world:added(component, function(entity, _, value)
storage[entity] = value storage[entity] = value
end) end)
world:changed(component, function(entity, _, value) world:changed(component, function(entity, _, value)
storage[entity] = value storage[entity] = value
end) end)
end end
world:removed(component, function(entity) world:removed(component, function(entity)
@ -64,51 +69,55 @@ return function(world: ty.World)
for _, relation in networked_pairs do for _, relation in networked_pairs do
world:added(relation, function(entity, id, value) world:added(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id) local is_tag = jecs.is_tag(world, id)
local storage = storages[id] local storage = storages[id]
if not storage then if not storage then
storage = {} storage = {}
storages[id] = storage storages[id] = storage
end end
if is_tag then if is_tag then
storage[entity] = true storage[entity] = true
else else
storage[entity] = value storage[entity] = value
end end
end) end)
world:changed(relation, function(entity, id, value) world:changed(relation, function(entity, id, value)
local is_tag = jecs.is_tag(world, id) local is_tag = jecs.is_tag(world, id)
if is_tag then if is_tag then
return return
end end
local storage = storages[id] local storage = storages[id]
if not storage then if not storage then
storage = {} storage = {}
storages[id] = storage storages[id] = storage
end end
storage[entity] = value storage[entity] = value
end) end)
world:removed(relation, function(entity, id) world:removed(relation, function(entity, id)
local storage = storages[id] local storage = storages[id]
if not storage then if not storage then
storage = {} storage = {}
storages[id] = storage storages[id] = storage
end end
storage[entity] = "jecs.Remove" storage[entity] = "jecs.Remove"
end) end)
end end
-- local requested_snapshots = collect(remotes.request_snapshot.OnServerEvent)
local players_added = collect(Players.PlayerAdded) local players_added = collect(Players.PlayerAdded)
return function() return function()
local snapshot_lazy: ty.Snapshot local snapshot_lazy: ty.Snapshot
local set_ids_lazy: { jecs.Entity } local set_ids_lazy: { jecs.Entity }
-- In the future maybe it should be requested by the player instead when they
-- are ready to receive the replication. Otherwise streaming could be complicated
-- with intances references being nil.
for player in players_added do for player in players_added do
if not snapshot_lazy then if not snapshot_lazy then
snapshot_lazy, set_ids_lazy = {}, {} snapshot_lazy, set_ids_lazy = {}, {}
@ -126,7 +135,7 @@ return function(world: ty.World)
if is_tag then if is_tag then
set_values = table.create(entities_len, true) set_values = table.create(entities_len, true)
else else
local column = archetype.columns[archetype.records[component]] local column = archetype.columns_map[component]
table.move(column, 1, entities_len, set_n + 1, set_values) table.move(column, 1, entities_len, set_n + 1, set_values)
end end
@ -135,10 +144,18 @@ return function(world: ty.World)
local set = table.move(set_ids_lazy, 1, set_n, 1, {}) local set = table.move(set_ids_lazy, 1, set_n, 1, {})
snapshot_lazy[tostring(component)] = { local ser_id: string = nil :: any
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil, if jecs.IS_PAIR(component) then
} ser_id = `{jecs.pair_first(world, component)},{jecs.pair_first(world, component)}`
else
ser_id = tostring(component)
end
snapshot_lazy[ser_id] = {
set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil,
}
end end
end end
@ -176,7 +193,15 @@ return function(world: ty.World)
if dirty then if dirty then
local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity } local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity }
local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity } local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity }
snapshot[tostring(component)] = { local ser_id: string = nil :: any
if jecs.IS_PAIR(component) then
ser_id = `{jecs.pair_first(world, component)},{jecs.pair_second(world, component)}`
else
ser_id = tostring(component)
end
snapshot[ser_id] = {
set = if set_n > 0 then set else nil, set = if set_n > 0 then set else nil,
values = if set_n > 0 then set_values else nil, values = if set_n > 0 then set_values else nil,
removed = if removed_n > 0 then removed else nil removed = if removed_n > 0 then removed else nil

View file

@ -2,7 +2,7 @@
Jecs. Just an Entity Component System. Jecs. Just an Entity Component System.
# Properties # Members
## World ## World
```luau ```luau
@ -12,27 +12,93 @@ A world is a container of all ECS data. Games can have multiple worlds but compo
## Wildcard ## Wildcard
```luau ```luau
jecs.Wildcard: Entity jecs.Wildcard: Id
``` ```
Builtin component type. This ID is used for wildcard queries. Builtin component type. This ID is used for wildcard queries.
## Component ## Component
```luau ```luau
jecs.Component: Entity jecs.Component: Id
``` ```
Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID. Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID.
## ChildOf ## ChildOf
```luau ```luau
jecs.ChildOf: Entity jecs.ChildOf: Id
``` ```
Builtin component type. This ID is for creating parent-child hierarchies. Builtin component type. This ID is for creating parent-child hierarchies.
## OnAdd
```luau
jecs.OnAdd: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is added.
## OnRemove
```luau
jecs.OnRemove: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is removed.
## OnChange
```luau
jecs.OnChange: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is changed.
## Exclusive
```lua
jecs.Exclusive: Id
```
Builtin component type. This ID is for encoding that an ID is Exclusive meaning that an entity can never have more than one target for that exclusive relation.
:::code-group
```luau [luau]
local ChildOf = world:entity()
world:add(ChildOf, jecs.Exclusive)
local pop = world:entity()
local dad = world:entity()
local kid = world:entity()
world:add(kid, pair(ChildOf, dad))
print(world:target(kid, ChildOf, 0) == dad)
world:add(kid, pair(ChildOf, pop))
print(world:target(kid, ChildOf, 1) == dad) -- If ChildOf was not exclusive this would have been true
print(world:target(kid, ChildOf, 0) == pop)
-- Output:
-- true
-- false
-- true
```
:::info
By default, jecs.ChildOf is already an exclusive relationship and this is just a demonstration of how to use it.
In some cases you can use Exclusive relationships as a performance optimization as you can guarantee there will only be one target, therefore
retrieving the data from a wildcard pair with that exclusive relationship can be deterministic.
:::
## Name
```luau
jecs.Name: Id
```
Builtin component type. This ID is for naming components, but realistically you could use any component to do that.
## Rest ## Rest
```luau ```luau
jecs.Rest: Entity jecs.Rest: Id
``` ```
Builtin component type. This ID is simply for denoting the end of the range for builtin component IDs.
# Functions # Functions
## pair() ## pair()
@ -48,3 +114,30 @@ function jecs.pair(
While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding. While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
::: :::
## pair_first()
```luau
function jecs.pair_first(
pair: Id, -- A full pair ID encoded using a relation-target pair.
): Entity -- The ID of the first element. Returns 0 if the ID is not alive.
```
Returns the first element (the relation part) of a pair ID.
**Example:**
```luau
local Likes = world:component()
local alice = world:entity()
local bob = world:entity()
local pair_id = pair(Likes, alice)
local relation = jecs.pair_first(pair_id)
print(relation == Likes) -- true
```
## pair_second()
```luau
function jecs.pair_second(
pair: Id, -- A full pair ID encoded using a relation-target pair.
): Entity -- The ID of the second element. Returns 0 if the ID is not alive.
```
Returns the second element (the target part) of a pair ID.

196
docs/api/observers.md Executable file
View file

@ -0,0 +1,196 @@
# Observers
The observers addon extends the World with signal-based reactivity and query-based observers. This addon provides a more ergonomic way to handle component lifecycle events and query changes.
## Installation
The observers addon is included with jecs and can be imported directly:
```luau
local jecs = require(path/to/jecs)
local observers_add = require(path/to/jecs/addons/observers)
local world = observers_add(jecs.world())
```
## Methods
### added
Registers a callback that is invoked when a component is added to any entity.
```luau
function World:added<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>, value: T?) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for additions
- `callback` - Function called when component is added, receives entity, component ID, and value
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:added(Health, function(entity, id, value)
print("Health component added to entity", entity, "with value", value)
end)
-- Later, to stop listening:
unsubscribe()
```
### removed
Registers a callback that is invoked when a component is removed from any entity.
```luau
function World:removed<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for removals
- `callback` - Function called when component is removed, receives entity and component ID
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:removed(Health, function(entity, id)
print("Health component removed from entity", entity)
end)
```
### changed
Registers a callback that is invoked when a component's value is changed on any entity.
```luau
function World:changed<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>, value: T) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for changes
- `callback` - Function called when component value changes, receives entity, component ID, and new value
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:changed(Health, function(entity, id, value)
print("Health changed to", value, "for entity", entity)
end)
```
### observer
Creates a query-based observer that triggers when entities match or stop matching a query.
```luau
function World:observer<T...>(
query: Query<T...>,
callback: ((entity: Entity, id: Id, value: any?) -> ())?
): () -> () -> Entity -- Returns an iterator function
```
**Parameters:**
- `query` - The query to observe for changes
- `callback` - Optional function called when entities match the query
**Returns:** An iterator function that returns entities that matched the query since last iteration
**Example:**
```luau
local Position = world:component() :: jecs.Id<Vector3>
local Velocity = world:component() :: jecs.Id<Vector3>
local moving_entities = world:observer(
world:query(Position, Velocity),
function(entity, id, value)
print("Entity", entity, "started moving")
end
)
-- In your game loop:
for entity in moving_entities() do
-- Process newly moving entities
end
```
### monitor
Creates a query-based monitor that triggers when entities are added to or removed from a query.
```luau
function World:monitor<T...>(
query: Query<T...>,
callback: ((entity: Entity, id: Id, value: any?) -> ())?
): () -> () -> Entity -- Returns an iterator function
```
**Parameters:**
- `query` - The query to monitor for additions/removals
- `callback` - Optional function called when entities are added or removed from the query
**Returns:** An iterator function that returns entities that were added or removed since last iteration
**Example:**
```luau
local Health = world:component() :: jecs.Id<number>
local health_changes = world:monitor(
world:query(Health),
function(entity, id, value)
print("Health component changed for entity", entity)
end
)
-- In your game loop:
for entity in health_changes() do
-- Process entities with health changes
end
```
## Usage Patterns
### Component Lifecycle Tracking
```luau
local Player = world:component()
local Health = world:component() :: jecs.Id<number>
-- Track when players are created
world:added(Player, function(entity, id, instance)
instance:SetAttribute("entityid", entity)
end)
world:removed(Player, function(entity, id)
world:add(entity, Destroy) -- process its deletion later!
end)
```
## Performance Considerations
- **Signal listeners** are called immediately when components are added/removed/changed
- **Query observers** cache the query for better performance
- **Multiple listeners** for the same component are supported and called in registration order
- **Unsubscribe functions** should be called when listeners are no longer needed to prevent memory leaks
- **Observer iterators** should be called regularly to clear the internal buffer
## Integration with Built-in Hooks
The observers addon integrates with the built-in component hooks (`OnAdd`, `OnRemove`, `OnChange`). If a component already has these hooks configured, the observers addon will preserve them and call both the original hook and any registered signal listeners.

View file

@ -4,13 +4,38 @@ A World contains entities which have components. The World is queryable and can
# Methods # Methods
## iter ## cached
Returns an iterator that can be used to iterate over the query. Returns a cached version of the query. This is useful if you want to create a query that you can iterate multiple times.
```luau ```luau
function Query:iter(): () -> (Entity, ...) function Query:cached(): Query -- Returns the cached Query
``` ```
Example:
::: code-group
```luau [luau]
local lerps = world:query(Lerp):cached() -- Ensure that you cache this outside a system so you do not create a new cache for a query every frame
local function system(dt)
for entity, lerp in lerps do
-- Do something
end
end
```
```ts [typescript]
const lerps = world.query(Lerp).cached()
function system(dt) {
for (const [entity, lerp] of lerps) {
// Do something
}
}
```
:::
## with ## with
@ -83,15 +108,13 @@ Example:
```luau [luau] ```luau [luau]
for i, archetype in world:query(Position, Velocity):archetypes() do for i, archetype in world:query(Position, Velocity):archetypes() do
local columns = archetype.columns local field = archetype.columns_map
local field = archetype.records local positions = field[Position]
local velocities = field[Velocity]
local P = field[Position]
local V = field[Velocity]
for row, entity in archetype.entities do for row, entity in archetype.entities do
local position = columns[P][row] local position = positions[row]
local velocity = columns[V][row] local velocity = velocities[row]
-- Do something -- Do something
end end
end end
@ -101,10 +124,27 @@ end
This function is meant for people who want to really customize their query behaviour at the archetype-level This function is meant for people who want to really customize their query behaviour at the archetype-level
::: :::
## cached ## iter
In most cases, you can iterate over queries directly using `for entity, ... in query do`. The `:iter()` method is mainly useful if you are on the old solver, to get types for the returned values.
Returns a cached version of the query. This is useful if you want to iterate over the same query multiple times.
```luau ```luau
function Query:cached(): Query -- Returns the cached Query function Query:iter(): () -> (Entity, ...)
```
Example:
::: code-group
```luau [luau]
local query = world:query(Position, Velocity)
-- Direct iteration (recommended)
for entity, position, velocity in query do
-- Process entity
end
-- Using explicit iterator (when needed for the old solver)
local iterator = query:iter()
for entity, position, velocity in iterator do
-- Process entity
end
``` ```

View file

@ -6,7 +6,7 @@ A World contains entities which have components. The World is queryable and can
## new ## new
`World` utilizes a class, meaning JECS allows you to create multiple worlds. `World` utilizes a class, meaning jecs allows you to create multiple worlds.
```luau ```luau
function World.new(): World function World.new(): World
@ -55,12 +55,12 @@ const entity = world.entity();
## component ## component
Creates a new component. Do note components are entities as well, meaning JECS allows you to add other components onto them. Creates a new component. Do note components are entities as well, meaning jecs allows you to add other components onto them.
These are meant to be added onto other entities through `add` and `set` These are meant to be added onto other entities through `add` and `set`
```luau ```luau
function World:component<T>(): Entity<T> -- The new componen. function World:component<T>(): Entity<T> -- The new component.
``` ```
Example: Example:
@ -241,7 +241,10 @@ print(world.get(Entity, Health));
// 100 // 100
// 50 // 50
``` ```
:::
:::info
`world:set(entity, component, value)` propagates that a change has happened for thes component on this entity, while mutating a value directly would not.
::: :::
## query ## query
@ -289,10 +292,52 @@ If the index is larger than the total number of instances the entity has for the
```luau ```luau
function World:target( function World:target(
entity: Entity, -- The entity entity: Entity, -- The entity
relation: Entity, -- The relationship between the entity and the target relation: Id, -- The relationship between the entity and the target
nth: number, -- The index nth: number, -- The index
): Entity? -- The target for the relationship at the specified index. ): Id? -- The target for the relationship at the specified index.
``` ```
Example:
::: code-group
```luau [luau]
local function timers_count(world: types.World)
local timers = world
:query(jecs.pair(ct.Timer, jecs.w))
:without(ct.Destroy)
:cached()
return function(_, dt: number)
for entity in timers do
local index = 0
local nth = world:target(entity, ct.Timer, index)
while nth do
local timer = world:get(entity, jecs.pair(ct.Timer, nth))
local elapsed = timer.elapsed + dt
if elapsed >= timer.duration then
world:add(entity, ct.Destroy)
end
timer.elapsed = elapsed
end
end
end
end
```
```ts [typescript]
const entity = world.entity();
print(world.contains(entity));
print(world.contains(1));
print(world.contains(2));
// Outputs:
// true
// true
// false
```
:::
## parent ## parent
@ -355,9 +400,9 @@ print(world.contains(2));
Removes a component (ID) from an entity Removes a component (ID) from an entity
```luau ```luau
function World:remove( function World:remove<T>(
entity: Entity, entity: Entity,
component: Entity<T> component: Id<T>
): void ): void
``` ```
@ -458,20 +503,20 @@ Useful when you only need the entity for a specific ID and you want to avoid cre
```luau ```luau
function World:each( function World:each(
id: Entity -- The component ID component: Id -- The component ID
): () -> Entity ): () -> Entity
``` ```
Example: Example:
::: code-group ::: code-group
```luau [luau] ```luau [luau]
local id = world:entity() local id = world:component()
for entity in world:each(id) do for entity in world:each(id) do
-- Do something -- Do something
end end
``` ```
```ts [typescript] ```ts [typescript]
const id = world.entity(); const id = world.component();
for (const entity of world.each(id)) { for (const entity of world.each(id)) {
// Do something // Do something
} }
@ -500,6 +545,122 @@ Enforces a check for entities to be created within a desired range.
```luau ```luau
function World:range( function World:range(
range_begin: number -- The starting point, range_begin: number -- The starting point,
range_begin: number? -- The end point (optional) range_end: number? -- The end point (optional)
) )
``` ```
Example:
::: code-group
```luau [luau]
world:range(1000, 5000) -- Entities will be created with IDs 1000-5000
local entity = world:entity()
print(entity) -- Will be >= 1000 and < 5000
```
```ts [typescript]
world.range(1000, 5000) // Entities will be created with IDs 1000-5000
const entity = world.entity()
print(entity) // Will be >= 1000 and < 5000
```
:::
## parent
Gets the parent entity of the specified entity using the built-in `ChildOf` relationship.
```luau
function World:parent(
entity: Entity
): Entity? -- Returns the parent entity or nil if no parent
```
Example:
::: code-group
```luau [luau]
local parent = world:entity()
local child = world:entity()
world:add(child, pair(jecs.ChildOf, parent))
local retrieved_parent = world:parent(child)
print(retrieved_parent == parent) -- true
```
```ts [typescript]
const parent = world.entity()
const child = world.entity()
world.add(child, pair(jecs.ChildOf, parent))
const retrievedParent = world.parent(child)
print(retrievedParent === parent) // true
```
:::
## contains
Checks if an entity exists and is alive in the world.
```luau
function World:contains(
entity: Entity
): boolean
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
print(world:contains(entity)) -- true
world:delete(entity)
print(world:contains(entity)) -- false
```
```ts [typescript]
const entity = world.entity()
print(world.contains(entity)) // true
world.delete(entity)
print(world.contains(entity)) // false
```
:::
## exists
Checks if the entity ID exists regardless of whether it is alive or not. Useful to know if the ID is occupied in the entity index.
```luau
function World:exists(
entity: Entity
): boolean
```
## cleanup
Cleans up deleted entities and their associated data. This is automatically called by jecs, but can be called manually if needed.
```luau
function World:cleanup(): void
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
world:delete(entity)
-- Cleanup is usually automatic, but can be called manually
world:cleanup()
```
```ts [typescript]
const entity = world.entity()
world.delete(entity)
// Cleanup is usually automatic, but can be called manually
world.cleanup()
```
:::

View file

@ -27,8 +27,8 @@ local jecs = require(path/to/jecs)
local world = jecs.world() local world = jecs.world()
``` ```
```typescript [typescript] ```typescript [typescript]
import { World } from "@rbxts/jecs" import * as jecs from "@rbxts/jecs"
const world = new World() const world = jecs.world()
// creates a new entity with no components and returns its identifier // creates a new entity with no components and returns its identifier
const entity = world.entity() const entity = world.entity()
@ -156,6 +156,13 @@ world.set(Transform, OnChange, (entity, id, data) => {
``` ```
::: :::
:::info
Children are cleaned up before parents
When a parent and its children are deleted, OnRemove hooks will be invoked for children first, under the condition that there are no cycles in the relationship graph of the deleted entities. This order is maintained for any relationship that has the (OnDeleteTarget, Delete) trait (see Component Traits for more details).
When an entity graph contains cycles, order is undefined. This includes cycles that can be formed using different relationships.
:::
### Cleanup Traits ### Cleanup Traits
When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references. When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references.
@ -166,15 +173,22 @@ This is what cleanup traits are for: to specify which action needs to be execute
To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed. To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed.
There are two cleanup actions: #### Cleanup Traits Summary
- `Remove`: removes instances of the specified (component) id from all entities (default) | Condition | Action | Description | Use Case |
- `Delete`: deletes all entities with specified id |-----------|--------|-------------|----------|
| `OnDelete` | `Remove` | Removes the component from all entities when the component is deleted | Default behavior, safe cleanup |
| `OnDelete` | `Delete` | Deletes all entities that have the component when the component is deleted | Cascading deletion, dangerous |
| `OnDeleteTarget` | `Remove` | Removes the relationship from all entities when the target is deleted | Safe relationship cleanup |
| `OnDeleteTarget` | `Delete` | Deletes all entities that have the relationship when the target is deleted | Hierarchical deletion (e.g., parent-child) |
There are two cleanup conditions: **Cleanup Actions:**
- `Remove`: removes instances of the specified (component) id from all entities (default)
- `Delete`: deletes all entities with specified id
- `OnDelete`: the component, tag or relationship is deleted **Cleanup Conditions:**
- `OnDeleteTarget`: a target used with the relationship is deleted - `OnDelete`: the component, tag or relationship is deleted
- `OnDeleteTarget`: a target used with the relationship is deleted
#### (OnDelete, Remove) #### (OnDelete, Remove)
::: code-group ::: code-group
@ -285,9 +299,10 @@ jecs.world() -- Position gets registered here
``` ```
```typescript [typescript] ```typescript [typescript]
import { world } from "@rbxts/jecs"
const Position = jecs.component<Vector3>(); const Position = jecs.component<Vector3>();
new World() // Position gets registered here world() // Position gets registered here
``` ```
::: :::
@ -301,9 +316,11 @@ jecs.world() -- Position gets registered here with its name "Position"
``` ```
```typescript [typescript] ```typescript [typescript]
import { world } from "@rbxts/jecs"
jecs.meta(Position, jecs.Name, "Position") jecs.meta(Position, jecs.Name, "Position")
new World() // Position gets registered here with its name "Position" world() // Position gets registered here with its name "Position"
``` ```
::: :::
@ -632,7 +649,7 @@ world:set(e, pair(Eats, Apples), { amount = 1 })
world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0)) world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0))
world:set(e, pair(End, Position), Vector3.new(10, 20, 30)) world:set(e, pair(End, Position), Vector3.new(10, 20, 30))
world:add(e, jecs.ChildOf, Position) world:add(e, pair(jecs.ChildOf, Position))
``` ```
```typescript [typescript] ```typescript [typescript]
@ -648,7 +665,7 @@ world.set(e, pair(Eats, Apples), { amount: 1 })
world.set(e, pair(Begin, Position), new Vector3(0, 0, 0)) world.set(e, pair(Begin, Position), new Vector3(0, 0, 0))
world.set(e, pair(End, Position), new Vector3(10, 20, 30)) world.set(e, pair(End, Position), new Vector3(10, 20, 30))
world.add(e, jecs.ChildOf, Position) world.add(e, pair(jecs.ChildOf, Position))
``` ```
::: :::
@ -695,3 +712,7 @@ To improve the speed of evaluating queries, Jecs has indices that store all arch
While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships. While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships.
This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html). This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html).
## Next Steps
- [API Reference](../api/jecs.md) - Complete API documentation

View file

@ -49,8 +49,8 @@ A tool for inspecting entity lifetimes
### Helpers ### Helpers
#### [jecs_observers](https://github.com/Ukendio/jecs/blob/main/addons/observers.luau) #### [jecs_ob](https://github.com/Ukendio/jecs/blob/main/addons/ob.luau)
Observers for queries and signals for components Observers & Monitors for queries
### [hammer](https://github.com/Mark-Marks/hammer) ### [hammer](https://github.com/Mark-Marks/hammer)
A set of utilities for Jecs A set of utilities for Jecs

19
jecs.d.ts vendored
View file

@ -49,7 +49,7 @@ export type Archetype<T extends unknown[]> = {
type: string; type: string;
entities: number[]; entities: number[];
columns: Column<unknown>[]; columns: Column<unknown>[];
columns_map: { [K in keyof T]: Column<T[K]> } columns_map: Record<Id, Column<T[number]>>
}; };
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>; type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
@ -105,7 +105,7 @@ export class World {
/** /**
* Creates a new World. * Creates a new World.
*/ */
constructor(); private constructor();
/** /**
* Enforces a check for entities to be created within a desired range. * Enforces a check for entities to be created within a desired range.
@ -156,8 +156,8 @@ export class World {
* @param hook The hook to install. * @param hook The hook to install.
* @param value The hook callback. * @param value The hook callback.
*/ */
set<T>(component: Entity<T>, hook: StatefulHook, value: (e: Entity<T>, id: Id<T>, data: T) => void): void; set<T>(component: Entity<T>, hook: StatefulHook, value: (e: Entity, id: Id<T>, data: T) => void): void;
set<T>(component: Entity<T>, hook: StatelessHook, value: (e: Entity<T>, id: Id<T>) => void): void; set<T>(component: Entity<T>, hook: StatelessHook, value: (e: Entity, id: Id<T>) => void): void;
/** /**
* Assigns a value to a component on the given entity. * Assigns a value to a component on the given entity.
* @param entity The target entity. * @param entity The target entity.
@ -247,8 +247,14 @@ export class World {
* @returns A Query object to iterate over results. * @returns A Query object to iterate over results.
*/ */
query<T extends Id[]>(...components: T): Query<InferComponents<T>>; query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
added<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void
changed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void
removed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>) => void): () => void
} }
export function world(): World;
export function component<T>(): Entity<T>; export function component<T>(): Entity<T>;
export function tag(): Tag; export function tag(): Tag;
@ -301,6 +307,7 @@ export declare const OnAdd: StatefulHook;
export declare const OnRemove: StatelessHook; export declare const OnRemove: StatelessHook;
export declare const OnChange: StatefulHook; export declare const OnChange: StatefulHook;
export declare const ChildOf: Tag; export declare const ChildOf: Tag;
export declare const Component: Tag;
export declare const Wildcard: Entity; export declare const Wildcard: Entity;
export declare const w: Entity; export declare const w: Entity;
export declare const OnDelete: Tag; export declare const OnDelete: Tag;
@ -308,6 +315,7 @@ export declare const OnDeleteTarget: Tag;
export declare const Delete: Tag; export declare const Delete: Tag;
export declare const Remove: Tag; export declare const Remove: Tag;
export declare const Name: Entity<string>; export declare const Name: Entity<string>;
export declare const Exclusive: Tag;
export declare const Rest: Entity; export declare const Rest: Entity;
export type ComponentRecord = { export type ComponentRecord = {
@ -317,3 +325,6 @@ export type ComponentRecord = {
} }
export function component_record(world: World, id: Id): ComponentRecord export function component_record(world: World, id: Id): ComponentRecord
export function bulk_insert<const C extends Id[]>(world: World, entity: Entity, ids: C, values: InferComponents<C>): void
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void

1452
jecs.luau

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "@rbxts/jecs", "name": "@rbxts/jecs",
"version": "0.7.2", "version": "0.9.0-rc.8",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "repository": {

View file

@ -2,43 +2,28 @@ local jecs = require("@jecs")
local testkit = require("@testkit") local testkit = require("@testkit")
local test = testkit.test() local test = testkit.test()
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local observers_add = require("@addons/observers") local FOCUS = test.FOCUS
local ob = require("@addons/ob")
TEST("addons/observers", function() TEST("addons/observers", function()
local world = observers_add(jecs.world())
do CASE "Should work even if set after the component has been used" local world = jecs.world()
do CASE "monitors should accept pairs"
local A = world:component() local A = world:component()
local B = world:component()
world:set(world:entity(), A, 2) local c = 1
local ran = false ob.monitor(world:query(jecs.pair(A, B)), function (_, event)
world:added(A, function() c += 1
ran = true
end) end)
local entity = world:entity() local child = world:entity()
world:set(entity, A, 3) world:add(child, jecs.pair(A, B))
CHECK(c == 2)
CHECK(ran) world:remove(child, jecs.pair(A, B))
CHECK(c == 3)
end end
do CASE "Should not override hook"
local A = world:component()
local count = 1
local function counter()
count += 1
end
world:set(A, jecs.OnAdd, counter)
world:added(A, counter)
world:set(world:entity(), A, false)
CHECK(count == (1 + 2))
world:set(world:entity(), A, false)
CHECK(count == (1 + (2 * 2)))
end
do CASE "Ensure ordering between signals and observers" do CASE "Ensure ordering between signals and observers"
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -48,11 +33,15 @@ TEST("addons/observers", function()
count += 1 count += 1
end end
world:observer(world:query(A, B), counter) ob.observer(world:query(A, B), counter)
world:added(A, counter) world:added(A, counter)
world:added(A, counter) world:added(A, counter)
for _ in world:query(A) do
end
local e = world:entity() local e = world:entity()
world:add(e, A) world:add(e, A)
CHECK(count == 3) CHECK(count == 3)
@ -69,7 +58,7 @@ TEST("addons/observers", function()
count += 1 count += 1
end end
world:observer(world:query(A), counter) ob.observer(world:query(A), counter)
local e = world:entity() local e = world:entity()
world:set(e, A, false) world:set(e, A, false)
@ -89,7 +78,7 @@ TEST("addons/observers", function()
count += 1 count += 1
end end
world:monitor(world:query(A), counter) ob.monitor(world:query(A), counter)
local e = world:entity() local e = world:entity()
world:set(e, A, false) world:set(e, A, false)

View file

@ -22,9 +22,185 @@ type Entity<T=nil> = jecs.Entity<T>
type Id<T=unknown> = jecs.Id<T> type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser") local entity_visualiser = require("@tools/entity_visualiser")
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local dwi = entity_visualiser.stringify local dwi = entity_visualiser.stringify
TEST("ardi", function()
local world = jecs.world()
local r = world:entity()
world:add(r, jecs.pair(jecs.OnDelete, jecs.Delete))
local e = world:entity()
local e1 = world:entity()
world:add(e, jecs.pair(r, e1))
world:delete(r)
CHECK(not world:contains(e))
end)
TEST("dai", function()
local world = jecs.world()
local C = world:component()
world:set(C, jecs.Name, "C")
CHECK(world:get(C, jecs.Name) == "C")
world:entity(2000)
CHECK(world:get(C, jecs.Name) == "C")
end)
TEST("another axen banger", function()
-- taken from jecs.luau
local world = jecs.world()
world:range(2000, 3000)
local e0v1_id = jecs.ECS_COMBINE(1000, 1) -- id can be both within or outside the world's range
local e0v1 = world:entity(e0v1_id)
assert(world:contains(e0v1)) -- fails
end)
TEST("Ensure archetype edges get cleaned", function()
local A = jecs.component()
local B = jecs.component()
local world = jecs.world()
local edges = world.archetype_edges
local e = world:entity()
local r = jecs.record(world, e)
world:set(e, A, true)
world:add(e, A)
local arch_a = r.archetype
world:set(e, B, true)
world:add(e, B)
local arch_ab = r.archetype
CHECK(edges[arch_a.id][B] == arch_ab)
CHECK(edges[arch_ab.id][B] == arch_a)
world:delete(B)
CHECK(edges[arch_a.id][B] == nil)
CHECK(edges[arch_ab.id][A] == nil)
for _ in edges[arch_ab.id] do
CHECK(false)
end
world:delete(A)
CHECK(edges[arch_a.id][B] == nil)
CHECK(edges[arch_a.id][A] == nil)
for _ in edges[arch_a.id] do
CHECK(false)
end
end)
TEST("repeated entity cached query", function()
local pair = jecs.pair
local world = jecs.world()
local rel = world:entity()
local cmp = world:component()
local query = world:query(cmp):cached()
local t1 = world:entity()
local p1 = pair(rel, t1)
local e1 = world:entity()
world:add(e1, p1)
world:set(e1, cmp, true)
CHECK(query:iter()() == e1)
world:delete(e1)
world:delete(t1)
local t2 = world:entity()
local p2 = pair(rel, t2)
local e2 = world:entity()
world:add(e2, p2)
world:set(e2, cmp, true)
CHECK(query:iter()() == e2) -- Fails
end)
TEST("repeated pairs", function()
local pair = jecs.pair
local world = jecs.world()
local rel = world:component() -- Does not error if this is just a tag
-- Does not happen if we delete manually instead of using this
world:add(rel, pair(jecs.OnDeleteTarget, jecs.Delete))
local t1 = world:entity()
local p1 = pair(rel, t1)
local e1 = world:entity()
world:set(e1, p1, true)
CHECK(world:get(e1, p1))
CHECK(world:each(p1)() == e1)
world:delete(t1)
local t2 = world:entity()
local p2 = pair(rel, t2)
local e2 = world:entity()
print("-----")
world:set(e2, p2, true)
CHECK(world:get(e2, p2))
CHECK(p1 == p2)
local count = 0
CHECK(world:has(e2, p2))
for _ in world:query(p2) do
count += 1
end
CHECK(count == 1)
CHECK(world:each(p2)() == e2) -- Fails
end)
TEST("repro", function()
local world = jecs.world()
local data = world:component()
local relation = world:component()
world:add(relation, jecs.pair(jecs.OnDeleteTarget, jecs.Delete))
local e1 = world:entity()
local e2 = world:entity()
world:set(e2, data, 456)
world:add(e2, jecs.pair(relation, e1))
world:delete(e1)
local e1v1 = world:entity()
CHECK(ECS_ID(e1v1) == e1::any)
local e2v1 = world:entity()
CHECK(ECS_ID(e2v1) == e2::any)
world:set(e2v1, data, 456)
CHECK(world:contains(e1v1))
CHECK(not world:contains(e2))
CHECK(world:contains(e2v1))
local count = 0
for i,val in world:query(data):iter() do
count += 1
end
CHECK(count == 1)
count = 0
print("----")
world:add(e2v1, jecs.pair(relation, e1v1))
CHECK(world:has(e2v1, jecs.pair(relation, e1v1)))
for i,val in world:query(data):iter() do
count += 1
end
print(count)
CHECK(count==1)
end)
TEST("bulk", function() TEST("bulk", function()
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
@ -42,7 +218,10 @@ TEST("bulk", function()
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 3) CHECK(world:get(e, C) == 3)
jecs.bulk_insert(world, e, { D, E, F }, { 4, nil, 5 }) jecs.bulk_insert(world, e,
{ D, E, F },
{ 4, nil, 5 }
)
CHECK(world:get(e, A) == 1) CHECK(world:get(e, A) == 1)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 3) CHECK(world:get(e, C) == 3)
@ -51,7 +230,10 @@ TEST("bulk", function()
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 5) CHECK(world:get(e, F) == 5)
jecs.bulk_insert(world, e, { A, D, E, F, C }, { 10, 40, nil, 50, 30 }) jecs.bulk_insert(world, e,
{ A, D, E, F, C },
{ 10, 40, nil, 50, 30 }
)
CHECK(world:get(e, A) == 10) CHECK(world:get(e, A) == 10)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
@ -137,6 +319,66 @@ TEST("repro", function()
end) end)
TEST("world:add()", function() TEST("world:add()", function()
do CASE "exclusive relations"
local world = jecs.world()
local A = world:component()
world:add(A, jecs.Exclusive)
local B = world:component()
local C = world:component()
local e = world:entity()
world:add(e, pair(A, B))
world:add(e, pair(A, C))
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
end
do CASE "exclusive relations invoke hooks"
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
local e_ptr: jecs.Entity = (jecs.Rest :: any) + 1
world:add(A, jecs.Exclusive)
local on_remove_call = false
world:set(A, jecs.OnRemove, function(e, id)
on_remove_call = true
end)
local on_add_call_count = 0
world:set(A, jecs.OnAdd, function(e, id)
on_add_call_count += 1
end)
local e = world:entity()
CHECK(e == e_ptr)
world:add(e, pair(A, B))
CHECK(on_add_call_count == 1)
world:add(e, pair(A, C))
CHECK(on_add_call_count == 2)
CHECK(on_remove_call)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
-- We have to ensure that it actually invokes hooks everytime it
-- traverses the archetype
e = world:entity()
world:add(e, pair(A, B))
CHECK(on_add_call_count == 3)
world:add(e, pair(A, C))
CHECK(on_add_call_count == 4)
CHECK(on_remove_call)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
end
do CASE "idempotent" do CASE "idempotent"
local world = jecs.world() local world = jecs.world()
local d = dwi(world) local d = dwi(world)
@ -188,6 +430,9 @@ TEST("world:children()", function()
local e3 = world:entity() local e3 = world:entity()
world:add(e3, pair(ChildOf, e1)) world:add(e3, pair(ChildOf, e1))
CHECK(world:has(e2, pair(ChildOf, e1)))
CHECK(world:has(e3, pair(ChildOf, e1)))
local count = 0 local count = 0
for entity in world:children(e1) do for entity in world:children(e1) do
count += 1 count += 1
@ -376,8 +621,51 @@ TEST("world:contains()", function()
end) end)
TEST("world:delete()", function() TEST("world:delete()", function()
do CASE "pair(OnDelete, Delete)"
local world = jecs.world()
local ct = world:component()
world:add(ct, jecs.pair(jecs.OnDelete, jecs.Delete))
local e1 = world:entity()
local e2 = world:entity()
local dummy = world:entity()
world:add(e1, ct)
world:add(e2, jecs.pair(ct, dummy))
-- world:delete(dummy)
-- CHECK(world:contains(e2))
world:delete(ct)
CHECK(not world:contains(e1))
end
do CASE "pair(OnDeleteTarget, Delete)"
local world = jecs.world()
local ct = world:component()
world:add(ct, jecs.pair(jecs.OnDeleteTarget, jecs.Delete))
local e1 = world:entity()
local e2 = world:entity()
local dummy = world:entity()
world:add(e1, ct)
world:add(e2, jecs.pair(ct, dummy))
world:delete(dummy)
CHECK(not world:contains(e2))
world:delete(ct)
CHECK(world:contains(e1))
end
do CASE "remove (*, R) pairs when relationship is invalidated" do CASE "remove (*, R) pairs when relationship is invalidated"
print("-------")
local world = jecs.world() local world = jecs.world()
local e1 = world:entity() local e1 = world:entity()
local e2 = world:entity() local e2 = world:entity()
@ -441,7 +729,7 @@ TEST("world:delete()", function()
local A = world:entity() local A = world:entity()
local B = world:entity() local B = world:entity()
world:add(Relation, pair(jecs.OnDelete, jecs.Delete)) world:add(Relation, pair(jecs.OnDeleteTarget, jecs.Delete))
local entity = world:entity() local entity = world:entity()
@ -548,7 +836,6 @@ TEST("world:delete()", function()
CHECK(not world:has(id1, Health)) CHECK(not world:has(id1, Health))
end end
do CASE "delete children" do CASE "delete children"
local world = jecs.world() local world = jecs.world()
@ -735,6 +1022,16 @@ TEST("world:delete()", function()
CHECK(not world:contains(bob)) CHECK(not world:contains(bob))
CHECK(not world:contains(alice)) CHECK(not world:contains(alice))
end end
do CASE "deleted entity should not be able to be operated on"
local world = jecs.world()
local e = world:entity()
local A = world:component()
world:set(e, A, true)
world:delete(e)
world:set(e, A, true)
CHECK(world:has(e, A) == false)
end
end) end)
TEST("world:each()", function() TEST("world:each()", function()
@ -765,19 +1062,180 @@ TEST("world:each()", function()
end end
end) end)
TEST("world:added", function()
local world = jecs.world()
do CASE "Should work even if set after the component has been used"
local A = world:component()
world:set(world:entity(), A, 2)
local ran = false
world:added(A, function()
ran = true
end)
local entity = world:entity()
world:set(entity, A, 3)
CHECK(ran)
end
do CASE "Should work even if set after the pair has been used"
local A = world:component()
local B = world:component()
world:set(world:entity(), A, 2)
world:set(world:entity(), pair(A, B), 2)
world:added(A, function()
ran = true
end)
local entity = world:entity()
world:set(entity, pair(A, B), 3)
CHECK(ran)
end
do CASE "Should allow setting signal after Relation has been used as a component"
local A = world:component()
local B = world:component()
world:add(world:entity(), A)
world:added(A, function()
ran = true
end)
world:add(world:entity(), pair(A, B))
CHECK(ran)
end
do CASE "Should invoke signal for the Relation being set as a key despite a pair with Relation having been cached"
local A = world:component()
local B = world:component()
world:add(world:entity(), pair(A, B))
world:added(A, function()
ran = true
end)
world:add(world:entity(), A)
CHECK(ran)
end
do CASE "Should not override hook"
local A = world:component()
local count = 1
local function counter()
count += 1
end
world:set(A, jecs.OnAdd, counter)
world:added(A, counter)
world:set(world:entity(), A, false)
CHECK(count == (1 + 2))
world:set(world:entity(), A, false)
CHECK(count == (1 + (2 * 2)))
end
end)
TEST("world:range()", function() TEST("world:range()", function()
do CASE "under range start"
do CASE "spawn entity under min range"
local world = jecs.world() local world = jecs.world()
world:range(400, 1000) world:range(400, 1000)
local id = world:entity() :: number CHECK(world.entity_index.alive_count == 399)
local e = world:entity(id + 5) local e = world:entity(300)
CHECK(e == id + 5) CHECK(world.entity_index.alive_count == 400)
local e1 = world:entity(300)
CHECK(world.entity_index.alive_count == 400)
CHECK(e)
end
do CASE "axen"
local base = jecs.world()
base:range(1_000, 2_000)
local mirror = jecs.world()
mirror:range(3_000, 4_000)
mirror:entity() -- NOTE: this fixes the "attempt to index nil with 'dense'" error
local foo = base:entity()
local bar = mirror:entity(foo)
base:delete(base:entity()) -- Removing this line stops the error below from happening
local meow = base:entity()
mirror:delete(bar)
CHECK(jecs.ECS_ID(foo))
CHECK(jecs.ECS_ID(meow))
local mrrp = mirror:entity(meow) -- jecs, Line 785 - Entity ID is already in use with a different generation
CHECK(mrrp == meow)
end
do CASE "axen2"
local world = jecs.world()
local mirror = jecs.world()
world:range(1000, 2000)
mirror:range(3000, 4000)
local foo = world:entity() -- 1000
local foo_mirror = mirror:entity(foo) -- 1000
CHECK(foo == foo_mirror)
for index = 1, 5 do
world:entity()
end
world:delete(foo)
mirror:delete(foo_mirror)
local bar = world:entity()
local bar_mirror = mirror:entity(bar)
CHECK(bar == bar_mirror)
end
do CASE "delete outside partitioned range"
local server = jecs.world()
local client = jecs.world()
server:range(0, 1000)
client:range(1000, 5000)
local e1 = server:entity()
CHECK((e1::any)< 1000)
server:delete(e1)
local e2 = client:entity(e1)
CHECK(e2 == e1)
local A = client:component()
client:set(e2, A, true)
CHECK(client:get(e2, A))
client:delete(e2)
local e3 = client:entity()
CHECK(ECS_ID(e3) == 1000)
local e1v1 = server:entity()
local e4 = client:entity(e1v1)
CHECK(ECS_ID(e4) == e1::any)
CHECK(ECS_GENERATION(e4) == 1)
CHECK(not client:contains(e2))
CHECK(client:contains(e4))
end
do CASE "under range start"
local world = jecs.world()
world:range(400, 1000)
local id = world:entity()
local e = world:entity(id::any + 5)
CHECK(e::any == (id::any) + 5)
CHECK(world:contains(e)) CHECK(world:contains(e))
local e2 = world:entity(399) local e2 = world:entity(399)
CHECK(world:contains(e2)) CHECK(world:contains(e2))
world:delete(e2) world:delete(e2)
CHECK(not world:contains(e2)) CHECK(not world:contains(e2))
local e2v1 = world:entity(399) :: number local e2v1 = world:entity(399)
CHECK(world:contains(e2v1)) CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 399) CHECK(ECS_ID(e2v1) == 399)
CHECK(ECS_GENERATION(e2v1) == 0) CHECK(ECS_GENERATION(e2v1) == 0)
@ -790,13 +1248,13 @@ TEST("world:range()", function()
CHECK(world:contains(e2)) CHECK(world:contains(e2))
world:delete(e2) world:delete(e2)
CHECK(not world:contains(e2)) CHECK(not world:contains(e2))
local e2v1 = world:entity(405) :: number local e2v1 = world:entity(405)
CHECK(world:contains(e2v1)) CHECK(world:contains(e2v1))
CHECK(ECS_ID(e2v1) == 405) CHECK(ECS_ID(e2v1) == 405)
CHECK(ECS_GENERATION(e2v1) == 0) CHECK(ECS_GENERATION(e2v1) == 0)
world:delete(e2v1) world:delete(e2v1)
local e2v2 = world:entity(e2v1) :: number local e2v2 = world:entity(e2v1)
CHECK(ECS_ID(e2v2) == 405) CHECK(ECS_ID(e2v2) == 405)
CHECK(ECS_GENERATION(e2v2) == 0) CHECK(ECS_GENERATION(e2v2) == 0)
end end
@ -805,9 +1263,10 @@ end)
TEST("world:entity()", function() TEST("world:entity()", function()
do CASE "desired id" do CASE "desired id"
local world = jecs.world() local world = jecs.world()
local id = world:entity() :: number local id = world:entity()
local e = world:entity(id + 5) local offset: jecs.Entity = (id ::any) + 5
CHECK(e == id + 5) local e = world:entity(offset)
CHECK(e == offset)
CHECK(world:contains(e)) CHECK(world:contains(e))
local e2 = world:entity(399) local e2 = world:entity(399)
CHECK(world:contains(e2)) CHECK(world:contains(e2))
@ -825,7 +1284,7 @@ TEST("world:entity()", function()
end end
do CASE "generations" do CASE "generations"
local world = jecs.world() local world = jecs.world()
local e = world:entity() :: any local e = world:entity()
CHECK(ECS_ID(e) == 1 + jecs.Rest :: any) CHECK(ECS_ID(e) == 1 + jecs.Rest :: any)
CHECK(ECS_GENERATION(e) == 0) -- 0 CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e) e = ECS_GENERATION_INC(e)
@ -875,11 +1334,11 @@ TEST("world:entity()", function()
local e = world:entity() local e = world:entity()
world:delete(e) world:delete(e)
end end
local e = world:entity() :: number local e = world:entity()
CHECK(ECS_ID(e) == pin) CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 2^16-1) CHECK(ECS_GENERATION(e) == 2^16-1)
world:delete(e) world:delete(e)
e = world:entity() :: number e = world:entity()
CHECK(ECS_ID(e) == pin) CHECK(ECS_ID(e) == pin)
CHECK(ECS_GENERATION(e) == 0) CHECK(ECS_GENERATION(e) == 0)
end end
@ -916,6 +1375,7 @@ end)
TEST("world:query()", function() TEST("world:query()", function()
local N = 2^8 local N = 2^8
do CASE "cached" do CASE "cached"
local world = jecs.world() local world = jecs.world()
local Foo = world:component() local Foo = world:component()
@ -952,6 +1412,26 @@ TEST("world:query()", function()
world:delete(Foo) world:delete(Foo)
CHECK(#q:archetypes() == 0) CHECK(#q:archetypes() == 0)
end end
do CASE "3 components"
local world = jecs.world()
local A = world:component() :: jecs.Entity<boolean>
local B = world:component() :: jecs.Entity<boolean>
local C = world:component() :: jecs.Entity<boolean>
local e = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e, C, true)
local q = world:query(A, B, C):cached()
local counter = 0
for x, a, b, c in q:iter() do
counter += 1
CHECK(a)
CHECK(b)
CHECK(c)
end
CHECK(counter == 1)
end
do CASE "multiple iter" do CASE "multiple iter"
local world = jecs.world() local world = jecs.world()
local A = world:component() :: jecs.Entity<string> local A = world:component() :: jecs.Entity<string>
@ -1133,6 +1613,7 @@ TEST("world:query()", function()
for i = 1, 9 do for i = 1, 9 do
local id = world:component() local id = world:component()
world:component() -- make the components sparsely interleaved
components[i] = id components[i] = id
end end
local e1 = world:entity() local e1 = world:entity()
@ -1376,7 +1857,7 @@ TEST("world:query()", function()
world:add(e2, B) world:add(e2, B)
local count = 0 local count = 0
for id in world:query(A) :: any do for id in world:query(A) do
world:clear(id) world:clear(id)
count += 1 count += 1
end end
@ -1621,7 +2102,9 @@ end)
TEST("#repro2", function() TEST("#repro2", function()
local world = jecs.world() local world = jecs.world()
local Lifetime = world:component() :: Id<number> local Lifetime = world:component() :: Id<number>
world:set(Lifetime, jecs.Name, "Lifetime")
local Particle = world:entity() local Particle = world:entity()
world:set(Particle, jecs.Name, "Particle")
local Beam = world:entity() local Beam = world:entity()
local entity = world:entity() local entity = world:entity()
@ -1629,19 +2112,24 @@ TEST("#repro2", function()
world:set(entity, pair(Lifetime, Beam), 2) world:set(entity, pair(Lifetime, Beam), 2)
world:set(entity, pair(4 :: any, 5 :: any), 6) -- noise world:set(entity, pair(4 :: any, 5 :: any), 6) -- noise
CHECK(world:get(entity, pair(Lifetime, Particle)) == 1)
CHECK(world:get(entity, pair(Lifetime, Beam)) == 2)
CHECK(world:target(entity, Lifetime, 0) == Particle)
CHECK(world:target(entity, Lifetime, 1) == Beam)
-- entity_visualizer.components(world, entity) -- entity_visualizer.components(world, entity)
-- print(CHECK(world:has(jecs.ChildOf, jecs.Exclusive)))
for e in world:each(pair(Lifetime, __)) do for e in world:each(pair(Lifetime, __)) do
local i = 0 local i = 0
local nth = world:target(e, Lifetime, i) local nth = world:target(e, Lifetime, i)
while nth do while nth do
-- entity_visualizer.components(world, e) -- entity_visualizer.components(world, e)
local data = world:get(e, pair(Lifetime, nth)) :: number local data = world:get(e, pair(Lifetime, nth)) :: number
data -= 1 if data > 0 then
if data <= 0 then data -= 1
world:remove(e, pair(Lifetime, nth))
else
world:set(e, pair(Lifetime, nth), data) world:set(e, pair(Lifetime, nth), data)
end end
i += 1 i += 1
@ -1649,7 +2137,7 @@ TEST("#repro2", function()
end end
end end
CHECK(not world:has(entity, pair(Lifetime, Particle))) CHECK(world:get(entity, pair(Lifetime, Particle)) == 0)
CHECK(world:get(entity, pair(Lifetime, Beam)) == 1) CHECK(world:get(entity, pair(Lifetime, Beam)) == 1)
end) end)
@ -1809,14 +2297,14 @@ TEST("change tracking", function()
world:set(e2, Foo, 2) world:set(e2, Foo, 2)
local i = 0 local i = 0
for e, new in q1 :: any do for e, new in q1 do
i += 1 i += 1
world:set(e, pair(Previous, Foo), new) world:set(e, pair(Previous, Foo), new)
end end
CHECK(i == 2) CHECK(i == 2)
local j = 0 local j = 0
for e, new in q1 :: any do for e, new in q1 do
j += 1 j += 1
world:set(e, pair(Previous, Foo), new) world:set(e, pair(Previous, Foo), new)
end end
@ -1837,14 +2325,14 @@ TEST("change tracking", function()
world:set(testEntity, component, 10) world:set(testEntity, component, 10)
local i = 0 local i = 0
for entity, number in q1 :: any do for entity, number in q1 do
i += 1 i += 1
world:add(testEntity, tag) world:add(testEntity, tag)
end end
CHECK(i == 1) CHECK(i == 1)
for e, n in q1 :: any do for e, n in q1 do
world:set(e, pair(previous, component), n) world:set(e, pair(previous, component), n)
end end
end end

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ukendio/jecs" name = "ukendio/jecs"
version = "0.7.2" version = "0.9.0-rc.8"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"