Merge branch 'types-dx' of https://github.com/alicesaidhi/jecs into alicesaidhi-types-dx

This commit is contained in:
Ukendio 2024-06-05 23:23:15 +02:00
commit 951c64a51c
29 changed files with 2051 additions and 4345 deletions

73
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,73 @@
name: Release
on:
push:
tags: ["v*"]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v3
- name: Install Aftman
uses: ok-nick/setup-aftman@v0.3.0
- name: Install Dependencies
run: wally install
- name: Build
run: rojo build --output build.rbxm default.project.json
- name: Upload Build Artifact
uses: actions/upload-artifact@v3
with:
name: build
path: build.rbxm
release:
name: Release
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Project
uses: actions/checkout@v3
- name: Download Jecs Build
uses: actions/download-artifact@v3
with:
name: build
path: build
- name: Rename Build
run: mv build/build.rbxm jecs.rbxm
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: Matter ${{ github.ref_name }}
body: |
Matter ${{ github.ref_name }} is now available!
files: |
jecs.rbxm
publish:
name: Publish
needs: [release]
runs-on: ubuntu-latest
steps:
- name: Checkout Project
uses: actions/checkout@v3
- name: Install Aftman
uses: ok-nick/setup-aftman@v0.3.0
- name: Wally Login
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
- name: Publish
run: wally publish

View file

@ -1,6 +1,7 @@
<p align="center"> <p align="center">
<img src="logo.png" /> <img src="jecs_darkmode.svg#gh-dark-mode-only" width=50%/>
<img src="jecs_lightmode.svg#gh-light-mode-only" width=50%/>
</p> </p>
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache-blue.svg?style=for-the-badge)](LICENSE-APACHE) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache-blue.svg?style=for-the-badge)](LICENSE-APACHE)
@ -10,44 +11,51 @@ Just an ECS
jecs is a stupidly fast Entity Component System (ECS). jecs is a stupidly fast Entity Component System (ECS).
- Process tens of thousands of entities with ease every frame - Entity Relationships as first class citizens
- Zero-dependency Luau package - Iterate 350,000 entities at 60 frames per second
- Type-safe [Luau](https://luau-lang.org/) API
- Zero-dependency package
- Optimized for column-major operations - Optimized for column-major operations
- Cache friendly archetype/SoA storage - Cache friendly archetype/SoA storage
- Unit tested for stability
### Example ### Example
```lua ```lua
local world = Jecs.World.new() local world = jecs.World.new()
local pair = jecs.pair
local Health = world:component() local ChildOf = world:component()
local Damage = world:component() local Name = world:component()
local Position = world:component()
local player = world:entity() local function parent(entity)
local opponent = world:entity() return world:target(entity, ChildOf)
end
world:set(player, Health, 100) local function getName(entity)
world:set(player, Damage, 8) return world:get(entity, Name)
world:set(player, Position, Vector3.new(0, 5, 0))
world:set(opponent, Health, 100)
world:set(opponent, Damage, 21)
world:set(opponent, Position, Vector3.new(0, 5, 3))
for playerId, playerPosition, health in world:query(Position, Health) do
local totalDamage = 0
for _, opponentPosition, damage in world:query(Position, Damage) do
if (playerPosition - opponentPosition).Magnitude < 5 then
totalDamage += damage
end
end
world:set(playerId, Health, health - totalDamage)
end end
assert(world:get(playerId, Health) == 79) local alice = world:entity()
assert(world:get(opponentId, Health) == 92) world:set(alice, Name, "alice")
local bob = world:entity()
world:add(bob, pair(ChildOf, alice))
world:set(bob, Name, "bob")
local sara = world:entity()
world:add(sara, pair(ChildOf, alice))
world:set(sara, Name, "sara")
print(getName(parent(sara)))
for e in world:query(pair(ChildOf, alice)) do
print(getName(e), "is the child of alice")
end
-- Output
-- "alice"
-- bob is the child of alice
-- sara is the child of alice
``` ```
125 archetypes, 4 random components queried. 125 archetypes, 4 random components queried.

View file

@ -1,6 +1,6 @@
[tools] [tools]
wally = "upliftgames/wally@0.3.1" wally = "upliftgames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.4.1" rojo = "rojo-rbx/rojo@7.4.1"
stylua = "johnnymorganz/stylua@0.19.1" stylua = "johnnymorganz/stylua@0.19.1"
selene = "kampfkarren/selene@0.26.1" selene = "kampfkarren/selene@0.26.1"
wally-patch-package="Barocena/wally-patch-package@1.2.1" wally-patch-package = "Barocena/wally-patch-package@1.2.1"

31
bench.project.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "jecs-test",
"tree": {
"$className": "DataModel",
"StarterPlayer": {
"$className": "StarterPlayer",
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"$path": "tests"
}
},
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Lib": {
"$path": "lib"
},
"rgb": {
"$path": "rgb.lua"
},
"benches": {
"$path": "benches"
},
"mirror": {
"$path": "mirror"
},
"DevPackages": {
"$path": "benches/visual/DevPackages"
}
}
}
}

View file

