mirror of
https://github.com/Ukendio/jecs.git
synced 2025-09-23 08:39:16 +00:00
Compare commits
24 commits
f912866fcb
...
af093713b4
Author | SHA1 | Date | |
---|---|---|---|
|
af093713b4 | ||
|
549fe97622 | ||
|
b0e73857b9 | ||
|
917c951d55 | ||
|
037035a9a1 | ||
|
29a66d92c2 | ||
|
5de842d144 | ||
|
9d1665944e | ||
|
8ace046470 | ||
|
0874e426af | ||
|
51b09549db | ||
|
a6c2d7152e | ||
|
2d9432ab7a | ||
|
96446f4a31 | ||
|
bd00edc8c0 | ||
|
0bc1848554 | ||
|
65a27a798a | ||
|
ac4441eb84 | ||
|
f031dcee8d | ||
|
1d650d12e9 | ||
|
8f95309871 | ||
|
0b6bfea5c8 | ||
|
3cfce10a4a | ||
|
add9ad3939 |
18 changed files with 2187 additions and 1329 deletions
|
@ -2,6 +2,14 @@
|
|||
|
||||
## 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.
|
||||
|
||||
|
|
|
@ -160,6 +160,10 @@ 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)
|
||||
|
|
|
@ -199,6 +199,7 @@ do
|
|||
end
|
||||
|
||||
local q = world:query(A, B, C, D)
|
||||
q:archetypes()
|
||||
START()
|
||||
for id in q do
|
||||
end
|
||||
|
|
|
@ -11,15 +11,22 @@ end
|
|||
local jecs = require("@jecs")
|
||||
local mirror = require("@mirror")
|
||||
|
||||
type i53 = number
|
||||
|
||||
do
|
||||
TITLE(testkit.color.white_underline("Jecs query"))
|
||||
local ecs = jecs.world()
|
||||
local ecs = jecs.world() :: jecs.World
|
||||
do
|
||||
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()
|
||||
for _ in world:query(A) do
|
||||
end
|
||||
|
@ -131,11 +138,21 @@ end
|
|||
|
||||
do
|
||||
TITLE(testkit.color.white_underline("Mirror query"))
|
||||
local ecs = mirror.World.new()
|
||||
local ecs = mirror.World.new() :: jecs.World
|
||||
do
|
||||
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()
|
||||
for _ in world:query(A) do
|
||||
end
|
||||
|
|
|
@ -355,46 +355,9 @@ 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
|
||||
|
||||
Removes a component (ID) from an entity
|
||||
|
@ -460,11 +423,11 @@ Example:
|
|||
|
||||
```luau [luau]
|
||||
local entity = world:entity()
|
||||
print(world:has(entity))
|
||||
print(world:contains(entity))
|
||||
|
||||
world:delete(entity)
|
||||
|
||||
print(world:has(entity))
|
||||
print(world:contains(entity))
|
||||
|
||||
-- Outputs:
|
||||
-- true
|
||||
|
@ -601,7 +564,7 @@ print(retrievedParent === parent) // true
|
|||
|
||||
## 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
|
||||
function World:contains(
|
||||
|
@ -640,12 +603,26 @@ function World:exists(
|
|||
|
||||
## 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
|
||||
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
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
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 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
|
||||
local bob = world:entity()
|
||||
|
||||
-- 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
|
||||
world:set(bob, Name, "Bob")
|
||||
-- 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
|
||||
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
|
||||
world:set(bob, Position, Vector3.new(40, 50, 60))
|
||||
world:set(bob, Position, vector.create(40, 50, 60))
|
||||
|
||||
local alice = world:entity()
|
||||
-- Create another named entity
|
||||
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)
|
||||
|
||||
-- Remove tag
|
||||
|
@ -34,7 +39,7 @@ world:remove(alice, Walking)
|
|||
|
||||
-- Iterate all entities with Position
|
||||
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
|
||||
|
||||
-- Output:
|
||||
|
|
60
examples/luau/queries/archetypes/targets.luau
Executable file
60
examples/luau/queries/archetypes/targets.luau
Executable 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
|
44
examples/luau/queries/archetypes/visibility_cascades.luau
Executable file
44
examples/luau/queries/archetypes/visibility_cascades.luau
Executable 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
|
|
@ -1,5 +1,5 @@
|
|||
local jecs = require("@jecs")
|
||||
local world = jecs.World.new()
|
||||
local world = jecs.world()
|
||||
|
||||
local Position = world:component()
|
||||
local Velocity = world:component()
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
local jecs = require("@jecs")
|
||||
local pair = jecs.pair
|
||||
|
||||
local world = jecs.World.new()
|
||||
local Name = world:component()
|
||||
local world = jecs.world()
|
||||
local Name = world:component() :: jecs.Id<string>
|
||||
|
||||
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)
|
||||
|
||||
local function name(e: jecs.Entity): string
|
||||
return assert(world:get(e, Name))
|
||||
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 added = world
|
||||
|
@ -29,10 +26,14 @@ local removed = world
|
|||
: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))
|
||||
local e2 = named(world.entity, "e2")
|
||||
|
||||
local e2 = world:entity()
|
||||
world:set(e2, Name, "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)
|
||||
|
@ -40,10 +41,10 @@ end
|
|||
|
||||
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
|
||||
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)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,32 +3,47 @@ local pair = jecs.pair
|
|||
local ChildOf = jecs.ChildOf
|
||||
local __ = jecs.Wildcard
|
||||
local Name = jecs.Name
|
||||
local world = jecs.World.new()
|
||||
local world = jecs.world()
|
||||
|
||||
type Id<T = nil> = number & { __T: T }
|
||||
local Voxel = world:component() :: Id
|
||||
local Position = world:component() :: Id<Vector3>
|
||||
local Perception = world:component() :: Id<{
|
||||
local Voxel = world:component() :: jecs.Id
|
||||
local Position = world:component() :: jecs.Id<vector>
|
||||
local Perception = world:component() :: jecs.Id<{
|
||||
range: 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)
|
||||
return (b - a).Magnitude
|
||||
local function distance(a: vector, b: vector)
|
||||
return vector.magnitude((b - a))
|
||||
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 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 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 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_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_difference = math.abs(forward_angle - angle_to_target)
|
||||
|
||||
|
@ -42,7 +57,7 @@ end
|
|||
local map = {}
|
||||
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 voxel_id = map[hash]
|
||||
if not voxel_id then
|
||||
|
@ -79,7 +94,7 @@ local function update_camera_direction(dt: number)
|
|||
end
|
||||
|
||||
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
|
||||
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
|
||||
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 = 100,
|
||||
range = 200,
|
||||
fov = 90,
|
||||
dir = Vector3.new(1, 0, 0),
|
||||
dir = vector.create(1, 0, 0),
|
||||
})
|
||||
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, Position, Vector3.zero)
|
||||
world:set(player, Position, vector.zero)
|
||||
|
||||
local enemy = world:entity()
|
||||
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(enemy, world)
|
||||
add_to_voxel(enemy, assert(world:get(enemy, Position)))
|
||||
|
||||
local dt = 1 / 60
|
||||
reconcile_client_owned_assembly_to_voxel(dt)
|
||||
|
|
90
jecs.d.ts
vendored
90
jecs.d.ts
vendored
|
@ -7,10 +7,14 @@ 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<undefined>;
|
||||
export type Tag = Entity<TagDiscriminator>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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>
|
||||
? D
|
||||
: E extends Pair<infer P, infer O>
|
||||
? P extends undefined
|
||||
? P extends TagDiscriminator
|
||||
? O
|
||||
: P
|
||||
: never;
|
||||
|
@ -43,22 +47,30 @@ 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 unknown[]> = {
|
||||
export type Archetype<T extends Id[]> = {
|
||||
id: number;
|
||||
types: number[];
|
||||
types: Entity[];
|
||||
type: string;
|
||||
entities: number[];
|
||||
entities: Entity[];
|
||||
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].
|
||||
*/
|
||||
iter(): Iter<T>;
|
||||
iter(): IterFn<T>;
|
||||
|
||||
/**
|
||||
* Returns the matched archetypes of the query
|
||||
|
@ -67,11 +79,11 @@ export type CachedQuery<T extends unknown[]> = {
|
|||
archetypes(): Archetype<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].
|
||||
*/
|
||||
iter(): Iter<T>;
|
||||
iter(): IterFn<T>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
|
@ -148,7 +160,7 @@ export class World {
|
|||
* @param entity The target entity.
|
||||
* @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.
|
||||
|
@ -172,6 +184,11 @@ 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.
|
||||
|
@ -246,11 +263,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<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
|
||||
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;
|
||||
|
@ -297,11 +314,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;
|
||||
|
@ -319,12 +336,27 @@ 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;
|
||||
|
||||
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
|
||||
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>;
|
||||
|
|
1205
mirror.luau
1205
mirror.luau
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rbxts/jecs",
|
||||
"version": "0.9.0-rc.7",
|
||||
"version": "0.9.0-rc.12",
|
||||
"description": "Stupidly fast Entity Component System",
|
||||
"main": "jecs.luau",
|
||||
"repository": {
|
||||
|
|
767
test/tests.luau
767
test/tests.luau
|
@ -24,38 +24,6 @@ type Id<T=unknown> = jecs.Id<T>
|
|||
local entity_visualiser = require("@tools/entity_visualiser")
|
||||
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()
|
||||
|
@ -90,6 +58,7 @@ TEST("Ensure archetype edges get cleaned", function()
|
|||
CHECK(false)
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("repeated entity cached query", function()
|
||||
local pair = jecs.pair
|
||||
local world = jecs.world()
|
||||
|
@ -148,7 +117,6 @@ TEST("repeated pairs", function()
|
|||
|
||||
local e2 = world:entity()
|
||||
|
||||
print("-----")
|
||||
world:set(e2, p2, true)
|
||||
|
||||
CHECK(world:get(e2, p2))
|
||||
|
@ -190,7 +158,6 @@ TEST("repro", function()
|
|||
end
|
||||
CHECK(count == 1)
|
||||
count = 0
|
||||
print("----")
|
||||
world:add(e2v1, jecs.pair(relation, e1v1))
|
||||
CHECK(world:has(e2v1, jecs.pair(relation, e1v1)))
|
||||
|
||||
|
@ -198,10 +165,11 @@ TEST("repro", function()
|
|||
count += 1
|
||||
end
|
||||
|
||||
print(count)
|
||||
CHECK(count==1)
|
||||
end)
|
||||
|
||||
TEST("bulk", function()
|
||||
do CASE "Should allow components and tags to be in disorder"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
|
@ -292,6 +260,76 @@ TEST("bulk", function()
|
|||
CHECK(world:has(e, E) == false)
|
||||
CHECK(world:has(e, F) == false)
|
||||
CHECK(world:has(e, G) == false)
|
||||
end
|
||||
do CASE "Should bulk add by default when there is no values"
|
||||
local world = jecs.world()
|
||||
local t1, t2, t3 = world:entity(), world:entity(), world:entity()
|
||||
local count = 0
|
||||
local function counter()
|
||||
count += 1
|
||||
end
|
||||
world:added(t1, counter)
|
||||
world:added(t2, counter)
|
||||
world:added(t3, counter)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, {t1,t2,t3}, {})
|
||||
|
||||
CHECK(world:has(e, t1, t2, t3))
|
||||
CHECK(count == 3)
|
||||
end
|
||||
|
||||
do CASE "Should bulk add by default when there is no values"
|
||||
local world = jecs.world()
|
||||
local c1, c2, c3 = world:component(), world:component(), world:component()
|
||||
local count = 0
|
||||
local function counter()
|
||||
count += 1
|
||||
end
|
||||
|
||||
world:changed(c1, counter)
|
||||
world:changed(c2, counter)
|
||||
world:changed(c3, counter)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, {c1,c2,c3}, {1,2,3})
|
||||
|
||||
jecs.bulk_insert(world, e, {c1,c2,c3}, {4,5,6})
|
||||
|
||||
CHECK(world:has(e, c1, c2, c3))
|
||||
CHECK(count == 3)
|
||||
end
|
||||
|
||||
do CASE "Should bulk add with hooks moving archetypes without previous"
|
||||
local world = jecs.world()
|
||||
local c1, c2, c3 = world:component(), world:component(), world:component()
|
||||
|
||||
world:added(c1, function(e)
|
||||
world:set(e, c3, "hello")
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
jecs.bulk_insert(world, e, {c1,c2}, {true, 123})
|
||||
CHECK(world:get(e, c1) == true)
|
||||
CHECK(world:get(e, c2) == 123)
|
||||
CHECK(world:get(e, c3) == "hello")
|
||||
end
|
||||
|
||||
do CASE "Should bulk add with hooks moving archetypes with previous"
|
||||
local world = jecs.world()
|
||||
local c1, c2, c3 = world:component(), world:component(), world:component()
|
||||
|
||||
world:added(c1, function(e)
|
||||
world:set(e, c3, "hello")
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, world:entity())
|
||||
jecs.bulk_insert(world, e, {c1,c2}, {true, 123})
|
||||
CHECK(world:get(e, c1) == true)
|
||||
CHECK(world:get(e, c2) == 123)
|
||||
CHECK(world:get(e, c3) == "hello")
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("repro", function()
|
||||
|
@ -319,7 +357,25 @@ TEST("repro", function()
|
|||
end)
|
||||
|
||||
TEST("world:add()", function()
|
||||
do CASE "exclusive relations"
|
||||
do CASE "Removing exclusive pair should traverse backwards on edge"
|
||||
local world = jecs.world()
|
||||
local a = world:entity()
|
||||
local b = world:entity()
|
||||
local c = world:entity()
|
||||
|
||||
world:add(a, pair(ChildOf, b))
|
||||
world:add(a, pair(ChildOf, c))
|
||||
|
||||
CHECK(not world:has(a, pair(ChildOf, b)))
|
||||
CHECK(world:has(a, pair(ChildOf, c)))
|
||||
|
||||
world:remove(a, pair(ChildOf, c))
|
||||
|
||||
CHECK(not world:has(a, pair(ChildOf, b)))
|
||||
CHECK(not world:has(a, pair(ChildOf, c)))
|
||||
CHECK(not world:target(a, ChildOf))
|
||||
end
|
||||
do CASE "Exclusive relations"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
world:add(A, jecs.Exclusive)
|
||||
|
@ -333,6 +389,15 @@ TEST("world:add()", function()
|
|||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
|
||||
-- We have to test the path that checks the uncached method
|
||||
local e1 = world:entity()
|
||||
|
||||
world:add(e1, pair(A, B))
|
||||
world:add(e1, pair(A, C))
|
||||
|
||||
CHECK(world:has(e1, pair(A, B)) == false)
|
||||
CHECK(world:has(e1, pair(A, C)) == true)
|
||||
end
|
||||
|
||||
do CASE "exclusive relations invoke hooks"
|
||||
|
@ -379,6 +444,44 @@ TEST("world:add()", function()
|
|||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
end
|
||||
|
||||
do CASE "exclusive relations invoke on_remove hooks that should allow side effects"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
world:add(A, jecs.Exclusive)
|
||||
local call_count = 0
|
||||
world:set(A, jecs.OnRemove, function(e, id)
|
||||
call_count += 1
|
||||
if call_count == 1 then
|
||||
world:add(e, C)
|
||||
else
|
||||
world:add(e, D)
|
||||
end
|
||||
end)
|
||||
|
||||
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)
|
||||
CHECK(world:has(e, C))
|
||||
|
||||
|
||||
-- We have to ensure that it actually invokes hooks everytime it
|
||||
-- traverses the archetype
|
||||
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)
|
||||
CHECK(world:has(e, D))
|
||||
end
|
||||
|
||||
do CASE "idempotent"
|
||||
local world = jecs.world()
|
||||
local d = dwi(world)
|
||||
|
@ -415,9 +518,9 @@ TEST("world:add()", function()
|
|||
end)
|
||||
|
||||
TEST("world:children()", function()
|
||||
local world = jecs.world()
|
||||
local C = jecs.component()
|
||||
local T = jecs.tag()
|
||||
local world = jecs.world()
|
||||
|
||||
local e1 = world:entity()
|
||||
world:set(e1, C, true)
|
||||
|
@ -456,100 +559,146 @@ TEST("world:children()", function()
|
|||
jecs.ECS_META_RESET()
|
||||
end)
|
||||
|
||||
TEST("world:clear()", function()
|
||||
do CASE "should remove its components"
|
||||
-- TEST("world:purge()", function()
|
||||
-- do CASE "should remove all instances of specified component"
|
||||
-- local world = jecs.world()
|
||||
-- local A = world:component()
|
||||
-- local B = world:component()
|
||||
|
||||
-- local e = world:entity()
|
||||
-- local e1 = world:entity()
|
||||
-- local _e2 = world:entity()
|
||||
|
||||
-- world:set(e, A, true)
|
||||
-- world:set(e, B, true)
|
||||
|
||||
-- world:set(e1, A, true)
|
||||
-- world:set(e1, B, true)
|
||||
|
||||
-- CHECK(world:get(e, A))
|
||||
-- CHECK(world:get(e, B))
|
||||
|
||||
-- world:purge(A)
|
||||
-- CHECK(world:get(e, A) == nil)
|
||||
-- CHECK(world:get(e, B))
|
||||
-- CHECK(world:get(e1, A) == nil)
|
||||
-- CHECK(world:get(e1, B))
|
||||
-- end
|
||||
|
||||
-- do CASE "remove purged component from entities"
|
||||
-- local world = jecs.world()
|
||||
-- local A = world:component()
|
||||
-- local B = world:component()
|
||||
-- local C = world:component()
|
||||
|
||||
-- do
|
||||
-- local id1 = world:entity()
|
||||
-- local id2 = world:entity()
|
||||
-- local id3 = world:entity()
|
||||
|
||||
-- world:set(id1, A, true)
|
||||
|
||||
-- world:set(id2, A, true)
|
||||
-- world:set(id2, B, true)
|
||||
|
||||
-- world:set(id3, A, true)
|
||||
-- world:set(id3, B, true)
|
||||
-- world:set(id3, C, true)
|
||||
|
||||
-- world:purge(A)
|
||||
|
||||
-- CHECK(not world:has(id1, A))
|
||||
-- CHECK(not world:has(id2, A))
|
||||
-- CHECK(not world:has(id3, A))
|
||||
|
||||
-- CHECK(world:has(id2, B))
|
||||
-- CHECK(world:has(id3, B, C))
|
||||
|
||||
-- world:purge(C)
|
||||
|
||||
-- CHECK(world:has(id2, B))
|
||||
-- CHECK(world:has(id3, B))
|
||||
|
||||
-- CHECK(world:contains(A))
|
||||
-- CHECK(world:contains(C))
|
||||
-- CHECK(world:has(A, jecs.Component))
|
||||
-- CHECK(world:has(B, jecs.Component))
|
||||
-- end
|
||||
|
||||
-- do
|
||||
-- local id1 = world:entity()
|
||||
-- local id2 = world:entity()
|
||||
-- local id3 = world:entity()
|
||||
|
||||
-- local tgt = world:entity()
|
||||
|
||||
-- world:add(id1, pair(A, tgt))
|
||||
-- world:add(id1, pair(B, tgt))
|
||||
-- world:add(id1, pair(C, tgt))
|
||||
|
||||
-- world:add(id2, pair(A, tgt))
|
||||
-- world:add(id2, pair(B, tgt))
|
||||
-- world:add(id2, pair(C, tgt))
|
||||
|
||||
-- world:add(id3, pair(A, tgt))
|
||||
-- world:add(id3, pair(B, tgt))
|
||||
-- world:add(id3, pair(C, tgt))
|
||||
|
||||
-- world:purge(B)
|
||||
-- CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
|
||||
-- CHECK(not world:has(id1, pair(B, tgt)))
|
||||
-- CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
|
||||
-- CHECK(not world:has(id1, pair(B, tgt)))
|
||||
-- CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
|
||||
|
||||
-- end
|
||||
|
||||
-- end
|
||||
-- end)
|
||||
|
||||
TEST("world:clear", function()
|
||||
do CASE "remove all components on entity"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
|
||||
local e = world:entity()
|
||||
local e1 = world:entity()
|
||||
local _e2 = world:entity()
|
||||
|
||||
world:set(e, A, true)
|
||||
world:set(e, B, true)
|
||||
|
||||
world:set(e1, A, true)
|
||||
world:set(e1, B, true)
|
||||
world:clear(e)
|
||||
|
||||
CHECK(world:get(e, A))
|
||||
CHECK(world:get(e, B))
|
||||
|
||||
world:clear(A)
|
||||
CHECK(world:get(e, A) == nil)
|
||||
CHECK(world:get(e, B))
|
||||
CHECK(world:get(e1, A) == nil)
|
||||
CHECK(world:get(e1, B))
|
||||
CHECK(world:contains(e))
|
||||
CHECK(not world:has(e, A))
|
||||
CHECK(not world:has(e, B))
|
||||
print(jecs.record(world, e).archetype == nil::any)
|
||||
end
|
||||
|
||||
do CASE "remove cleared ID from entities"
|
||||
do CASE "should invoke hooks"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local called = 0
|
||||
world:set(A, jecs.OnRemove, function()
|
||||
called += 1
|
||||
end)
|
||||
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
world:set(B, jecs.OnRemove, function()
|
||||
called += 1
|
||||
end)
|
||||
|
||||
do
|
||||
local id1 = world:entity()
|
||||
local id2 = world:entity()
|
||||
local id3 = world:entity()
|
||||
local e = world:entity()
|
||||
|
||||
world:set(id1, A, true)
|
||||
world:set(e, A, true)
|
||||
world:set(e, B, true)
|
||||
|
||||
world:set(id2, A, true)
|
||||
world:set(id2, B, true)
|
||||
|
||||
world:set(id3, A, true)
|
||||
world:set(id3, B, true)
|
||||
world:set(id3, C, true)
|
||||
|
||||
world:clear(A)
|
||||
|
||||
CHECK(not world:has(id1, A))
|
||||
CHECK(not world:has(id2, A))
|
||||
CHECK(not world:has(id3, A))
|
||||
|
||||
CHECK(world:has(id2, B))
|
||||
CHECK(world:has(id3, B, C))
|
||||
|
||||
world:clear(C)
|
||||
|
||||
CHECK(world:has(id2, B))
|
||||
CHECK(world:has(id3, B))
|
||||
|
||||
CHECK(world:contains(A))
|
||||
CHECK(world:contains(C))
|
||||
CHECK(world:has(A, jecs.Component))
|
||||
CHECK(world:has(B, jecs.Component))
|
||||
end
|
||||
|
||||
do
|
||||
local id1 = world:entity()
|
||||
local id2 = world:entity()
|
||||
local id3 = world:entity()
|
||||
|
||||
local tgt = world:entity()
|
||||
|
||||
world:add(id1, pair(A, tgt))
|
||||
world:add(id1, pair(B, tgt))
|
||||
world:add(id1, pair(C, tgt))
|
||||
|
||||
world:add(id2, pair(A, tgt))
|
||||
world:add(id2, pair(B, tgt))
|
||||
world:add(id2, pair(C, tgt))
|
||||
|
||||
world:add(id3, pair(A, tgt))
|
||||
world:add(id3, pair(B, tgt))
|
||||
world:add(id3, pair(C, tgt))
|
||||
|
||||
world:clear(B)
|
||||
CHECK(world:has(id1, pair(A, tgt), pair(C, tgt)))
|
||||
CHECK(not world:has(id1, pair(B, tgt)))
|
||||
CHECK(world:has(id2, pair(A, tgt), pair(C, tgt)))
|
||||
CHECK(not world:has(id1, pair(B, tgt)))
|
||||
CHECK(world:has(id3, pair(A, tgt), pair(C, tgt)))
|
||||
|
||||
end
|
||||
world:clear(e)
|
||||
|
||||
CHECK(world:contains(e))
|
||||
CHECK(not world:has(e, A))
|
||||
CHECK(not world:has(e, B))
|
||||
CHECK(called == 2)
|
||||
end
|
||||
end)
|
||||
|
||||
|
@ -621,6 +770,128 @@ TEST("world:contains()", function()
|
|||
end)
|
||||
|
||||
TEST("world:delete()", function()
|
||||
do CASE "OnDelete cleanup policy cascades deletion to entites with idr_r pairs"
|
||||
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
|
||||
|
||||
do CASE "OnDeleteTarget works correctly regardless of adjacent archetype iteration order"
|
||||
local world = jecs.world()
|
||||
local t = world:entity()
|
||||
local c = world:component()
|
||||
world:add(c, t)
|
||||
|
||||
local component = world:component()
|
||||
local lifetime = world:component()
|
||||
|
||||
local tag = world:entity()
|
||||
local rel1 = world:entity()
|
||||
local rel2 = world:entity()
|
||||
local rel3 = world:entity()
|
||||
|
||||
local destroyed = false
|
||||
|
||||
world:removed(lifetime, function(e)
|
||||
destroyed = true
|
||||
end)
|
||||
|
||||
local parent = world:entity()
|
||||
world:set(parent, component, "foo")
|
||||
world:add(parent, jecs.pair(rel1, component))
|
||||
|
||||
local other1 = world:entity()
|
||||
world:add(other1, tag)
|
||||
world:add(other1, jecs.pair(jecs.ChildOf, parent))
|
||||
world:add(other1, jecs.pair(rel1, component))
|
||||
|
||||
local child = world:entity()
|
||||
world:set(child, lifetime, "")
|
||||
world:add(child, jecs.pair(jecs.ChildOf, parent))
|
||||
world:add(child, jecs.pair(rel3, parent))
|
||||
world:add(child, jecs.pair(rel2, other1))
|
||||
|
||||
world:delete(parent)
|
||||
|
||||
CHECK(destroyed)
|
||||
CHECK(not world:contains(child))
|
||||
end
|
||||
|
||||
do CASE "Should delete children in different archetypes if they have the same parent"
|
||||
local world = jecs.world()
|
||||
|
||||
local component = world:entity()
|
||||
|
||||
local parent = world:entity()
|
||||
|
||||
local child = world:entity()
|
||||
world:add(child, jecs.pair(jecs.ChildOf, parent))
|
||||
|
||||
local child2 = world:entity()
|
||||
world:add(child2, component) -- important, they need to be in different archetypes
|
||||
world:add(child2, jecs.pair(jecs.ChildOf, parent))
|
||||
|
||||
world:delete(parent)
|
||||
CHECK(not world:contains(child))
|
||||
CHECK(not world:contains(child2)) -- fails
|
||||
end
|
||||
do CASE "idr_t//delete_mask@3102..3108"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
world:add(A, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local B = world:component()
|
||||
local B_OnAdd_called = false
|
||||
local B_OnRemove_called = false
|
||||
world:set(B, jecs.OnAdd, function()
|
||||
B_OnAdd_called = true
|
||||
end)
|
||||
world:set(B, jecs.OnRemove, function()
|
||||
B_OnRemove_called = true
|
||||
end)
|
||||
|
||||
world:set(A, jecs.OnRemove, function(entity, id)
|
||||
world:set(entity, B, true)
|
||||
end)
|
||||
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
world:set(e2, pair(A, e1), true)
|
||||
|
||||
world:delete(e1)
|
||||
|
||||
CHECK(not world:has(e2, pair(A, e1)))
|
||||
CHECK(not world:has(e2, B))
|
||||
CHECK(not world:contains(e1))
|
||||
CHECK(not world:contains(e2))
|
||||
CHECK(B_OnAdd_called)
|
||||
-- CHECK(B_OnRemove_called)
|
||||
end
|
||||
|
||||
do CASE "idr_t//remove//on_remove//changed_archetype@3123..3126"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
world:set(A, jecs.OnRemove, function(entity, id)
|
||||
world:set(entity, B, true)
|
||||
end)
|
||||
|
||||
local e1 = world:entity()
|
||||
local e2 = world:entity()
|
||||
world:add(e2, pair(A, e2))
|
||||
world:set(e2, pair(A, e1), true)
|
||||
|
||||
world:delete(e1)
|
||||
|
||||
CHECK(not world:has(e2, pair(A, e1)))
|
||||
end
|
||||
|
||||
do CASE "pair(OnDelete, Delete)"
|
||||
local world = jecs.world()
|
||||
local ct = world:component()
|
||||
|
@ -1064,40 +1335,104 @@ 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 ""
|
||||
local world = jecs.world()
|
||||
local IsNearby = world:component()
|
||||
world:set(IsNearby, jecs.Name, "IsNearby")
|
||||
local person1, person2 = world:entity(), world:entity()
|
||||
|
||||
world:add(person2, jecs.pair(IsNearby, person1))
|
||||
local IsNearby_added_called = false
|
||||
world:added(IsNearby, function(...) -- This prints fine
|
||||
IsNearby_added_called = true
|
||||
end)
|
||||
local IsNearby_removed_called = false
|
||||
world:removed(IsNearby, function(...)
|
||||
IsNearby_removed_called = true
|
||||
end)
|
||||
|
||||
world:remove(person2, pair(IsNearby, person1))
|
||||
world:add(person2, pair(IsNearby, person1))
|
||||
world:remove(person2, pair(IsNearby, person1))
|
||||
|
||||
CHECK(IsNearby_added_called)
|
||||
CHECK(IsNearby_removed_called)
|
||||
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(), pair(A, B), 2)
|
||||
|
||||
local ran = false
|
||||
world:added(A, function()
|
||||
ran = true
|
||||
end)
|
||||
|
||||
local entity = world:entity()
|
||||
world:set(entity, A, 3)
|
||||
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)
|
||||
|
||||
local ran = false
|
||||
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))
|
||||
|
||||
local ran = false
|
||||
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()
|
||||
|
@ -1112,7 +1447,7 @@ TEST("world:range()", function()
|
|||
CHECK(world.entity_index.alive_count == 400)
|
||||
CHECK(e)
|
||||
end
|
||||
do CASE "axen"
|
||||
do CASE "entity ID reuse works correctly across different world ranges"
|
||||
local base = jecs.world()
|
||||
base:range(1_000, 2_000)
|
||||
|
||||
|
@ -1224,6 +1559,24 @@ TEST("world:range()", function()
|
|||
end)
|
||||
|
||||
TEST("world:entity()", function()
|
||||
do CASE "entity mirroring preserves IDs across world ranges"
|
||||
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)
|
||||
CHECK(world:contains(e0v1)) -- fails
|
||||
end
|
||||
|
||||
do CASE "component names persist after entity creation"
|
||||
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
|
||||
do CASE "desired id"
|
||||
local world = jecs.world()
|
||||
local id = world:entity()
|
||||
|
@ -1597,6 +1950,41 @@ TEST("world:query()", function()
|
|||
end
|
||||
end
|
||||
|
||||
do CASE "query more than 8 components"
|
||||
local world = jecs.world()
|
||||
local components = {}
|
||||
|
||||
for i = 1, 15 do
|
||||
local id = world:component()
|
||||
world:component() -- make the components sparsely interleaved
|
||||
components[i] = id
|
||||
end
|
||||
local e1 = world:entity()
|
||||
for i, id in components do
|
||||
world:set(e1, id, 13 ^ i)
|
||||
end
|
||||
|
||||
local q = world:query(unpack(components))
|
||||
|
||||
for entity, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o in q do
|
||||
CHECK(a == 13 ^ 1)
|
||||
CHECK(b == 13 ^ 2)
|
||||
CHECK(c == 13 ^ 3)
|
||||
CHECK(d == 13 ^ 4)
|
||||
CHECK(e == 13 ^ 5)
|
||||
CHECK(f == 13 ^ 6)
|
||||
CHECK(g == 13 ^ 7)
|
||||
CHECK(h == 13 ^ 8)
|
||||
CHECK(i == 13 ^ 9)
|
||||
CHECK(j == 13 ^ 10)
|
||||
CHECK(k == 13 ^ 11)
|
||||
CHECK(l == 13 ^ 12)
|
||||
CHECK(m == 13 ^ 13)
|
||||
CHECK(n == 13 ^ 14)
|
||||
CHECK(o == 13 ^ 15)
|
||||
end
|
||||
end
|
||||
|
||||
do CASE "should be able to get next results"
|
||||
local world = jecs.world()
|
||||
world:component()
|
||||
|
@ -1881,6 +2269,140 @@ TEST("world:remove()", function()
|
|||
end)
|
||||
|
||||
TEST("world:set()", function()
|
||||
do CASE "Removing exclusive pair should traverse backwards on edge"
|
||||
local world = jecs.world()
|
||||
local a = world:entity()
|
||||
local b = world:entity()
|
||||
local c = world:entity()
|
||||
|
||||
local BattleLink = world:component()
|
||||
world:add(BattleLink, jecs.Exclusive)
|
||||
|
||||
world:set(a, pair(BattleLink, b), {
|
||||
timestamp = 1,
|
||||
transform = vector.create(1, 2, 3)
|
||||
})
|
||||
world:set(a, pair(BattleLink, c), {
|
||||
timestamp = 2,
|
||||
transform = vector.create(1, 2, 3)
|
||||
})
|
||||
|
||||
CHECK(not world:has(a, pair(BattleLink, b)))
|
||||
CHECK(world:has(a, pair(BattleLink, c)))
|
||||
|
||||
world:remove(a, pair(BattleLink, c))
|
||||
|
||||
CHECK(not world:has(a, pair(BattleLink, b)))
|
||||
CHECK(not world:has(a, pair(BattleLink, c)))
|
||||
CHECK(not world:target(a, BattleLink))
|
||||
end
|
||||
|
||||
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:set(e, pair(A, B), true)
|
||||
world:set(e, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
|
||||
-- We have to test the path that checks the uncached method
|
||||
local e1 = world:entity()
|
||||
|
||||
world:set(e1, pair(A, B), true)
|
||||
world:set(e1, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e1, pair(A, B)) == false)
|
||||
CHECK(world:has(e1, 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:set(e, pair(A, B))
|
||||
CHECK(on_add_call_count == 1)
|
||||
world:set(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 "exclusive relations invoke on_remove hooks that should allow side effects"
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
world:add(A, jecs.Exclusive)
|
||||
local call_count = 0
|
||||
world:set(A, jecs.OnRemove, function(e, id)
|
||||
call_count += 1
|
||||
if call_count == 1 then
|
||||
world:set(e, C, true)
|
||||
else
|
||||
world:set(e, D, true)
|
||||
end
|
||||
end)
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, pair(A, B), true)
|
||||
world:set(e, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, C))
|
||||
|
||||
|
||||
-- We have to ensure that it actually invokes hooks everytime it
|
||||
-- traverses the archetype
|
||||
e = world:entity()
|
||||
world:set(e, pair(A, B), true)
|
||||
world:set(e, pair(A, C), true)
|
||||
|
||||
CHECK(world:has(e, pair(A, B)) == false)
|
||||
CHECK(world:has(e, pair(A, C)) == true)
|
||||
CHECK(world:has(e, D))
|
||||
end
|
||||
do CASE "archetype move"
|
||||
local world = jecs.world()
|
||||
|
||||
|
@ -2073,7 +2595,7 @@ TEST("#repro2", function()
|
|||
local entity = world:entity()
|
||||
world:set(entity, pair(Lifetime, Particle), 1)
|
||||
world:set(entity, pair(Lifetime, Beam), 2)
|
||||
world:set(entity, pair(4 :: any, 5 :: any), 6) -- noise
|
||||
world:set(entity, pair(world:component(), world:component()), 6) -- noise
|
||||
|
||||
CHECK(world:get(entity, pair(Lifetime, Particle)) == 1)
|
||||
CHECK(world:get(entity, pair(Lifetime, Beam)) == 2)
|
||||
|
@ -2162,7 +2684,6 @@ TEST("#repro", function()
|
|||
local types1 = { pair(Attacks, e1), pair(Eats, e1) }
|
||||
table.sort(types1)
|
||||
|
||||
|
||||
CHECK(d.tbl(e1).type == "")
|
||||
CHECK(d.tbl(e3).type == table.concat(types1, "_"))
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ukendio/jecs"
|
||||
version = "0.9.0-rc.7"
|
||||
version = "0.9.0-rc.12"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
license = "MIT"
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue