Compare commits

...

24 commits

Author SHA1 Message Date
Ukendio
af093713b4 Add examples
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-30 16:12:14 +02:00
Ukendio
549fe97622 Inline world_remove 2025-08-30 14:43:52 +02:00
Ukendio
b0e73857b9 Cleanup tests 2025-08-30 14:43:34 +02:00
Ukendio
917c951d55 Remove eagerly 2025-08-30 13:47:08 +02:00
Ukendio
037035a9a1 Revert :clear to previous behaviour 2025-08-29 17:01:36 +02:00
Ukendio
29a66d92c2 Add component trait lazily
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-26 23:15:48 +02:00
Ukendio
5de842d144 query should use internal world type
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-23 18:33:53 +02:00
Ukendio
9d1665944e world:added should not union the parameterized type with nil 2025-08-22 18:02:58 +02:00
Ukendio
8ace046470 Optimize queries 2025-08-22 17:54:35 +02:00
dai
0874e426af
Fix bulk_insert with moving archetypes (#272)
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
* Fix bulk_insert with moving archetypes

* Reword message
2025-08-21 21:32:11 +02:00
dai
51b09549db
Add record types (#271)
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
* Add record typings

* Correct
2025-08-20 22:37:32 +02:00
dai
a6c2d7152e
Allow tags in bulk_insert (#269) 2025-08-20 22:30:43 +02:00
Ukendio
2d9432ab7a Bump package versions
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-20 01:17:24 +02:00
Ukendio
96446f4a31 Fix 9+ term queries and cascaded deletion bug with different archetype 2025-08-20 01:15:30 +02:00
dai
bd00edc8c0
Separate Iter from IterFn (#267)
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
* Separate Iter from IterFn

* Clean up
2025-08-17 21:59:06 +02:00
dai
0bc1848554
Correct Archetype field types (#268) 2025-08-17 21:58:41 +02:00
dai
65a27a798a
Separate undefined components and tags (#266)
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-12 20:39:04 +02:00
Six
ac4441eb84
roblox-ts: Fix columns_map for easier lookups (#263)
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
* feat: retype archetype columns_map based on query

* feat: move InferComponents into Iter
2025-08-10 18:13:13 +02:00
Laptev Stanislav
f031dcee8d
docs(api): consolidate and clarify contains method documentation (#264)
Remove duplicate contains method section and update description to be more precise about checking both entities and components. Also fix example code references to use contains instead of has for consistency.
2025-08-10 18:12:50 +02:00
Ukendio
1d650d12e9 Fix backwards edge traversal for exclusive relationships 2025-08-10 16:52:08 +02:00
Ukendio
8f95309871 Improve relationship performance
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-07 18:53:50 +02:00
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
18 changed files with 2187 additions and 1329 deletions

View file

@ -2,6 +2,14 @@
## Unreleased ## Unreleased
### Added
- Added signals that allow listening to relation part of pairs in signals.
### Changed
- `OnRemove` hooks so that they are allowed to move entity's archetype even during deletion.
## 0.8.0
### Added ### Added
- `jecs.Exclusive` trait for making exclusive relationships. - `jecs.Exclusive` trait for making exclusive relationships.

View file

@ -160,6 +160,10 @@ local function monitors_new<T...>(
end end
local function removed(entity: jecs.Entity, component: jecs.Id) local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.record(world, entity)
if not archetypes[r.archetype.id] then
return
end
local EcsOnRemove = jecs.OnRemove :: jecs.Id local EcsOnRemove = jecs.OnRemove :: jecs.Id
if callback ~= nil then if callback ~= nil then
callback(entity, EcsOnRemove) callback(entity, EcsOnRemove)

View file

@ -199,6 +199,7 @@ do
end end
local q = world:query(A, B, C, D) local q = world:query(A, B, C, D)
q:archetypes()
START() START()
for id in q do for id in q do
end end

View file

@ -11,15 +11,22 @@ end
local jecs = require("@jecs") local jecs = require("@jecs")
local mirror = require("@mirror") local mirror = require("@mirror")
type i53 = number
do do
TITLE(testkit.color.white_underline("Jecs query")) TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.world() local ecs = jecs.world() :: jecs.World
do do
TITLE("one component in common") TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) local function view_bench(world: jecs.World,
A: jecs.Id,
B: jecs.Id,
C: jecs.Id,
D: jecs.Id,
E: jecs.Id,
F: jecs.Id,
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end
@ -131,11 +138,21 @@ end
do do
TITLE(testkit.color.white_underline("Mirror query")) TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new() local ecs = mirror.World.new() :: jecs.World
do do
TITLE("one component in common") TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) local function view_bench(
world: mirror.World,
A: jecs.Id,
B: jecs.Id,
C: jecs.Id,
D: jecs.Id,
E: jecs.Id,
F: jecs.Id,
G: jecs.Id,
H: jecs.Id
)
BENCH("1 component", function() BENCH("1 component", function()
for _ in world:query(A) do for _ in world:query(A) do
end end

View file

@ -355,46 +355,9 @@ This operation is the same as calling:
world:target(entity, jecs.ChildOf, 0) world:target(entity, jecs.ChildOf, 0)
``` ```
## contains
Checks if an entity or component (id) exists in the world.
```luau
function World:contains(
entity: Entity,
): boolean
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
print(world:contains(entity))
print(world:contains(1))
print(world:contains(2))
-- Outputs:
-- true
-- true
-- false
```
```ts [typescript]
const entity = world.entity();
print(world.contains(entity));
print(world.contains(1));
print(world.contains(2));
// Outputs:
// true
// true
// false
```
::: :::
## remove ## remove
Removes a component (ID) from an entity Removes a component (ID) from an entity
@ -460,11 +423,11 @@ Example:
```luau [luau] ```luau [luau]
local entity = world:entity() local entity = world:entity()
print(world:has(entity)) print(world:contains(entity))
world:delete(entity) world:delete(entity)
print(world:has(entity)) print(world:contains(entity))
-- Outputs: -- Outputs:
-- true -- true
@ -601,7 +564,7 @@ print(retrievedParent === parent) // true
## contains ## contains
Checks if an entity exists and is alive in the world. Checks if an entity or component (id) exists and is alive in the world.
```luau ```luau
function World:contains( function World:contains(
@ -640,12 +603,26 @@ function World:exists(
## cleanup ## cleanup
Cleans up deleted entities and their associated data. This is automatically called by jecs, but can be called manually if needed. Cleans up empty archetypes.
```luau ```luau
function World:cleanup(): void function World:cleanup(): void
``` ```
:::info
It is recommended to profile the optimal interval you should cleanup because it varies completely from game to game.
Here are a couple of reasons from Sander Mertens:
- some applications are memory constrained, so any wasted memory on empty
archetypes has to get cleaned up
- many archetypes can get created during game startup but aren't used later
on, so it would be wasteful to keep them around
- empty archetypes can slow queries down, especially if there are many more
empty ones than non-empty ones
- if the total number of component permutations (/relationships) is too
high, you have no choice but to periodically cleanup empty archetypes
:::
Example: Example:
::: code-group ::: code-group

View file

@ -1,15 +1,19 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local world = jecs.World.new() local world = jecs.world()
local Position = world:component() local Position = world:component() :: jecs.Id<vector>
local Walking = world:component() local Walking = world:component()
local Name = world:component() local Name = world:component() :: jecs.Id<string>
local function name(e: jecs.Entity): string
return assert(world:get(e, Name))
end
-- Create an entity with name Bob -- Create an entity with name Bob
local bob = world:entity() local bob = world:entity()
-- The set operation finds or creates a component, and sets it. -- The set operation finds or creates a component, and sets it.
world:set(bob, Position, Vector3.new(10, 20, 30)) world:set(bob, Position, vector.create(10, 20, 30))
-- Name the entity Bob -- Name the entity Bob
world:set(bob, Name, "Bob") world:set(bob, Name, "Bob")
-- The add operation adds a component without setting a value. This is -- The add operation adds a component without setting a value. This is
@ -18,15 +22,16 @@ world:add(bob, Walking)
-- Get the value for the Position component -- Get the value for the Position component
local pos = world:get(bob, Position) local pos = world:get(bob, Position)
print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`) assert(pos)
print(`\{{pos.x}, {pos.y}, {pos.z}\}`)
-- Overwrite the value of the Position component -- Overwrite the value of the Position component
world:set(bob, Position, Vector3.new(40, 50, 60)) world:set(bob, Position, vector.create(40, 50, 60))
local alice = world:entity() local alice = world:entity()
-- Create another named entity -- Create another named entity
world:set(alice, Name, "Alice") world:set(alice, Name, "Alice")
world:set(alice, Position, Vector3.new(10, 20, 30)) world:set(alice, Position, vector.create(10, 20, 30))
world:add(alice, Walking) world:add(alice, Walking)
-- Remove tag -- Remove tag
@ -34,7 +39,7 @@ world:remove(alice, Walking)
-- Iterate all entities with Position -- Iterate all entities with Position
for entity, p in world:query(Position) do for entity, p in world:query(Position) do
print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`) print(`{name(entity)}: \{{p.x}, {p.y}, {p.z}\}`)
end end
-- Output: -- Output:

View file

@ -0,0 +1,60 @@
-- Using world:target is the recommended way to grab the target in a wildcard
-- query. However the random access can add up in very hot paths. Accessing its
-- column can drastically improve performance, especially when there are
-- multiple adjacent pairs.
local jecs = require("@jecs")
local pair = jecs.pair
local __ = jecs.Wildcard
local world = jecs.world()
local Likes = world:entity()
local function name(e, name: string): string
if name then
world:set(e, jecs.Name, name)
return name
end
return assert(world:get(e, jecs.Name))
end
local e1 = world:entity()
name(e1, "e1")
local e2 = world:entity()
name(e2, "e2")
local e3 = world:entity()
name(e3, "e3")
world:add(e1, pair(Likes, e2))
world:add(e1, pair(Likes, e3))
local likes = jecs.component_record(world, pair(Likes, __))
assert(likes)
local likes_cr = likes.records
local likes_counts = likes.counts
local archetypes = world:query(pair(Likes, __)):archetypes()
for _, archetype in archetypes do
local types = archetype.types
-- Get the starting index which is what the (R, *) alias is at
local wc = likes_cr[archetype.id]
local count = likes_counts[archetype.id]
local entities = archetype.entities
for i = #entities, 1, -1 do
-- It is generally a good idea to iterate backwards on arrays if you
-- need to delete the iterated entity to prevent iterator invalidation
local entity = entities[i]
for cr = wc, wc + count - 1 do
local person = jecs.pair_second(world, types[cr])
print(`entity ${entity} likes ${person}`)
end
end
end
-- Output:
-- entity $273 likes $275
-- entity $273 likes $274

View file

@ -0,0 +1,44 @@
-- To get the most out of performance, you can lift the inner loop of queries to
-- the system in which you can do archetype-specific optimizations like finding
-- the parent once per archetype rather than per entity.
local jecs = require("@jecs")
local pair = jecs.pair
local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard
local world = jecs.world()
local Position = world:component() :: jecs.Id<vector>
local Visible = world:entity()
local parent = world:entity()
world:set(parent, Position, vector.zero)
world:add(parent, Visible)
local child = world:entity()
world:set(child, Position, vector.one)
world:add(child, pair(ChildOf, parent))
local parents = jecs.component_record(world, pair(ChildOf, __))
assert(parents)
local parent_cr = parents.records
local archetypes = world:query(Position, pair(ChildOf, __)):archetypes()
for _, archetype in archetypes do
local types = archetype.types
local p = jecs.pair_second(world, types[parent_cr[archetype.id]])
if world:has(p, Visible) then
local columns = archetype.columns_map
local positions = columns[Position]
for row, entity in archetype.entities do
local pos = positions[row]
print(`Child ${entity} of ${p} is visible at {pos}`)
end
end
end
-- Output:
-- Child $274 of $273 is visibile at 1,1,1

View file

@ -1,5 +1,5 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local world = jecs.World.new() local world = jecs.world()
local Position = world:component() local Position = world:component()
local Velocity = world:component() local Velocity = world:component()

View file

@ -1,19 +1,16 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local pair = jecs.pair local pair = jecs.pair
local world = jecs.World.new() local world = jecs.world()
local Name = world:component() local Name = world:component() :: jecs.Id<string>
local function named(ctr, name)
local e = ctr(world) local function name(e: jecs.Entity): string
world:set(e, Name, name) return assert(world:get(e, Name))
return e
end
local function name(e)
return world:get(e, Name)
end end
local Position = named(world.component, "Position") :: jecs.Entity<vector> local Position = world:component() :: jecs.Id<vector>
world:set(Position, Name, "Position")
local Previous = jecs.Rest local Previous = jecs.Rest
local added = world local added = world
@ -29,10 +26,14 @@ local removed = world
:cached() :cached()
local e1 = named(world.entity, "e1") local e1 = world:entity()
world:set(e1, Name, "e1")
world:set(e1, Position, vector.create(10, 20, 30)) world:set(e1, Position, vector.create(10, 20, 30))
local e2 = named(world.entity, "e2")
local e2 = world:entity()
world:set(e2, Name, "e2")
world:set(e2, Position, vector.create(10, 20, 30)) world:set(e2, Position, vector.create(10, 20, 30))
for entity, p in added do for entity, p in added do
print(`Added {name(entity)}: \{{p.x}, {p.y}, {p.z}}`) print(`Added {name(entity)}: \{{p.x}, {p.y}, {p.z}}`)
world:set(entity, pair(Previous, Position), p) world:set(entity, pair(Previous, Position), p)
@ -40,10 +41,10 @@ end
world:set(e1, Position, vector.create(999, 999, 1998)) world:set(e1, Position, vector.create(999, 999, 1998))
for _, archetype in changed:archetypes() do for entity, new, old in changed do
if new ~= old then if new ~= old then
print(`{name(e)}'s Position changed from \{{old.x}, {old.y}, {old.z}\} to \{{new.x}, {new.y}, {new.z}\}`) print(`{name(entity)}'s Position changed from \{{old.x}, {old.y}, {old.z}\} to \{{new.x}, {new.y}, {new.z}\}`)
world:set(e, pair(Previous, Position), new) world:set(entity, pair(Previous, Position), new)
end end
end end

View file

@ -3,32 +3,47 @@ local pair = jecs.pair
local ChildOf = jecs.ChildOf local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard local __ = jecs.Wildcard
local Name = jecs.Name local Name = jecs.Name
local world = jecs.World.new() local world = jecs.world()
type Id<T = nil> = number & { __T: T } local Voxel = world:component() :: jecs.Id
local Voxel = world:component() :: Id local Position = world:component() :: jecs.Id<vector>
local Position = world:component() :: Id<Vector3> local Perception = world:component() :: jecs.Id<{
local Perception = world:component() :: Id<{
range: number, range: number,
fov: number, fov: number,
dir: Vector3, dir: vector,
}> }>
local PrimaryPart = world:component() :: Id<Part> type part = {
Position: vector
}
local PrimaryPart = world:component() :: jecs.Id<part>
local local_player = game:GetService("Players").LocalPlayer local local_player = {
Character = {
PrimaryPart = {
Position = vector.create(50, 0, 30)
}
}
}
local workspace = {
CurrentCamera = {
CFrame = {
LookVector = vector.create(0, 0, -1)
}
}
}
local function distance(a: Vector3, b: Vector3) local function distance(a: vector, b: vector)
return (b - a).Magnitude return vector.magnitude((b - a))
end end
local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number) local function is_in_fov(a: vector, b: vector, forward_dir: vector, fov_angle: number)
local to_target = b - a local to_target = b - a
local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit local forward_xz = vector.normalize(vector.create(forward_dir.x, 0, forward_dir.z))
local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit local to_target_xz = vector.normalize(vector.create(to_target.x, 0, to_target.z))
local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X)) local angle_to_target = math.deg(math.atan2(to_target_xz.z, to_target_xz.x))
local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X)) local forward_angle = math.deg(math.atan2(forward_xz.z, forward_xz.z))
local angle_difference = math.abs(forward_angle - angle_to_target) local angle_difference = math.abs(forward_angle - angle_to_target)
@ -42,7 +57,7 @@ end
local map = {} local map = {}
local grid = 50 local grid = 50
local function add_to_voxel(source: number, position: Vector3, prev_voxel_id: number?) local function add_to_voxel(source: jecs.Entity, position: vector, prev_voxel_id: jecs.Entity?)
local hash = position // grid local hash = position // grid
local voxel_id = map[hash] local voxel_id = map[hash]
if not voxel_id then if not voxel_id then
@ -79,7 +94,7 @@ local function update_camera_direction(dt: number)
end end
local function perceive_enemies(dt: number) local function perceive_enemies(dt: number)
local it = world:query(Perception, Position, PrimaryPart) local it = world:query(Perception, Position, PrimaryPart):iter()
-- There is only going to be one entity matching the query -- There is only going to be one entity matching the query
local e, self_perception, self_position, self_primary_part = it() local e, self_perception, self_position, self_primary_part = it()
@ -93,28 +108,28 @@ local function perceive_enemies(dt: number)
if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then
local p = target_position local p = target_position
print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`) print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.x}, {p.y}, {p.z})`)
end end
end end
end end
local player = world:entity() local player = world:entity()
world:set(player, Perception, { world:set(player, Perception, {
range = 100, range = 200,
fov = 90, fov = 90,
dir = Vector3.new(1, 0, 0), dir = vector.create(1, 0, 0),
}) })
world:set(player, Name, "LocalPlayer") world:set(player, Name, "LocalPlayer")
local primary_part = (local_player.Character :: Model).PrimaryPart :: Part local primary_part = local_player.Character.PrimaryPart
world:set(player, PrimaryPart, primary_part) world:set(player, PrimaryPart, primary_part)
world:set(player, Position, Vector3.zero) world:set(player, Position, vector.zero)
local enemy = world:entity() local enemy = world:entity()
world:set(enemy, Name, "Enemy $1") world:set(enemy, Name, "Enemy $1")
world:set(enemy, Position, Vector3.new(50, 0, 20)) world:set(enemy, Position, vector.create(50, 0, 20))
add_to_voxel(player, primary_part.Position) add_to_voxel(player, primary_part.Position)
add_to_voxel(enemy, world) add_to_voxel(enemy, assert(world:get(enemy, Position)))
local dt = 1 / 60 local dt = 1 / 60
reconcile_client_owned_assembly_to_voxel(dt) reconcile_client_owned_assembly_to_voxel(dt)

90
jecs.d.ts vendored
View file

@ -7,10 +7,14 @@ export type Entity<TData = unknown> = number & {
readonly __type_TData: TData; readonly __type_TData: TData;
}; };
type TagDiscriminator = {
readonly __nominal_Tag: unique symbol;
};
/** /**
* An entity with no associated data when used as a component * An entity with no associated data when used as a component
*/ */
export type Tag = Entity<undefined>; export type Tag = Entity<TagDiscriminator>;
/** /**
* A pair of entities: * A pair of entities:
@ -26,12 +30,12 @@ export type Pair<P = unknown, O = unknown> = number & {
* An `Id` can be either a single Entity or a Pair of Entities. * An `Id` can be either a single Entity or a Pair of Entities.
* By providing `TData`, you can specifically require an Id that yields that type. * By providing `TData`, you can specifically require an Id that yields that type.
*/ */
export type Id<TData = unknown> = Entity<TData> | Pair<TData, unknown> | Pair<undefined, TData>; export type Id<TData = unknown> = Entity<TData> | Pair<TData, unknown> | Pair<TagDiscriminator, TData>;
export type InferComponent<E> = E extends Entity<infer D> export type InferComponent<E> = E extends Entity<infer D>
? D ? D
: E extends Pair<infer P, infer O> : E extends Pair<infer P, infer O>
? P extends undefined ? P extends TagDiscriminator
? O ? O
: P : P
: never; : never;
@ -43,22 +47,30 @@ type InferComponents<A extends Id[]> = { [K in keyof A]: InferComponent<A[K]> };
type ArchetypeId = number; type ArchetypeId = number;
export type Column<T> = T[]; export type Column<T> = T[];
export type Archetype<T extends unknown[]> = { export type Archetype<T extends Id[]> = {
id: number; id: number;
types: number[]; types: Entity[];
type: string; type: string;
entities: number[]; entities: Entity[];
columns: Column<unknown>[]; columns: Column<unknown>[];
columns_map: Record<Id, Column<T[number]>> columns_map: { [K in T[number]]: Column<InferComponent<K>> };
}; };
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>; type IterFn<T extends Id[]> = IterableFunction<LuaTuple<[Entity, ...InferComponents<T>]>>;
type Iter<T extends Id[]> = IterFn<T> & {
/**
* This isn't callable
* @hidden
* @deprecated
*/
(): never
};
export type CachedQuery<T extends unknown[]> = { export type CachedQuery<T extends Id[]> = {
/** /**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents]. * Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/ */
iter(): Iter<T>; iter(): IterFn<T>;
/** /**
* Returns the matched archetypes of the query * Returns the matched archetypes of the query
@ -67,11 +79,11 @@ export type CachedQuery<T extends unknown[]> = {
archetypes(): Archetype<T>[]; archetypes(): Archetype<T>[];
} & Iter<T>; } & Iter<T>;
export type Query<T extends unknown[]> = { export type Query<T extends Id[]> = {
/** /**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents]. * Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/ */
iter(): Iter<T>; iter(): IterFn<T>;
/** /**
* Creates and returns a cached version of this query for efficient reuse. * Creates and returns a cached version of this query for efficient reuse.
@ -119,7 +131,7 @@ export class World {
* @returns An entity (Tag) with no data. * @returns An entity (Tag) with no data.
*/ */
entity(): Tag; entity(): Tag;
entity<T extends Entity>(id: T): InferComponent<T> extends undefined ? Tag : T; entity<T extends Entity>(id: T): T;
/** /**
* Creates a new entity in the first 256 IDs, typically used for static * Creates a new entity in the first 256 IDs, typically used for static
@ -148,7 +160,7 @@ export class World {
* @param entity The target entity. * @param entity The target entity.
* @param component The component (or tag) to add. * @param component The component (or tag) to add.
*/ */
add<C>(entity: Entity, component: undefined extends InferComponent<C> ? C : Id<undefined>): void; add<C>(entity: Entity, component: TagDiscriminator extends InferComponent<C> ? C : Id<TagDiscriminator>): void;
/** /**
* Installs a hook on the given component. * Installs a hook on the given component.
@ -172,6 +184,11 @@ export class World {
*/ */
cleanup(): void; cleanup(): void;
/**
* Removes all instances of specified component
*/
// purge<T>(component: Id<T>): void
/** /**
* Clears all components and relationships from the given entity, but * Clears all components and relationships from the given entity, but
* does not delete the entity from the world. * does not delete the entity from the world.
@ -246,11 +263,11 @@ export class World {
* @param components The list of components to query. * @param components The list of components to query.
* @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<T>;
added<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void 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 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 removed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>) => void): () => void;
} }
export function world(): World; export function world(): World;
@ -297,11 +314,11 @@ export function ECS_PAIR_FIRST(pair: Pair): number;
export function ECS_PAIR_SECOND(pair: Pair): number; export function ECS_PAIR_SECOND(pair: Pair): number;
type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & { type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & {
readonly __nominal_StatefulHook: unique symbol, readonly __nominal_StatefulHook: unique symbol;
} };
type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & { type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & {
readonly __nominal_StatelessHook: unique symbol, readonly __nominal_StatelessHook: unique symbol;
} };
export declare const OnAdd: StatefulHook; export declare const OnAdd: StatefulHook;
export declare const OnRemove: StatelessHook; export declare const OnRemove: StatelessHook;
@ -319,12 +336,27 @@ export declare const Exclusive: Tag;
export declare const Rest: Entity; export declare const Rest: Entity;
export type ComponentRecord = { export type ComponentRecord = {
records: Map<Id, number>, records: Map<Id, number>;
counts: Map<Id, number>, counts: Map<Id, number>;
size: number, size: number;
} };
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 type TagToUndefined<T> = T extends TagDiscriminator ? undefined : T
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void
export function bulk_insert<const C extends Id[]>(
world: World,
entity: Entity,
ids: C,
values: { [K in keyof C]: TagToUndefined<InferComponent<C[K]>> },
): void;
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void;
export type EntityRecord<T extends Id[]> = {
archetype: Archetype<T>,
row: number,
dense: number,
};
export function record<T extends Id[] = []>(world: World, entity: Entity): EntityRecord<T>;

892
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.9.0-rc.7", "version": "0.9.0-rc.12",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "repository": {

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,66 +0,0 @@
local jecs = require("@jecs")
local world = jecs.world()
local pair = jecs.pair
local IsA = world:entity()
local traits = {
IsA = IsA
}
world:set(IsA, jecs.OnAdd, function(component, id)
local second = jecs.pair_second(world, id)
assert(second ~= component, "circular")
local is_tag = jecs.is_tag(world, second)
world:added(component, function(entity, _, value)
if is_tag then
world:add(entity, second)
else
world:set(entity, second, value)
end
end)
world:removed(component, function(entity)
world:remove(entity, second)
end)
if not is_tag then
world:changed(component, function(entity, _, value)
world:set(entity, second, value)
end)
end
local r = jecs.record(world, second) :: jecs.Record
local archetype = r.archetype
if not archetype then
return
end
local types = archetype.types
for _, id in types do
if jecs.is_tag(world, id) then
world:add(component, id)
else
local metadata = world:get(second, id)
if not world:has(component, id) then
world:set(component, id, metadata)
end
end
end
-- jecs.bulk_insert(world, component, ids, values)
end)
local Witch = world:entity()
local Werewolf = world:entity()
local WereWitch = world:entity()
world:add(WereWitch, pair(IsA, Witch))
world:add(WereWitch, pair(IsA, Werewolf))
local e = world:entity()
world:add(e, WereWitch)
print(world:has(e, pair(IsA, Witch))) -- false
print(world:has(e, Witch)) -- true