@ -1,299 +1,246 @@
--!optimize 2 --!optimize 2
--!native --!native
local testkit = require('../testkit') local testkit = require("../testkit")
local BENCH, START = testkit.benchmark() local BENCH, START = testkit.benchmark()
local function TITLE(title: string) local function TITLE(title: string)
print() print()
print(testkit.color.white(title)) print(testkit.color.white(title))
end end
local jecs = require("../mirror/init") local jecs = require("../lib/init")
local mirror = require("../mirror/init")
local oldMatter = require("../oldMatter")
local newMatter = require("../newMatter")
type i53 = number type i53 = number
do TITLE (testkit.color.white_underline("Jecs query")) do
local ecs = jecs.World.new() TITLE(testkit.color.white_underline("Jecs query"))
do TITLE "one component in common" local ecs = jecs.World.new()
do
TITLE("one component in common")
local function view_bench( local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
world: jecs.World, BENCH("1 component", function()
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53 for _ in world:query(A) do
) end
end)
BENCH("1 component", function() BENCH("2 component", function()
for _ in world:query(A) do end for _ in world:query(A, B) do
end) end
end)
BENCH("2 component", function() BENCH("4 component", function()
for _ in world:query(A, B) do end for _ in world:query(A, B, C, D) do
end) end
end)
BENCH("4 component", function() BENCH("8 component", function()
for _ in world:query(A, B, C, D) do for _ in world:query(A, B, C, D, E, F, G, H) do
end end
end) end)
BENCH("8 component", function() local e = world:entity()
for _ in world:query(A, B, C, D, E, F, G, H) do end world:set(e, A, true)
end) world:set(e, B, true)
end world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
local D1 = ecs:component() BENCH("Update Data", function()
local D2 = ecs:component() for _ = 1, 100 do
local D3 = ecs:component() world:set(e, A, false)
local D4 = ecs:component() world:set(e, B, false)
local D5 = ecs:component() world:set(e, C, false)
local D6 = ecs:component() world:set(e, D, false)
local D7 = ecs:component() world:set(e, E, false)
local D8 = ecs:component() world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
local function flip() local D1 = ecs:component()
return math.random() >= 0.15 local D2 = ecs:component()
end local D3 = ecs:component()
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local added = 0 local function flip()
return math.random() >= 0.15
end
local added = 0
local archetypes = {} local archetypes = {}
for i = 1, 2^16-2 do for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity() local entity = ecs:entity()
local combination = "" local combination = ""
if flip() then if flip() then
combination ..= "B" combination ..= "B"
ecs:set(entity, D2, {value = true}) ecs:set(entity, D2, {value = true})
end end
if flip() then if flip() then
combination ..= "C" combination ..= "C"
ecs:set(entity, D3, { value = true }) ecs:set(entity, D3, {value = true})
end end
if flip() then if flip() then
combination ..= "D" combination ..= "D"
ecs:set(entity, D4, { value = true}) ecs:set(entity, D4, {value = true})
end end
if flip() then if flip() then
combination ..= "E" combination ..= "E"
ecs:set(entity, D5, { value = true}) ecs:set(entity, D5, {value = true})
end end
if flip() then if flip() then
combination ..= "F" combination ..= "F"
ecs:set(entity, D6, {value = true}) ecs:set(entity, D6, {value = true})
end end
if flip() then if flip() then
combination ..= "G" combination ..= "G"
ecs:set(entity, D7, { value = true}) ecs:set(entity, D7, {value = true})
end end
if flip() then if flip() then
combination ..= "H" combination ..= "H"
ecs:set(entity, D8, {value = true}) ecs:set(entity, D8, {value = true})
end
end if #combination == 7 then
added += 1
if #combination == 7 then ecs:set(entity, D1, {value = true})
added += 1 end
ecs:set(entity, D1, { value = true})
end
archetypes[combination] = true archetypes[combination] = true
end end
local a = 0 local a = 0
for _ in archetypes do a+= 1 end for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end end
end end
do TITLE(testkit.color.white_underline("OldMatter query")) do
TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new()
do
TITLE("one component in common")
local ecs = oldMatter.World.new() local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
local component = oldMatter.component BENCH("1 component", function()
for _ in world:query(A) do
end
end)
do TITLE "one component in common" BENCH("2 component", function()
local function view_bench( for _ in world:query(A, B) do
world: jecs.World, end
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53 end)
)
BENCH("1 component", function() BENCH("4 component", function()
for _ in world:query(A) do end for _ in world:query(A, B, C, D) do
end) end
end)
BENCH("2 component", function() BENCH("8 component", function()
for _ in world:query(A, B) do end for _ in world:query(A, B, C, D, E, F, G, H) do
end) end
end)
BENCH("4 component", function() local e = world:entity()
for _ in world:query(A, B, C, D) do world:set(e, A, true)
end world:set(e, B, true)
end) world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
BENCH("8 component", function() BENCH("Update Data", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end for _ = 1, 100 do
end) world:set(e, A, false)
end world:set(e, B, false)
world:set(e, C, false)
world:set(e, D, false)
world:set(e, E, false)
world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
local D1 = component() local D1 = ecs:component()
local D2 = component() local D2 = ecs:component()
local D3 = component() local D3 = ecs:component()
local D4 = component() local D4 = ecs:component()
local D5 = component() local D5 = ecs:component()
local D6 = component() local D6 = ecs:component()
local D7 = component() local D7 = ecs:component()
local D8 = component() local D8 = ecs:component()
local function flip() local function flip()
return math.random() >= 0.15 return math.random() >= 0.15
end end
local added = 0 local added = 0
local archetypes = {} local archetypes = {}
for i = 1, 2^16-2 do for i = 1, 2 ^ 16 - 2 do
local entity = ecs:spawn() local entity = ecs:entity()
local combination = "" local combination = ""
if flip() then if flip() then
combination ..= "B" combination ..= "B"
ecs:insert(entity, D2({value = true})) ecs:set(entity, D2, {value = true})
end end
if flip() then if flip() then
combination ..= "C" combination ..= "C"
ecs:insert(entity, D3({value = true})) ecs:set(entity, D3, {value = true})
end end
if flip() then if flip() then
combination ..= "D" combination ..= "D"
ecs:insert(entity, D4({value = true})) ecs:set(entity, D4, {value = true})
end end
if flip() then if flip() then
combination ..= "E" combination ..= "E"
ecs:insert(entity, D5({value = true})) ecs:set(entity, D5, {value = true})
end end
if flip() then if flip() then
combination ..= "F" combination ..= "F"
ecs:insert(entity, D6({value = true})) ecs:set(entity, D6, {value = true})
end
if flip() then
combination ..= "G"
ecs:set(entity, D7, {value = true})
end
if flip() then
combination ..= "H"
ecs:set(entity, D8, {value = true})
end
end if #combination == 7 then
if flip() then added += 1
combination ..= "G" ecs:set(entity, D1, {value = true})
ecs:insert(entity, D7({value = true})) end
end
if flip() then
combination ..= "H"
ecs:insert(entity, D8({value = true}))
end
if #combination == 7 then
added += 1
ecs:insert(entity, D1({value = true}))
end
archetypes[combination] = true archetypes[combination] = true
end end
local a = 0 local a = 0
for _ in archetypes do a+= 1 end for _ in archetypes do
a += 1
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) end
end
end
do TITLE(testkit.color.white_underline("NewMatter query"))
local ecs = newMatter.World.new()
local component = newMatter.component
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
)
BENCH("1 component", function()
for _ in world:query(A) do end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
end)
BENCH("4 component", function()
for _ in world:query(A, B, C, D) do
end
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
end)
end
local D1 = component()
local D2 = component()
local D3 = component()
local D4 = component()
local D5 = component()
local D6 = component()
local D7 = component()
local D8 = component()
local function flip()
return math.random() >= 0.15
end
local added = 0
local archetypes = {}
for i = 1, 2^16-2 do
local entity = ecs:spawn()
local combination = ""
if flip() then
combination ..= "B"
ecs:insert(entity, D2({value = true}))
end
if flip() then
combination ..= "C"
ecs:insert(entity, D3({value = true}))
end
if flip() then
combination ..= "D"
ecs:insert(entity, D4({value = true}))
end
if flip() then
combination ..= "E"
ecs:insert(entity, D5({value = true}))
end
if flip() then
combination ..= "F"
ecs:insert(entity, D6({value = true}))
end
if flip() then
combination ..= "G"
ecs:insert(entity, D7({value = true}))
end
if flip() then
combination ..= "H"
ecs:insert(entity, D8({value = true}))
end
if #combination == 7 then
added += 1
ecs:insert(entity, D1({value = true}))
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do a+= 1 end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end end

View file

