Compare commits

..

1 commit

Author SHA1 Message Date
PepeElToro41
f912866fcb add way to check preregistered id creations after world creation 2025-07-29 14:18:15 -06:00
18 changed files with 1348 additions and 2192 deletions

View file

@ -2,14 +2,6 @@
## 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
- `jecs.Exclusive` trait for making exclusive relationships.

View file

@ -160,10 +160,6 @@ local function monitors_new<T...>(
end
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
if callback ~= nil then
callback(entity, EcsOnRemove)

View file

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

View file

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

View file

@ -355,8 +355,45 @@ This operation is the same as calling:
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
@ -423,11 +460,11 @@ Example:
```luau [luau]
local entity = world:entity()
print(world:contains(entity))
print(world:has(entity))
world:delete(entity)
print(world:contains(entity))
print(world:has(entity))
-- Outputs:
-- true
@ -564,7 +601,7 @@ print(retrievedParent === parent) // true
## contains
Checks if an entity or component (id) exists and is alive in the world.
Checks if an entity exists and is alive in the world.
```luau
function World:contains(
@ -603,26 +640,12 @@ function World:exists(
## cleanup
Cleans up empty archetypes.
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
```
:::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:
::: code-group

View file

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

View file

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

@ -1,44 +0,0 @@
-- 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 world = jecs.world()
local world = jecs.World.new()
local Position = world:component()
local Velocity = world:component()

View file

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

View file

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

90
jecs.d.ts vendored
View file

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

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

File diff suppressed because it is too large Load diff

View file

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

66
werewitch.luau Executable file
View file

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