@ -2,12 +2,13 @@
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local rgb = require(ReplicatedStorage.rgb)
local Matter = require(ReplicatedStorage.DevPackages.Matter) local Matter = require(ReplicatedStorage.DevPackages.Matter)
local jecs = require(ReplicatedStorage.Lib) local jecs = require(ReplicatedStorage.Lib)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local newWorld = Matter.World.new() local newWorld = Matter.World.new()
local ecs = jecs.World.new() local ecs = jecs.World.new()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local A1 = Matter.component() local A1 = Matter.component()
local A2 = Matter.component() local A2 = Matter.component()
@ -35,6 +36,15 @@ local C5 = ecs:entity()
local C6 = ecs:entity() local C6 = ecs:entity()
local C7 = ecs:entity() local C7 = ecs:entity()
local C8 = ecs:entity() local C8 = ecs:entity()
local E1 = mcs:entity()
local E2 = mcs:entity()
local E3 = mcs:entity()
local E4 = mcs:entity()
local E5 = mcs:entity()
local E6 = mcs:entity()
local E7 = mcs:entity()
local E8 = mcs:entity()
local registry2 = ecr.registry() local registry2 = ecr.registry()
return { return {
@ -44,7 +54,7 @@ return {
Functions = { Functions = {
Matter = function() Matter = function()
for i = 1, 50 do for i = 1, 500 do
newWorld:spawn( newWorld:spawn(
A1({ value = true }), A1({ value = true }),
A2({ value = true }), A2({ value = true }),
@ -60,8 +70,8 @@ return {
ECR = function() ECR = function()
for i = 1, 50 do local e = registry2.create()
local e = registry2.create() for i = 1, 500 do
registry2:set(e, B1, {value = false}) registry2:set(e, B1, {value = false})
registry2:set(e, B2, {value = false}) registry2:set(e, B2, {value = false})
registry2:set(e, B3, {value = false}) registry2:set(e, B3, {value = false})
@ -78,7 +88,7 @@ return {
local e = ecs:entity() local e = ecs:entity()
for i = 1, 50 do for i = 1, 500 do
ecs:set(e, C1, {value = false}) ecs:set(e, C1, {value = false})
ecs:set(e, C2, {value = false}) ecs:set(e, C2, {value = false})
@ -89,6 +99,23 @@ return {
ecs:set(e, C7, {value = false}) ecs:set(e, C7, {value = false})
ecs:set(e, C8, {value = false}) ecs:set(e, C8, {value = false})
end
end,
Mirror = function()
local e = ecs:entity()
for i = 1, 500 do
mcs:set(e, E1, {value = false})
mcs:set(e, E2, {value = false})
mcs:set(e, E3, {value = false})
mcs:set(e, E4, {value = false})
mcs:set(e, E5, {value = false})
mcs:set(e, E6, {value = false})
mcs:set(e, E7, {value = false})
mcs:set(e, E8, {value = false})
end end
end end

View file

@ -2,41 +2,37 @@
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local rgb = require(ReplicatedStorage.rgb)
local Matter = require(ReplicatedStorage.DevPackages.Matter) local Matter = require(ReplicatedStorage.DevPackages.Matter)
local jecs = require(ReplicatedStorage.Lib)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local rgb = require(ReplicatedStorage.rgb)
local newWorld = Matter.World.new() local newWorld = Matter.World.new()
local ecs = jecs.World.new() local ecs = jecs.World.new()
return { return {
ParameterGenerator = function() ParameterGenerator = function()
local registry2 = ecr.registry() local registry2 = ecr.registry()
return registry2 return registry2
end, end;
Functions = { Functions = {
Matter = function() Matter = function()
for i = 1, 1000 do for i = 1, 1000 do
newWorld:spawn() newWorld:spawn()
end end
end, end;
ECR = function(_, registry2)
for i = 1, 1000 do
registry2.create()
end
end;
ECR = function(_, registry2) Jecs = function()
for i = 1, 1000 do for i = 1, 1000 do
registry2.create() ecs:entity()
end end
end, end;
};
Jecs = function()
for i = 1, 1000 do
ecs:entity()
end
end
},
} }

11
benches/visual/wally.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "private/private"
version = "0.1.0-rc.6"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
exclude = ["**"]
[dev-dependencies]
Matter = "matter-ecs/matter@0.8.0"
ecr = "centau/ecr@0.8.0"

45
docs/api-types.md Normal file
View file

@ -0,0 +1,45 @@
# World
A World contains all ECS data
Games can have multiple worlds, although typically only one is necessary. These worlds are isolated from each other, meaning they donot share the same entities nor component IDs.
---
# Entity
An unique id.
Entities consist out of a number unique to the entity in the lower 32 bits, and a counter used to track entity liveliness in the upper 32 bits. When an id is recycled, its generation count is increased. This causes recycled ids to be very large (>4 billion), which is normal.
---
# QueryIter
A result from the `World:query` function.
Queries are used to iterate over entities that match against the set collection of components.
Calling it in a loop will allow iteration over the results.
```lua
for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
-- Do something
end
```
### QueryIter.without
QueryIter.without(iter: QueryIter
...: [Entity](#Entity)): QueryIter
Create a new Query Iterator from the filter
#### Parameters
world The world.
... The collection of components to filter archetypes against.
#### Returns
The new query iterator.

187
docs/api/world.md Normal file
View file

@ -0,0 +1,187 @@
# World
### World.new
World.new(): [World](../api-types.md#World)
Create a new world.
#### Returns
A new world
---
### World.entity
World.entity(world: [World](../api-types.md#World)): [Entity](../api-types.md#Entity)
Creates an entity in the world.
#### Returns
A new entiity id
---
### World.target
World.target(world: [World](../api-types.md#World),
entity: [Entity](../api-types.md#Entity),
rel: [Entity](../api-types.md#Entity)): [Entity](../api-types.md#Entity)
Get the target of a relationship.
This will return a target (second element of a pair) of the entity for the specified relationship.
#### Parameters
world The world.
entity The entity.
rel The relationship between the entity and the target.
#### Returns
The first target for the relationship
---
### World.add
World.add(world: [World](../api-types.md#World),
entity: [Entity](../api-types.md#Entity),
id: [Entity](../api-types.md#Entity)): [Entity](..#api-types.md#Entity)
Add a (component) id to an entity.
This operation adds a single (component) id to an entity.
If the entity already has the id, this operation will have no side effects.
#### Parameters
world The world.
entity The entity.
id The id to add.
---
### World.remove
World.remove(world: [World](../api-types#World),
entity: [Entity](../api-types#Entity),
id: [Entity](../api-types#Entity)): [Entity](../api-types#Entity)
Remove a (component) id to an entity.
This operation removes a single (component) id to an entity.
If the entity already has the id, this operation will have no side effects.
#### Parameters
world The world.
entity The entity.
id The id to add.
---
### World.get
World.get(world: [World](../api-types.md#World),
entity: [Entity](../api-types.md#Entity),
id: [Entity](../api-types.md#Entity)): any
Gets the component data.
#### Parameters
world The world.
entity The entity.
id The id of component to get.
#### Returns
The component data, nil if the entity does not have the componnet.
---
### World.set
World.set(world: [World](../api-types.md#World),
entity: [Entity](../api-types.md#Entity),
id: [Entity](../api-types.md#Entity)
data: any)
Set the value of a component.
#### Parameters
world The world.
entity The entity.
id The id of the componment set.
data The data to the component.
---
### World.query
World.query(world: [World](../api-types.md#World),
...: [Entity](../api-types.mdEntity)): [QueryIter](../api-types.md#QueryIter)
Create a QueryIter from the list of filters.
#### Parameters
world The world.
... The collection of components to match entities against.
#### Returns
The query iterator.
---
# Pair
### pair
pair(first: [Entity](../api-types#Entity), second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity)
Creates a composite key.
#### Parameters
first The first element.
second The second element.
#### Returns
The pair of the two elements
---
### IS_PAIR
jecs.IS_PAIR(id: [Entity](../api-types#Entity)): boolean
Creates a composite key.
#### Parameters
id The id to check.
#### Returns
If id is a pair.
---
# Constants
### OnAdd
---
### OnRemove
---
### Rest
---
### OnSet
---
### Wildcard
Matches any id, returns all matches.

View file

@ -0,0 +1,19 @@
# Getting Started
This section will provide a walk through setting up your development environment and a quick overview of the different features and concepts in Jecs with short examples.
## Installing Jecs
To use Jecs, you will need to add the library to your project's source folder.
## Installing as standalone
Head over to the [Releases](https://github.com/ukendio/jecs/releases/latest) page and install the rbxm file.
![jecs.rbxm](rbxm.png)
## Installing with Wally
Jecs is available as a package on [wally.run](https://wally.run/package/ukendio/jecs)
Add it to your project's Wally.toml like this:
```toml
[dependencies]
jecs = "0.1.0" # Make sure this is the latest version
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

6
jecs_darkmode.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="white"/>
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

6
jecs_lightmode.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="black"/>
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,321 +0,0 @@
--!strict
local jecs = require(script.Parent)
local world = jecs.World.new()
type Component<T> = jecs.Component<T>
local A, B, C, D = world:entity(), world:entity(), world:entity(), world:entity()
local E, F, G, H = world:entity(), world:entity(), world:entity(), world:entity()
print("A", A)
print("B", B)
print("C", C)
print("D", D)
print("E", E)
print("F", F)
print("G", G)
print("H", H)
local common = 0
local N = 2^16-2
local archetypes = {}
local function flip()
return math.random() >= 0.5
end
local amountOfCombination = 0
for i = 1, N do
local entity = world:entity()
local combination = ""
if flip() then
combination ..= "2_"
world:set(entity, B, { value = true})
end
if flip() then
combination ..= "3_"
world:set(entity, C, { value = true})
end
if flip() then
combination ..= "4_"
world:set(entity, D, { value = true})
end
if flip() then
combination ..= "5_"
world:set(entity, E, { value = true})
end
if flip() then
combination ..= "6_"
world:set(entity, F, { value = true})
end
if flip() then
combination ..= "7_"
world:set(entity, G, { value = true})
end
if flip() then
combination ..= "8"
world:set(entity, H, { value = true})
end
if #combination == 7 then
combination = "1_" .. combination
common += 1
world:set(entity, A, { value = true})
end
if combination:find("2")
and combination:find("3")
and combination:find("4")
and combination:find("6")
then
amountOfCombination += 1
end
archetypes[combination] = true
end
return function()
describe("World", function()
it("should add component", function()
local id = world:entity()
world:set(id, A, true)
world:set(id, B, 1)
local s = world:component() :: jecs.Component<boolean>
world:set(id, s, true)
local id1 = world:entity()
world:set(id1, A, "hello")
expect(world:get(id, A)).to.equal(true)
expect(world:get(id, B)).to.equal(1)
expect(world:get(id1, A)).to.equal("hello")
end)
it("should remove component", function()
local Tag = world:entity()
local entities = {}
for i = 1, 10 do
local entity = world:entity()
entities[i] = entity
world:set(entity, Tag)
end
for i = 1, 10 do
local entity = entities[i]
expect(world:get(entity, Tag)).to.equal(nil)
world:remove(entity, Tag)
end
end)
it("should override component data", function()
local id = world:entity()
world:set(id, A, true)
expect(world:get(id, A)).to.equal(true)
world:set(id, A, false)
expect(world:get(id, A)).to.equal(false)
end)
it("should not query a removed component", function()
local Tag = (world:entity() :: any) :: jecs.Component<nil>
local AnotherTag = world:entity()
local entity = world:entity()
world:set(entity, Tag)
world:set(entity, AnotherTag)
world:remove(entity, AnotherTag)
local added = 0
for e, t in world:query(Tag) do
added += 1
end
expect(added).to.equal(0)
end)
it("should query correct number of compatible archetypes", function()
local added = 0
for _ in world:query(B, C, D, F) do
added += 1
end
expect(added).to.equal(amountOfCombination)
end)
it("should not query poisoned players", function()
local Player = world:entity()
local Health = world:entity()
local Poison = world:entity()
local one = world:entity()
world:set(one, Player, { name = "alice"})
world:set(one, Health, 100)
world:set(one, Poison)
local two = world:entity()
world:set(two, Player, { name = "bob"})
world:set(two, Health, 90)
local withoutCount = 0
for _id, _player in world:query(Player):without(Poison) do
withoutCount += 1
end
expect(withoutCount).to.equal(1)
end)
it("should allow calling world:entity before world:component", function()
for _ = 1, 256 do
world:entity()
end
expect(world:component()).to.be.ok()
end)
it("should skip iteration", function()
local Position, Velocity = world:entity(), world:entity()
local e = world:entity()
world:set(e, Position, Vector3.zero)
world:set(e, Velocity, Vector3.one)
local added = 0
for i in world:query(Position):without(Velocity) do
added += 1
end
expect(added).to.equal(0)
end)
it("track changes", function()
local Position = world:entity()
local moving = world:entity()
world:set(moving, Position, Vector3.new(1, 2, 3))
local count = 0
for e, position in world:observer(Position).event(jecs.ON_ADD) do
count += 1
expect(e).to.equal(moving)
expect(position).to.equal(Vector3.new(1, 2, 3))
end
expect(count).to.equal(1)
end)
it("should query all matching entities", function()
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i > 5 then world:set(id, B, true) end
entities[i] = id
end
for id in world:query(A) do
local i = table.find(entities, id)
expect(i).to.be.ok()
table.remove(entities, i)
end
expect(#entities).to.equal(0)
end)
it("should query all matching entities when irrelevant component is removed", function()
local world = jecs.World.new()
local A = world:component()
local B = world:component() :: jecs.Component<true>
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
world:set(id, B, true)
if i > 5 then world:remove(id, B) end
entities[i] = id
end
local added = 0
for id in world:query(A) do
added += 1
local i = table.find(entities, id)
expect(i).to.be.ok()
table.remove(entities, i)
end
expect(added).to.equal(N)
end)
it("should query all entities without B", function()
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i < 5 then
entities[i] = id
else
world:set(id, B, true)
end
end
for id in world:query(A):without(B) do
local i = table.find(entities, id)
expect(i).to.be.ok()
table.remove(entities, i)
end
expect(#entities).to.equal(0)
end)
it("should allow setting components in arbitrary order", function()
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
expect(world:get(id, Poison)).to.equal(5)
end)
it("Should allow deleting components", function()
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
world:delete(id)
expect(world:get(id, Poison)).to.never.be.ok()
expect(world:get(id, Health)).to.never.be.ok()
end)
it("try types", function()
local test = world:component() :: Component<Vector3>
for id, t in world:query(test) do
print(t)
end
end)
end)
end

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -6,10 +6,10 @@
type i53 = number type i53 = number
type i24 = number type i24 = number
type Ty = { i53 } type Ty = {i53}
type ArchetypeId = number type ArchetypeId = number
type Column = { any } type Column = {any}
type Archetype = { type Archetype = {
id: number, id: number,
@ -20,9 +20,9 @@ type Archetype = {
}, },
}, },
types: Ty, types: Ty,
type: string | number, type: string | number,
entities: { number }, entities: {number},
columns: { Column }, columns: {Column},
records: {}, records: {},
} }
@ -31,13 +31,13 @@ type Record = {
row: number, row: number,
} }
type EntityIndex = { [i24]: Record } type EntityIndex = {[i24]: Record}
type ComponentIndex = { [i24]: ArchetypeMap} type ComponentIndex = {[i24]: ArchetypeMap}
type ArchetypeRecord = number type ArchetypeRecord = number
type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number } type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
type Archetypes = { [ArchetypeId]: Archetype } type Archetypes = {[ArchetypeId]: Archetype}
type ArchetypeDiff = { type ArchetypeDiff = {
added: Ty, added: Ty,
removed: Ty, removed: Ty,
@ -51,38 +51,58 @@ local REST = HI_COMPONENT_ID + 4
local function transitionArchetype( local function transitionArchetype(
entityIndex: EntityIndex, entityIndex: EntityIndex,
destinationArchetype: Archetype, to: Archetype,
destinationRow: i24, destinationRow: i24,
sourceArchetype: Archetype, from: Archetype,
sourceRow: i24 sourceRow: i24
) )
local columns = sourceArchetype.columns local columns = from.columns
local sourceEntities = sourceArchetype.entities local sourceEntities = from.entities
local destinationEntities = destinationArchetype.entities local destinationEntities = to.entities
local destinationColumns = destinationArchetype.columns local destinationColumns = to.columns
local tr = to.records
local types = from.types
for componentId, column in columns do for i, column in columns do
local targetColumn = destinationColumns[componentId] -- Retrieves the new column index from the source archetype's record from each component
if targetColumn then -- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local targetColumn = destinationColumns[tr[types[i]]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if targetColumn then
targetColumn[destinationRow] = column[sourceRow] targetColumn[destinationRow] = column[sourceRow]
end end
column[sourceRow] = column[#column] -- If the entity is the last row in the archetype then swapping it would be meaningless.
column[#column] = nil local last = #column
if sourceRow ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype.
column[sourceRow] = column[last]
end
column[last] = nil
end end
destinationEntities[destinationRow] = sourceEntities[sourceRow] -- Move the entity from the source to the destination archetype.
entityIndex[sourceEntities[sourceRow]].row = destinationRow local atSourceRow = sourceEntities[sourceRow]
destinationEntities[destinationRow] = atSourceRow
entityIndex[atSourceRow].row = destinationRow
-- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped.
local movedAway = #sourceEntities local movedAway = #sourceEntities
sourceEntities[sourceRow] = sourceEntities[movedAway] if sourceRow ~= movedAway then
entityIndex[sourceEntities[movedAway]].row = sourceRow local atMovedAway = sourceEntities[movedAway]
sourceEntities[sourceRow] = atMovedAway
entityIndex[atMovedAway].row = sourceRow
end
sourceEntities[movedAway] = nil sourceEntities[movedAway] = nil
end end
local function archetypeAppend(entity: i53, archetype: Archetype): i24 local function archetypeAppend(entity: number, archetype: Archetype): number
local entities = archetype.entities local entities = archetype.entities
table.insert(entities, entity) local length = #entities + 1
return #entities entities[length] = entity
return length
end end
local function newEntity(entityId: i53, record: Record, archetype: Archetype) local function newEntity(entityId: i53, record: Record, archetype: Archetype)
@ -105,102 +125,104 @@ local function hash(arr): string | number
return table.concat(arr, "_") return table.concat(arr, "_")
end end
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?) local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
local destinationCount = #to.types
local destinationIds = to.types local destinationIds = to.types
local records = to.records
local id = to.id
for i = 1, destinationCount do for i, destinationId in destinationIds do
local destinationId = destinationIds[i] local archetypesMap = componentIndex[destinationId]
if not componentIndex[destinationId] then if not archetypesMap then
componentIndex[destinationId] = { size = 0, sparse = {} } archetypesMap = {size = 0, sparse = {}}
componentIndex[destinationId] = archetypesMap
end end
local archetypesMap = componentIndex[destinationId] archetypesMap.sparse[id] = i
archetypesMap.sparse[to.id] = i records[destinationId] = i
to.records[destinationId] = i
end end
end end
local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
local ty = hash(types) local ty = hash(types)
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1 local id = world.nextArchetypeId + 1
local id = world.nextArchetypeId world.nextArchetypeId = id
local columns = {} :: { any } local length = #types
local columns = table.create(length) :: {any}
for _ in types do for index in types do
table.insert(columns, {}) columns[index] = {}
end end
local archetype = { local archetype = {
id = id, columns = columns;
types = types, edges = {};
type = ty, entities = {};
columns = columns, id = id;
entities = {}, records = {};
edges = {}, type = ty;
records = {}, types = types;
} }
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype world.archetypes[id] = archetype
createArchetypeRecords(world.componentIndex, archetype, prev) if length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev)
end
return archetype return archetype
end end
local World = {} local World = {}
World.__index = World World.__index = World
function World.new() function World.new()
local self = setmetatable({ local self = setmetatable({
entityIndex = {}, archetypeIndex = {};
componentIndex = {}, archetypes = {};
archetypes = {}, componentIndex = {};
archetypeIndex = {}, entityIndex = {};
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
nextEntityId = 0,
nextComponentId = 0,
nextArchetypeId = 0,
hooks = { hooks = {
[ON_ADD] = {} [ON_ADD] = {};
} };
nextArchetypeId = 0;
nextComponentId = 0;
nextEntityId = 0;
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World) }, World)
return self return self
end end
local function emit(world, eventDescription) local function emit(world, eventDescription)
local event = eventDescription.event local event = eventDescription.event
table.insert(world.hooks[event], { table.insert(world.hooks[event], {
ids = eventDescription.ids, archetype = eventDescription.archetype;
archetype = eventDescription.archetype, ids = eventDescription.ids;
otherArchetype = eventDescription.otherArchetype, offset = eventDescription.offset;
offset = eventDescription.offset otherArchetype = eventDescription.otherArchetype;
}) })
end end
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
if #added > 0 then
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
if #added > 0 then
emit(world, { emit(world, {
event = ON_ADD, archetype = archetype;
ids = added, event = ON_ADD;
archetype = archetype, ids = added;
otherArchetype = otherArchetype, offset = row;
offset = row, otherArchetype = otherArchetype;
}) })
end end
end end
export type World = typeof(World.new()) export type World = typeof(World.new())
local function ensureArchetype(world: World, types, prev) local function ensureArchetype(world: World, types, prev)
if #types < 1 then if #types < 1 then
return world.ROOT_ARCHETYPE return world.ROOT_ARCHETYPE
end end
local ty = hash(types) local ty = hash(types)
local archetype = world.archetypeIndex[ty] local archetype = world.archetypeIndex[ty]
if archetype then if archetype then
@ -210,10 +232,8 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev) return archetypeOf(world, types, prev)
end end
local function findInsert(types: { i53 }, toAdd: i53) local function findInsert(types: {i53}, toAdd: i53)
local count = #types for i, id in types do
for i = 1, count do
local id = types[i]
if id == toAdd then if id == toAdd then
return -1 return -1
end end
@ -221,13 +241,18 @@ local function findInsert(types: { i53 }, toAdd: i53)
return i return i
end end
end end
return count + 1 return #types + 1
end end
local function findArchetypeWith(world: World, node: Archetype, componentId: i53) local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
local types = node.types local types = node.types
-- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
local at = findInsert(types, componentId) local at = findInsert(types, componentId)
if at == -1 then if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype.
return node return node
end end
@ -237,88 +262,108 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
end end
local function ensureEdge(archetype: Archetype, componentId: i53) local function ensureEdge(archetype: Archetype, componentId: i53)
if not archetype.edges[componentId] then local edges = archetype.edges
archetype.edges[componentId] = {} :: any local edge = edges[componentId]
if not edge then
edge = {} :: any
edges[componentId] = edge
end end
return archetype.edges[componentId] return edge
end end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
if not from then if not from then
if not world.ROOT_ARCHETYPE then -- If there was no source archetype then it should return the ROOT_ARCHETYPE
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil) local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE if not ROOT_ARCHETYPE then
end ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
from = world.ROOT_ARCHETYPE world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
end
from = ROOT_ARCHETYPE
end end
local edge = ensureEdge(from, componentId) local edge = ensureEdge(from, componentId)
local add = edge.add
if not edge.add then if not add then
edge.add = findArchetypeWith(world, from, componentId) -- Save an edge using the component ID to the archetype to allow
-- faster traversals to adjacent archetypes.
add = findArchetypeWith(world, from, componentId)
edge.add = add :: never
end end
return edge.add return add
end end
local function ensureRecord(entityIndex, entityId: i53): Record local function ensureRecord(entityIndex, entityId: i53): Record
local id = entityId local record = entityIndex[entityId]
if not entityIndex[id] then
entityIndex[id] = {} if not record then
record = {}
entityIndex[entityId] = record
end end
return entityIndex[id] :: Record
return record :: Record
end end
function World.set(world: World, entityId: i53, componentId: i53, data: unknown) function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = ensureRecord(world.entityIndex, entityId) local record = ensureRecord(world.entityIndex, entityId)
local sourceArchetype = record.archetype local from = record.archetype
local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype) local to = archetypeTraverseAdd(world, componentId, from)
if sourceArchetype == destinationArchetype then if from == to then
local archetypeRecord = destinationArchetype.records[componentId] -- If the archetypes are the same it can avoid moving the entity
destinationArchetype.columns[archetypeRecord][record.row] = data -- and just set the data directly.
local archetypeRecord = to.records[componentId]
from.columns[archetypeRecord][record.row] = data
-- Should fire an OnSet event here.
return return
end end
if sourceArchetype then if from then
moveEntity(world.entityIndex, entityId, record, destinationArchetype) -- If there was a previous archetype, then the entity needs to move the archetype
moveEntity(world.entityIndex, entityId, record, to)
else else
if #destinationArchetype.types > 0 then if #to.types > 0 then
newEntity(entityId, record, destinationArchetype) -- When there is no previous archetype it should create the archetype
onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId }) newEntity(entityId, record, to)
onNotifyAdd(world, to, from, record.row, {componentId})
end end
end end
local archetypeRecord = destinationArchetype.records[componentId] local archetypeRecord = to.records[componentId]
destinationArchetype.columns[archetypeRecord][record.row] = data to.columns[archetypeRecord][record.row] = data
end end
local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
local edge = ensureEdge(from, componentId) local edge = ensureEdge(from, componentId)
local remove = edge.remove
if not edge.remove then if not remove then
local to = table.clone(from.types) local to = table.clone(from.types)
table.remove(to, table.find(to, componentId)) table.remove(to, table.find(to, componentId))
edge.remove = ensureArchetype(world, to, from) remove = ensureArchetype(world, to, from)
edge.remove = remove :: never
end end
return edge.remove return remove
end end
function World.remove(world: World, entityId: i53, componentId: i53) function World.remove(world: World, entityId: i53, componentId: i53)
local record = ensureRecord(world.entityIndex, entityId) local entityIndex = world.entityIndex
local record = ensureRecord(entityIndex, entityId)
local sourceArchetype = record.archetype local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then if sourceArchetype and not (sourceArchetype == destinationArchetype) then
moveEntity(world.entityIndex, entityId, record, destinationArchetype) moveEntity(entityIndex, entityId, record, destinationArchetype)
end end
end end
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24) -- Keeping the function as small as possible to enable inlining
local function get(record: Record, componentId: i24)
local archetype = record.archetype local archetype = record.archetype
local archetypeRecord = componentIndex[componentId].sparse[archetype.id] local archetypeRecord = archetype.records[componentId]
if not archetypeRecord then if not archetypeRecord then
return nil return nil
@ -329,35 +374,35 @@ end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId local id = entityId
local componentIndex = world.componentIndex
local record = world.entityIndex[id] local record = world.entityIndex[id]
if not record then if not record then
return nil return nil
end end
local va = get(componentIndex, record, a) local va = get(record, a)
if b == nil then if b == nil then
return va return va
elseif c == nil then elseif c == nil then
return va, get(componentIndex, record, b) return va, get(record, b)
elseif d == nil then elseif d == nil then
return va, get(componentIndex, record, b), get(componentIndex, record, c) return va, get(record, b), get(record, c)
elseif e == nil then elseif e == nil then
return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d) return va, get(record, b), get(record, c), get(record, d)
else else
error("args exceeded") error("args exceeded")
end end
end end
local function noop(self: Query, ...: i53): () -> (number, ...any) -- the less creation the better
return function() local function actualNoOperation() end
end :: any local function noop(_self: Query, ...: i53): () -> (number, ...any)
return actualNoOperation :: any
end end
local EmptyQuery = { local EmptyQuery = {
__iter = noop, __iter = noop;
without = noop without = noop;
} }
EmptyQuery.__index = EmptyQuery EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery) setmetatable(EmptyQuery, EmptyQuery)
@ -365,138 +410,136 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery) export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query function World.query(world: World, ...: i53): Query
local compatibleArchetypes = {} -- breaking?
local components = { ... } if (...) == nil then
local archetypes = world.archetypes
local queryLength = #components
if queryLength == 0 then
error("Missing components") error("Missing components")
end end
local compatibleArchetypes = {}
local length = 0
local components = {...}
local archetypes = world.archetypes
local queryLength = #components
local firstArchetypeMap local firstArchetypeMap
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
for i, componentId in components do for _, componentId in components do
local map = componentIndex[componentId] local map = componentIndex[componentId]
if not map then if not map then
return EmptyQuery return EmptyQuery
end end
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map firstArchetypeMap = map
end end
end end
local i = 0
for id in firstArchetypeMap.sparse do for id in firstArchetypeMap.sparse do
local archetype = archetypes[id] local archetype = archetypes[id]
local archetypeRecords = archetype.records local archetypeRecords = archetype.records
local indices = {} local indices = {}
local skip = false local skip = false
for j, componentId in components do for i, componentId in components do
local index = archetypeRecords[componentId] local index = archetypeRecords[componentId]
if not index then if not index then
skip = true skip = true
break break
end end
indices[j] = archetypeRecords[componentId] indices[i] = index
end end
if skip then if skip then
continue continue
end end
i += 1
table.insert(compatibleArchetypes, { archetype, indices }) length += 1
compatibleArchetypes[length] = {archetype, indices}
end end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes) local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then if not lastArchetype then
return EmptyQuery return EmptyQuery
end end
local preparedQuery = {} local preparedQuery = {}
preparedQuery.__index = preparedQuery preparedQuery.__index = preparedQuery
function preparedQuery:without(...) function preparedQuery:without(...)
local components = { ... } local withoutComponents = {...}
for i = #compatibleArchetypes, 1, -1 do for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i][1] local archetype = compatibleArchetypes[i][1]
local records = archetype.records
local shouldRemove = false local shouldRemove = false
for _, componentId in components do
if archetype.records[componentId] then for _, componentId in withoutComponents do
if records[componentId] then
shouldRemove = true shouldRemove = true
break break
end end
end end
if shouldRemove then
if shouldRemove then
table.remove(compatibleArchetypes, i) table.remove(compatibleArchetypes, i)
end end
end end
lastArchetype, compatibleArchetype = next(compatibleArchetypes) lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then if not lastArchetype then
return EmptyQuery return EmptyQuery
end end
return self return self
end end
local lastRow local lastRow
local queryOutput = {} local queryOutput = {}
function preparedQuery:__iter()
function preparedQuery:__iter() return function()
return function()
local archetype = compatibleArchetype[1] local archetype = compatibleArchetype[1]
local row = next(archetype.entities, lastRow) local row = next(archetype.entities, lastRow)
while row == nil do while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
if lastArchetype == nil then if lastArchetype == nil then
return return
end end
archetype = compatibleArchetype[1] archetype = compatibleArchetype[1]
row = next(archetype.entities, row) row = next(archetype.entities, row)
end end
lastRow = row lastRow = row
local entityId = archetype.entities[row :: number] local entityId = archetype.entities[row :: number]
local columns = archetype.columns local columns = archetype.columns
local tr = compatibleArchetype[2] local tr = compatibleArchetype[2]
if queryLength == 1 then if queryLength == 1 then
return entityId, columns[tr[1]][row] return entityId, columns[tr[1]][row]
elseif queryLength == 2 then elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row]
elseif queryLength == 3 then elseif queryLength == 3 then
return entityId, return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
columns[tr[1]][row], elseif queryLength == 4 then
columns[tr[2]][row], return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
columns[tr[3]][row] elseif queryLength == 5 then
elseif queryLength == 4 then return entityId,
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row]
elseif queryLength == 5 then
return entityId,
columns[tr[1]][row], columns[tr[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
columns[tr[4]][row], columns[tr[4]][row],
columns[tr[5]][row] columns[tr[5]][row]
elseif queryLength == 6 then elseif queryLength == 6 then
return entityId, return entityId,
columns[tr[1]][row], columns[tr[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
columns[tr[4]][row], columns[tr[4]][row],
columns[tr[5]][row], columns[tr[5]][row],
columns[tr[6]][row] columns[tr[6]][row]
elseif queryLength == 7 then elseif queryLength == 7 then
return entityId, return entityId,
columns[tr[1]][row], columns[tr[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
@ -504,8 +547,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[5]][row], columns[tr[5]][row],
columns[tr[6]][row], columns[tr[6]][row],
columns[tr[7]][row] columns[tr[7]][row]
elseif queryLength == 8 then elseif queryLength == 8 then
return entityId, return entityId,
columns[tr[1]][row], columns[tr[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
@ -516,8 +559,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[8]][row] columns[tr[8]][row]
end end
for i in components do for i in components do
queryOutput[i] = tr[i][row] queryOutput[i] = columns[tr[i]][row]
end end
return entityId, unpack(queryOutput, 1, queryLength) return entityId, unpack(queryOutput, 1, queryLength)
@ -527,72 +570,90 @@ function World.query(world: World, ...: i53): Query
return setmetatable({}, preparedQuery) :: any return setmetatable({}, preparedQuery) :: any
end end
function World.component(world: World) function World.component(world: World)
local componentId = world.nextComponentId + 1 local componentId = world.nextComponentId + 1
if componentId > HI_COMPONENT_ID then if componentId > HI_COMPONENT_ID then
error("Too many components") -- IDs are partitioned into ranges because component IDs are not nominal,
-- so it needs to error when IDs intersect into the entity range.
error("Too many components, consider using world:entity() instead to create components.")
end end
world.nextComponentId = componentId world.nextComponentId = componentId
return componentId return componentId
end end
function World.entity(world: World) function World.entity(world: World)
world.nextEntityId += 1 local nextEntityId = world.nextEntityId + 1
return world.nextEntityId + REST world.nextEntityId = nextEntityId
return nextEntityId + REST
end
function World.delete(world: World, entityId: i53)
local entityIndex = world.entityIndex
local record = entityIndex[entityId]
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
-- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from
-- the entities array and delete the record. We know there won't be the hole since
-- we are always removing the last row.
--world.ROOT_ARCHETYPE.entities[record.row] = nil
--entityIndex[entityId] = nil
end end
function World.observer(world: World, ...) function World.observer(world: World, ...)
local componentIds = { ... } local componentIds = {...}
local idsCount = #componentIds
local hooks = world.hooks
return { return {
event = function(event) event = function(event)
local hook = world.hooks[event] local hook = hooks[event]
world.hooks[event] = nil hooks[event] = nil
local last, change local last, change
return function() return function()
last, change = next(hook, last) last, change = next(hook, last)
if not last then if not last then
return return
end end
local matched = false local matched = false
local ids = change.ids
while not matched do
while not matched do
local skip = false local skip = false
for _, id in change.ids do for _, id in ids do
if not table.find(componentIds, id) then if not table.find(componentIds, id) then
skip = true skip = true
break break
end end
end end
if skip then if skip then
last, change = next(hook, last) last, change = next(hook, last)
ids = change.ids
continue continue
end end
matched = true matched = true
end end
local queryOutput = {} local queryOutput = table.create(idsCount)
local row = change.offset local row = change.offset
local archetype = change.archetype local archetype = change.archetype
local columns = archetype.columns local columns = archetype.columns
local archetypeRecords = archetype.records local archetypeRecords = archetype.records
for _, id in componentIds do for index, id in componentIds do
table.insert(queryOutput, columns[archetypeRecords[id]][row]) queryOutput[index] = columns[archetypeRecords[id]][row]
end end
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput) return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end end
end end;
} }
end end
return table.freeze({ return table.freeze({
World = World, World = World;
ON_ADD = ON_ADD, ON_ADD = ON_ADD;
ON_REMOVE = ON_REMOVE, ON_REMOVE = ON_REMOVE;
ON_SET = ON_SET ON_SET = ON_SET;
}) })

186
mkdocs.yml Normal file
View file

@ -0,0 +1,186 @@
site_name: Jecs
site_url: jecs.github.io/jecs
repo_name: ukendio/jecs
repo_url: https://github.com/ukendio/jecs
extra:
version:
provider: mike
theme:
name: material
custom_dir: docs/assets/overrides
logo: assets/logo
favicon: assets/logo-dark.svg
palette:
- media: "(prefers-color-scheme: dark)"
scheme: fusiondoc-dark
toggle:
icon: octicons/sun-24
title: Switch to light theme
- media: "(prefers-color-scheme: light)"
scheme: fusiondoc-light
toggle:
icon: octicons/moon-24
title: Switch to dark theme
font:
text: Plus Jakarta Sans
code: JetBrains Mono
features:
- navigation.tabs
- navigation.top
- navigation.sections
- navigation.instant
- navigation.indexes
- search.suggest
- search.highlight
icon:
repo: octicons/mark-github-16
extra_css:
- assets/theme/fusiondoc.css
- assets/theme/colours.css
- assets/theme/code.css
- assets/theme/paragraph.css
- assets/theme/page.css
- assets/theme/admonition.css
- assets/theme/404.css
- assets/theme/api-reference.css
- assets/theme/dev-tools.css
extra_javascript:
- assets/scripts/smooth-scroll.js
nav:
- Home: index.md
- Tutorials:
- Get Started: tutorials/index.md
- Installing Fusion: tutorials/get-started/installing-fusion.md
- Developer Tools: tutorials/get-started/developer-tools.md
- Getting Help: tutorials/get-started/getting-help.md
- Fundamentals:
- Scopes: tutorials/fundamentals/scopes.md
- Values: tutorials/fundamentals/values.md
- Observers: tutorials/fundamentals/observers.md
- Computeds: tutorials/fundamentals/computeds.md
- Tables:
- ForValues: tutorials/tables/forvalues.md
- ForKeys: tutorials/tables/forkeys.md
- ForPairs: tutorials/tables/forpairs.md
- Animation:
- Tweens: tutorials/animation/tweens.md
- Springs: tutorials/animation/springs.md
- Roblox:
- Hydration: tutorials/roblox/hydration.md
- New Instances: tutorials/roblox/new-instances.md
- Parenting: tutorials/roblox/parenting.md
- Events: tutorials/roblox/events.md
- Change Events: tutorials/roblox/change-events.md
- Outputs: tutorials/roblox/outputs.md
- References: tutorials/roblox/references.md
- Best Practices:
- Components: tutorials/best-practices/components.md
- Instance Handling: tutorials/best-practices/instance-handling.md
- Callbacks: tutorials/best-practices/callbacks.md
- State: tutorials/best-practices/state.md
- Sharing Values: tutorials/best-practices/sharing-values.md
- Error Safety: tutorials/best-practices/error-safety.md
- Optimisation: tutorials/best-practices/optimisation.md
- Examples:
- Home: examples/index.md
- Cookbook:
- examples/cookbook/index.md
- Player List: examples/cookbook/player-list.md
- Animated Computed: examples/cookbook/animated-computed.md
- Fetch Data From Server: examples/cookbook/fetch-data-from-server.md
- Light & Dark Theme: examples/cookbook/light-and-dark-theme.md
- Button Component: examples/cookbook/button-component.md
- Loading Spinner: examples/cookbook/loading-spinner.md
- Drag & Drop: examples/cookbook/drag-and-drop.md
- API Reference:
- api-reference/index.md
- General:
- Errors: api-reference/general/errors.md
- Types:
- Contextual: api-reference/general/types/contextual.md
- Version: api-reference/general/types/version.md
- Members:
- Contextual: api-reference/general/members/contextual.md
- Safe: api-reference/general/members/safe.md
- version: api-reference/general/members/version.md
- Memory:
- Types:
- Scope: api-reference/memory/types/scope.md
- ScopedObject: api-reference/memory/types/scopedobject.md
- Task: api-reference/memory/types/task.md
- Members:
- deriveScope: api-reference/memory/members/derivescope.md
- doCleanup: api-reference/memory/members/docleanup.md
- scoped: api-reference/memory/members/scoped.md
- State:
- Types:
- UsedAs: api-reference/state/types/usedas.md
- Computed: api-reference/state/types/computed.md
- Dependency: api-reference/state/types/dependency.md
- Dependent: api-reference/state/types/dependent.md
- For: api-reference/state/types/for.md
- Observer: api-reference/state/types/observer.md
- StateObject: api-reference/state/types/stateobject.md
- Use: api-reference/state/types/use.md
- Value: api-reference/state/types/value.md
- Members:
- Computed: api-reference/state/members/computed.md
- ForKeys: api-reference/state/members/forkeys.md
- ForPairs: api-reference/state/members/forpairs.md
- ForValues: api-reference/state/members/forvalues.md
- Observer: api-reference/state/members/observer.md
- peek: api-reference/state/members/peek.md
- Value: api-reference/state/members/value.md
- Roblox:
- Types:
- Child: api-reference/roblox/types/child.md
- PropertyTable: api-reference/roblox/types/propertytable.md
- SpecialKey: api-reference/roblox/types/specialkey.md
- Members:
- Attribute: api-reference/roblox/members/attribute.md
- AttributeChange: api-reference/roblox/members/attributechange.md
- AttributeOut: api-reference/roblox/members/attributeout.md
- Children: api-reference/roblox/members/children.md
- Hydrate: api-reference/roblox/members/hydrate.md
- New: api-reference/roblox/members/new.md
- OnChange: api-reference/roblox/members/onchange.md
- OnEvent: api-reference/roblox/members/onevent.md
- Out: api-reference/roblox/members/out.md
- Ref: api-reference/roblox/members/ref.md
- Animation:
- Types:
- Animatable: api-reference/animation/types/animatable.md
- Spring: api-reference/animation/types/spring.md
- Tween: api-reference/animation/types/tween.md
- Members:
- Tween: api-reference/animation/members/tween.md
- Spring: api-reference/animation/members/spring.md
- Extras:
- Home: extras/index.md
- Backgrounds: extras/backgrounds.md
- Brand Guidelines: extras/brand-guidelines.md
markdown_extensions:
- admonition
- attr_list
- meta
- md_in_html
- pymdownx.superfences
- pymdownx.betterem
- pymdownx.details
- pymdownx.tabbed:
alternate_style: true
- pymdownx.inlinehilite
- toc:
permalink: true
- pymdownx.highlight:
guess_lang: false
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

4
selene.toml Normal file
View file

@ -0,0 +1,4 @@
std = "roblox"
[lints]
global_usage = "allow"

5
stylua.toml Normal file
View file

@ -0,0 +1,5 @@
column_width = 120
quote_style = "ForceDouble"
[sort_requires]
enabled = true

View file

@ -11,9 +11,6 @@
}, },
"ReplicatedStorage": { "ReplicatedStorage": {
"$className": "ReplicatedStorage", "$className": "ReplicatedStorage",
"DevPackages": {
"$path": "DevPackages"
},
"Lib": { "Lib": {
"$path": "lib" "$path": "lib"
}, },
@ -26,15 +23,6 @@
"mirror": { "mirror": {
"$path": "mirror" "$path": "mirror"
} }
},
"TestService": {
"$properties": {
"ExecuteWithStudioRun": true
},
"$className": "TestService",
"run": {
"$path": "tests.server.lua"
}
} }
} }
} }

3
testez-companion.toml Normal file
View file

@ -0,0 +1,3 @@
roots = ["ServerStorage"]
[extraOptions]

View file

@ -1,9 +0,0 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
require(ReplicatedStorage.DevPackages.TestEZ).TestBootstrap:run({
ReplicatedStorage.Lib,
nil,
{
noXpcallByDefault = true,
},
})

View file

@ -1,115 +0,0 @@
local testkit = require("../testkit")
local jecs = require("../lib/init")
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local N = 10
TEST("world:query", function()
do CASE "should query all matching entities"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i > 5 then world:set(id, B, true) end
entities[i] = id
end
for id in world:query(A) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
do CASE "should query all matching entities when irrelevant component is removed"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
world:set(id, B, true)
if i > 5 then world:remove(id, B, true) end
entities[i] = id
end
local added = 0
for id in world:query(A) do
added += 1
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(added == N)
end
do CASE "should query all entities without B"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i < 5 then
entities[i] = id
else
world:set(id, B, true)
end
end
for id in world:query(A):without(B) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
do CASE "should allow setting components in arbitrary order"
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
CHECK(world:get(id, Poison) == 5)
end
do CASE "Should allow deleting components"
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
world:delete(id)
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == nil)
end
end)
FINISH()

353
tests/world.lua Normal file
View file

@ -0,0 +1,353 @@
local jecs = require("../lib/init")
local testkit = require("../testkit")
local __ = jecs.Wildcard
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
local IS_PAIR = jecs.IS_PAIR
local ECS_PAIR = jecs.ECS_PAIR
local getAlive = jecs.getAlive
local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION
local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
local ok, err: string? = pcall(fn, ...)
if not CHECK(not ok, 2) then
local i = string.find(err :: string, " ")
assert(i)
local msg = string.sub(err :: string, i + 1)
CHECK(msg == s, 2)
end
end
local N = 10
TEST("world", function()
do
CASE("should be iterable")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local eA = world:entity()
world:set(eA, A, true)
local eB = world:entity()
world:set(eB, B, true)
local eAB = world:entity()
world:set(eAB, A, true)
world:set(eAB, B, true)
local count = 0
for id, data in world do
count += 1
if id == eA then
CHECK(data[A] == true)
CHECK(data[B] == nil)
elseif id == eB then
CHECK(data[A] == nil)
CHECK(data[B] == true)
elseif id == eAB then
CHECK(data[A] == true)
CHECK(data[B] == true)
end
end
-- components are registered in the entity index as well
-- so this test has to add 2 to account for them
CHECK(count == 3 + 2)
end
do
CASE("should query all matching entities")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i > 5 then
world:set(id, B, true)
end
entities[i] = id
end
for id in world:query(A) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
do
CASE("should query all matching entities when irrelevant component is removed")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
-- specifically put them in disorder to track regression
-- https://github.com/Ukendio/jecs/pull/15
world:set(id, B, true)
world:set(id, A, true)
if i > 5 then
world:remove(id, B)
end
entities[i] = id
end
local added = 0
for id in world:query(A) do
added += 1
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(added == N)
end
do
CASE("should query all entities without B")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i < 5 then
entities[i] = id
else
world:set(id, B, true)
end
end
for id in world:query(A):without(B) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
do
CASE("should allow setting components in arbitrary order")
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
CHECK(world:get(id, Poison) == 5)
end
do
CASE("should allow deleting components")
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
local id1 = world:entity()
world:set(id1, Poison, 500)
world:set(id1, Health, 50)
world:delete(id)
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == nil)
CHECK(world:get(id1, Poison) == 500)
CHECK(world:get(id1, Health) == 50)
end
do
CASE("should allow remove that doesn't exist on entity")
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Health, 50)
world:remove(id, Poison)
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
do
CASE("should increment generation")
local world = jecs.World.new()
local e = world:entity()
CHECK(ECS_ID(e) == 1 + jecs.Rest)
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
do
CASE("should get alive from index in the dense array")
local world = jecs.World.new()
local _e = world:entity()
local e2 = world:entity()
local e3 = world:entity()
CHECK(IS_PAIR(world:entity()) == false)
local pair = ECS_PAIR(e2, e3)
CHECK(IS_PAIR(pair) == true)
CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2)
CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3)
end
do
CASE("should allow querying for relations")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), true)
for e, bool in world:query(ECS_PAIR(Eats, Apples)) do
CHECK(e == bob)
CHECK(bool)
end
end
do
CASE("should allow wildcards in queries")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
local w = jecs.Wildcard
for e, data in world:query(ECS_PAIR(Eats, w)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
for e, data in world:query(ECS_PAIR(w, Apples)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
end
do
CASE("should match against multiple pairs")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
local w = jecs.Wildcard
local count = 0
for e, data in world:query(ECS_PAIR(Eats, w)) do
count += 1
if e == bob then
CHECK(data == "bob eats apples")
else
CHECK(data == "alice eats oranges")
end
end
CHECK(count == 2)
count = 0
for e, data in world:query(ECS_PAIR(w, Apples)) do
count += 1
CHECK(data == "bob eats apples")
end
CHECK(count == 1)
end
do
CASE("should only relate alive entities")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
world:set(bob, Apples, "apples")
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
world:delete(Apples)
local Wildcard = jecs.Wildcard
local count = 0
for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do
count += 1
end
world:delete(ECS_PAIR(Eats, Apples))
CHECK(count == 0)
CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil)
end
do
CASE("should error when setting invalid pair")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:delete(Apples)
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
end
do
CASE("should find target for ChildOf")
local world = jecs.World.new()
local ChildOf = world:component()
local Name = world:component()
local function parent(entity)
return world:target(entity, ChildOf)
end
local bob = world:entity()
local alice = world:entity()
local sara = world:entity()
world:add(bob, ECS_PAIR(ChildOf, alice))
world:set(bob, Name, "bob")
world:add(sara, ECS_PAIR(ChildOf, alice))
world:set(sara, Name, "sara")
CHECK(parent(bob) == alice) -- O(1)
local count = 0
for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do
print(name)
count += 1
end
CHECK(count == 2)
end
end)
FINISH()

View file

@ -1,15 +1,7 @@
[package] [package]
name = "ukendio/jecs" name = "ukendio/jecs"
version = "0.0.0-prototype.rc.3" version = "0.1.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
exclude = ["**"] include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
include = ["default.project.json", "lib", "wally.toml", "README.md"] exclude = ["**"]
[dev-dependencies]
TestEZ = "roblox/testez@0.4.1"
Matter = "matter-ecs/matter@0.8.0"
ecr = "centau/ecr@0.8.0"