mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 09:30:03 +00:00
Merge branch 'types-dx' of https://github.com/alicesaidhi/jecs into alicesaidhi-types-dx
This commit is contained in:
commit
951c64a51c
29 changed files with 2051 additions and 4345 deletions
73
.github/workflows/release.yaml
vendored
Normal file
73
.github/workflows/release.yaml
vendored
Normal 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
|
66
README.md
66
README.md
|
@ -1,6 +1,7 @@
|
|||
|
||||
<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>
|
||||
|
||||
[](LICENSE-APACHE)
|
||||
|
@ -10,44 +11,51 @@ Just an ECS
|
|||
|
||||
jecs is a stupidly fast Entity Component System (ECS).
|
||||
|
||||
- Process tens of thousands of entities with ease every frame
|
||||
- Zero-dependency Luau package
|
||||
- Entity Relationships as first class citizens
|
||||
- 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
|
||||
- Cache friendly archetype/SoA storage
|
||||
- Unit tested for stability
|
||||
|
||||
### Example
|
||||
|
||||
```lua
|
||||
local world = Jecs.World.new()
|
||||
local world = jecs.World.new()
|
||||
local pair = jecs.pair
|
||||
|
||||
local Health = world:component()
|
||||
local Damage = world:component()
|
||||
local Position = world:component()
|
||||
local ChildOf = world:component()
|
||||
local Name = world:component()
|
||||
|
||||
local player = world:entity()
|
||||
local opponent = world:entity()
|
||||
|
||||
world:set(player, Health, 100)
|
||||
world:set(player, Damage, 8)
|
||||
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)
|
||||
local function parent(entity)
|
||||
return world:target(entity, ChildOf)
|
||||
end
|
||||
local function getName(entity)
|
||||
return world:get(entity, Name)
|
||||
end
|
||||
|
||||
assert(world:get(playerId, Health) == 79)
|
||||
assert(world:get(opponentId, Health) == 92)
|
||||
local alice = world:entity()
|
||||
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.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tools]
|
||||
wally = "upliftgames/wally@0.3.1"
|
||||
wally = "upliftgames/wally@0.3.2"
|
||||
rojo = "rojo-rbx/rojo@7.4.1"
|
||||
stylua = "johnnymorganz/stylua@0.19.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
31
bench.project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,299 +1,246 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
|
||||
local testkit = require('../testkit')
|
||||
local testkit = require("../testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
local function TITLE(title: string)
|
||||
print()
|
||||
print(testkit.color.white(title))
|
||||
print()
|
||||
print(testkit.color.white(title))
|
||||
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
|
||||
|
||||
do TITLE (testkit.color.white_underline("Jecs query"))
|
||||
local ecs = jecs.World.new()
|
||||
do TITLE "one component in common"
|
||||
do
|
||||
TITLE(testkit.color.white_underline("Jecs query"))
|
||||
local ecs = jecs.World.new()
|
||||
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: 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("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("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("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)
|
||||
|
||||
BENCH("8 component", function()
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do end
|
||||
end)
|
||||
end
|
||||
local e = world:entity()
|
||||
world:set(e, A, true)
|
||||
world:set(e, B, true)
|
||||
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()
|
||||
local D2 = ecs:component()
|
||||
local D3 = ecs:component()
|
||||
local D4 = ecs:component()
|
||||
local D5 = ecs:component()
|
||||
local D6 = ecs:component()
|
||||
local D7 = ecs:component()
|
||||
local D8 = ecs:component()
|
||||
BENCH("Update Data", function()
|
||||
for _ = 1, 100 do
|
||||
world:set(e, A, false)
|
||||
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 function flip()
|
||||
return math.random() >= 0.15
|
||||
end
|
||||
local D1 = ecs:component()
|
||||
local D2 = ecs:component()
|
||||
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 = {}
|
||||
for i = 1, 2^16-2 do
|
||||
local entity = ecs:entity()
|
||||
for i = 1, 2 ^ 16 - 2 do
|
||||
local entity = ecs:entity()
|
||||
|
||||
local combination = ""
|
||||
local combination = ""
|
||||
|
||||
if flip() then
|
||||
combination ..= "B"
|
||||
ecs:set(entity, D2, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "C"
|
||||
ecs:set(entity, D3, { value = true })
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "D"
|
||||
ecs:set(entity, D4, { value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "E"
|
||||
ecs:set(entity, D5, { value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "F"
|
||||
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})
|
||||
if flip() then
|
||||
combination ..= "B"
|
||||
ecs:set(entity, D2, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "C"
|
||||
ecs:set(entity, D3, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "D"
|
||||
ecs:set(entity, D4, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "E"
|
||||
ecs:set(entity, D5, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "F"
|
||||
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
|
||||
added += 1
|
||||
ecs:set(entity, D1, { value = true})
|
||||
end
|
||||
if #combination == 7 then
|
||||
added += 1
|
||||
ecs:set(entity, D1, {value = true})
|
||||
end
|
||||
archetypes[combination] = true
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
||||
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 component = oldMatter.component
|
||||
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)
|
||||
|
||||
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("2 component", function()
|
||||
for _ in world:query(A, B) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("1 component", function()
|
||||
for _ in world:query(A) do end
|
||||
end)
|
||||
BENCH("4 component", function()
|
||||
for _ in world:query(A, B, C, D) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("2 component", function()
|
||||
for _ in world:query(A, B) do end
|
||||
end)
|
||||
BENCH("8 component", function()
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("4 component", function()
|
||||
for _ in world:query(A, B, C, D) do
|
||||
end
|
||||
end)
|
||||
local e = world:entity()
|
||||
world:set(e, A, true)
|
||||
world:set(e, B, true)
|
||||
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()
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do end
|
||||
end)
|
||||
end
|
||||
BENCH("Update Data", function()
|
||||
for _ = 1, 100 do
|
||||
world:set(e, A, false)
|
||||
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 D2 = component()
|
||||
local D3 = component()
|
||||
local D4 = component()
|
||||
local D5 = component()
|
||||
local D6 = component()
|
||||
local D7 = component()
|
||||
local D8 = component()
|
||||
local D1 = ecs:component()
|
||||
local D2 = ecs:component()
|
||||
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 function flip()
|
||||
return math.random() >= 0.15
|
||||
end
|
||||
local function flip()
|
||||
return math.random() >= 0.15
|
||||
end
|
||||
|
||||
local added = 0
|
||||
local added = 0
|
||||
local archetypes = {}
|
||||
for i = 1, 2^16-2 do
|
||||
local entity = ecs:spawn()
|
||||
for i = 1, 2 ^ 16 - 2 do
|
||||
local entity = ecs:entity()
|
||||
|
||||
local combination = ""
|
||||
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}))
|
||||
if flip() then
|
||||
combination ..= "B"
|
||||
ecs:set(entity, D2, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "C"
|
||||
ecs:set(entity, D3, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "D"
|
||||
ecs:set(entity, D4, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "E"
|
||||
ecs:set(entity, D5, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "F"
|
||||
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 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
|
||||
if #combination == 7 then
|
||||
added += 1
|
||||
ecs:set(entity, D1, {value = true})
|
||||
end
|
||||
archetypes[combination] = true
|
||||
end
|
||||
end
|
||||
|
||||
local a = 0
|
||||
for _ in archetypes do a+= 1 end
|
||||
|
||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
||||
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
|
||||
for _ in archetypes do
|
||||
a += 1
|
||||
end
|
||||
|
||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
||||
end
|
||||
end
|
|
@ -2,12 +2,13 @@
|
|||
--!native
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rgb = require(ReplicatedStorage.rgb)
|
||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local newWorld = Matter.World.new()
|
||||
local ecs = jecs.World.new()
|
||||
local mirror = require(ReplicatedStorage.mirror)
|
||||
local mcs = mirror.World.new()
|
||||
|
||||
local A1 = Matter.component()
|
||||
local A2 = Matter.component()
|
||||
|
@ -35,6 +36,15 @@ local C5 = ecs:entity()
|
|||
local C6 = ecs:entity()
|
||||
local C7 = 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()
|
||||
return {
|
||||
|
@ -44,7 +54,7 @@ return {
|
|||
|
||||
Functions = {
|
||||
Matter = function()
|
||||
for i = 1, 50 do
|
||||
for i = 1, 500 do
|
||||
newWorld:spawn(
|
||||
A1({ value = true }),
|
||||
A2({ value = true }),
|
||||
|
@ -60,8 +70,8 @@ return {
|
|||
|
||||
|
||||
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, B2, {value = false})
|
||||
registry2:set(e, B3, {value = false})
|
||||
|
@ -78,7 +88,7 @@ return {
|
|||
|
||||
local e = ecs:entity()
|
||||
|
||||
for i = 1, 50 do
|
||||
for i = 1, 500 do
|
||||
|
||||
ecs:set(e, C1, {value = false})
|
||||
ecs:set(e, C2, {value = false})
|
||||
|
@ -89,6 +99,23 @@ return {
|
|||
ecs:set(e, C7, {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
|
||||
|
||||
|
|
|
@ -2,41 +2,37 @@
|
|||
--!native
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rgb = require(ReplicatedStorage.rgb)
|
||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local rgb = require(ReplicatedStorage.rgb)
|
||||
local newWorld = Matter.World.new()
|
||||
local ecs = jecs.World.new()
|
||||
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
local registry2 = ecr.registry()
|
||||
local registry2 = ecr.registry()
|
||||
|
||||
return registry2
|
||||
end,
|
||||
end;
|
||||
|
||||
Functions = {
|
||||
Matter = function()
|
||||
for i = 1, 1000 do
|
||||
newWorld:spawn()
|
||||
end
|
||||
end,
|
||||
Matter = function()
|
||||
for i = 1, 1000 do
|
||||
newWorld:spawn()
|
||||
end
|
||||
end;
|
||||
|
||||
ECR = function(_, registry2)
|
||||
for i = 1, 1000 do
|
||||
registry2.create()
|
||||
end
|
||||
end;
|
||||
|
||||
ECR = function(_, registry2)
|
||||
for i = 1, 1000 do
|
||||
registry2.create()
|
||||
end
|
||||
end,
|
||||
|
||||
|
||||
Jecs = function()
|
||||
for i = 1, 1000 do
|
||||
ecs:entity()
|
||||
end
|
||||
end
|
||||
|
||||
},
|
||||
Jecs = function()
|
||||
for i = 1, 1000 do
|
||||
ecs:entity()
|
||||
end
|
||||
end;
|
||||
};
|
||||
}
|
||||
|
|
11
benches/visual/wally.toml
Normal file
11
benches/visual/wally.toml
Normal 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
45
docs/api-types.md
Normal 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
187
docs/api/world.md
Normal 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.
|
19
docs/tutorials/quick-start/getting-started.md
Normal file
19
docs/tutorials/quick-start/getting-started.md
Normal 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.
|
||||

|
||||
|
||||
## 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
|
||||
```
|
BIN
docs/tutorials/quick-start/rbxm.png
Normal file
BIN
docs/tutorials/quick-start/rbxm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
6
jecs_darkmode.svg
Normal file
6
jecs_darkmode.svg
Normal 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
6
jecs_lightmode.svg
Normal 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 |
901
lib/init.lua
901
lib/init.lua
File diff suppressed because it is too large
Load diff
|
@ -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
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
503
mirror/init.lua
503
mirror/init.lua
|
@ -6,10 +6,10 @@
|
|||
type i53 = number
|
||||
type i24 = number
|
||||
|
||||
type Ty = { i53 }
|
||||
type Ty = {i53}
|
||||
type ArchetypeId = number
|
||||
|
||||
type Column = { any }
|
||||
type Column = {any}
|
||||
|
||||
type Archetype = {
|
||||
id: number,
|
||||
|
@ -20,9 +20,9 @@ type Archetype = {
|
|||
},
|
||||
},
|
||||
types: Ty,
|
||||
type: string | number,
|
||||
entities: { number },
|
||||
columns: { Column },
|
||||
type: string | number,
|
||||
entities: {number},
|
||||
columns: {Column},
|
||||
records: {},
|
||||
}
|
||||
|
||||
|
@ -31,13 +31,13 @@ type Record = {
|
|||
row: number,
|
||||
}
|
||||
|
||||
type EntityIndex = { [i24]: Record }
|
||||
type ComponentIndex = { [i24]: ArchetypeMap}
|
||||
type EntityIndex = {[i24]: Record}
|
||||
type ComponentIndex = {[i24]: ArchetypeMap}
|
||||
|
||||
type ArchetypeRecord = number
|
||||
type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
|
||||
type Archetypes = { [ArchetypeId]: Archetype }
|
||||
|
||||
type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
|
||||
type Archetypes = {[ArchetypeId]: Archetype}
|
||||
|
||||
type ArchetypeDiff = {
|
||||
added: Ty,
|
||||
removed: Ty,
|
||||
|
@ -51,38 +51,58 @@ local REST = HI_COMPONENT_ID + 4
|
|||
|
||||
local function transitionArchetype(
|
||||
entityIndex: EntityIndex,
|
||||
destinationArchetype: Archetype,
|
||||
to: Archetype,
|
||||
destinationRow: i24,
|
||||
sourceArchetype: Archetype,
|
||||
from: Archetype,
|
||||
sourceRow: i24
|
||||
)
|
||||
local columns = sourceArchetype.columns
|
||||
local sourceEntities = sourceArchetype.entities
|
||||
local destinationEntities = destinationArchetype.entities
|
||||
local destinationColumns = destinationArchetype.columns
|
||||
local columns = from.columns
|
||||
local sourceEntities = from.entities
|
||||
local destinationEntities = to.entities
|
||||
local destinationColumns = to.columns
|
||||
local tr = to.records
|
||||
local types = from.types
|
||||
|
||||
for componentId, column in columns do
|
||||
local targetColumn = destinationColumns[componentId]
|
||||
if targetColumn then
|
||||
for i, column in columns do
|
||||
-- Retrieves the new column index from the source archetype's record from each component
|
||||
-- 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]
|
||||
end
|
||||
column[sourceRow] = column[#column]
|
||||
column[#column] = nil
|
||||
-- If the entity is the last row in the archetype then swapping it would be meaningless.
|
||||
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
|
||||
|
||||
destinationEntities[destinationRow] = sourceEntities[sourceRow]
|
||||
entityIndex[sourceEntities[sourceRow]].row = destinationRow
|
||||
-- Move the entity from the source to the destination archetype.
|
||||
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
|
||||
sourceEntities[sourceRow] = sourceEntities[movedAway]
|
||||
entityIndex[sourceEntities[movedAway]].row = sourceRow
|
||||
if sourceRow ~= movedAway then
|
||||
local atMovedAway = sourceEntities[movedAway]
|
||||
sourceEntities[sourceRow] = atMovedAway
|
||||
entityIndex[atMovedAway].row = sourceRow
|
||||
end
|
||||
|
||||
sourceEntities[movedAway] = nil
|
||||
end
|
||||
|
||||
local function archetypeAppend(entity: i53, archetype: Archetype): i24
|
||||
local function archetypeAppend(entity: number, archetype: Archetype): number
|
||||
local entities = archetype.entities
|
||||
table.insert(entities, entity)
|
||||
return #entities
|
||||
local length = #entities + 1
|
||||
entities[length] = entity
|
||||
return length
|
||||
end
|
||||
|
||||
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
|
||||
|
@ -105,102 +125,104 @@ local function hash(arr): string | number
|
|||
return table.concat(arr, "_")
|
||||
end
|
||||
|
||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
||||
local destinationCount = #to.types
|
||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
|
||||
local destinationIds = to.types
|
||||
local records = to.records
|
||||
local id = to.id
|
||||
|
||||
for i = 1, destinationCount do
|
||||
local destinationId = destinationIds[i]
|
||||
for i, destinationId in destinationIds do
|
||||
local archetypesMap = componentIndex[destinationId]
|
||||
|
||||
if not componentIndex[destinationId] then
|
||||
componentIndex[destinationId] = { size = 0, sparse = {} }
|
||||
if not archetypesMap then
|
||||
archetypesMap = {size = 0, sparse = {}}
|
||||
componentIndex[destinationId] = archetypesMap
|
||||
end
|
||||
|
||||
local archetypesMap = componentIndex[destinationId]
|
||||
archetypesMap.sparse[to.id] = i
|
||||
to.records[destinationId] = i
|
||||
archetypesMap.sparse[id] = i
|
||||
records[destinationId] = i
|
||||
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)
|
||||
|
||||
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
|
||||
local id = world.nextArchetypeId
|
||||
local id = world.nextArchetypeId + 1
|
||||
world.nextArchetypeId = id
|
||||
|
||||
local columns = {} :: { any }
|
||||
local length = #types
|
||||
local columns = table.create(length) :: {any}
|
||||
|
||||
for _ in types do
|
||||
table.insert(columns, {})
|
||||
for index in types do
|
||||
columns[index] = {}
|
||||
end
|
||||
|
||||
local archetype = {
|
||||
id = id,
|
||||
types = types,
|
||||
type = ty,
|
||||
columns = columns,
|
||||
entities = {},
|
||||
edges = {},
|
||||
records = {},
|
||||
columns = columns;
|
||||
edges = {};
|
||||
entities = {};
|
||||
id = id;
|
||||
records = {};
|
||||
type = ty;
|
||||
types = types;
|
||||
}
|
||||
world.archetypeIndex[ty] = archetype
|
||||
world.archetypes[id] = archetype
|
||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
||||
if length > 0 then
|
||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
||||
end
|
||||
|
||||
return archetype
|
||||
end
|
||||
|
||||
local World = {}
|
||||
World.__index = World
|
||||
function World.new()
|
||||
function World.new()
|
||||
local self = setmetatable({
|
||||
entityIndex = {},
|
||||
componentIndex = {},
|
||||
archetypes = {},
|
||||
archetypeIndex = {},
|
||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||
nextEntityId = 0,
|
||||
nextComponentId = 0,
|
||||
nextArchetypeId = 0,
|
||||
archetypeIndex = {};
|
||||
archetypes = {};
|
||||
componentIndex = {};
|
||||
entityIndex = {};
|
||||
hooks = {
|
||||
[ON_ADD] = {}
|
||||
}
|
||||
[ON_ADD] = {};
|
||||
};
|
||||
nextArchetypeId = 0;
|
||||
nextComponentId = 0;
|
||||
nextEntityId = 0;
|
||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
||||
}, World)
|
||||
return self
|
||||
return self
|
||||
end
|
||||
|
||||
local function emit(world, eventDescription)
|
||||
local function emit(world, eventDescription)
|
||||
local event = eventDescription.event
|
||||
|
||||
table.insert(world.hooks[event], {
|
||||
ids = eventDescription.ids,
|
||||
archetype = eventDescription.archetype,
|
||||
otherArchetype = eventDescription.otherArchetype,
|
||||
offset = eventDescription.offset
|
||||
archetype = eventDescription.archetype;
|
||||
ids = eventDescription.ids;
|
||||
offset = eventDescription.offset;
|
||||
otherArchetype = eventDescription.otherArchetype;
|
||||
})
|
||||
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, {
|
||||
event = ON_ADD,
|
||||
ids = added,
|
||||
archetype = archetype,
|
||||
otherArchetype = otherArchetype,
|
||||
offset = row,
|
||||
archetype = archetype;
|
||||
event = ON_ADD;
|
||||
ids = added;
|
||||
offset = row;
|
||||
otherArchetype = otherArchetype;
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
export type World = typeof(World.new())
|
||||
|
||||
local function ensureArchetype(world: World, types, prev)
|
||||
if #types < 1 then
|
||||
return world.ROOT_ARCHETYPE
|
||||
end
|
||||
|
||||
local ty = hash(types)
|
||||
local archetype = world.archetypeIndex[ty]
|
||||
if archetype then
|
||||
|
@ -210,10 +232,8 @@ local function ensureArchetype(world: World, types, prev)
|
|||
return archetypeOf(world, types, prev)
|
||||
end
|
||||
|
||||
local function findInsert(types: { i53 }, toAdd: i53)
|
||||
local count = #types
|
||||
for i = 1, count do
|
||||
local id = types[i]
|
||||
local function findInsert(types: {i53}, toAdd: i53)
|
||||
for i, id in types do
|
||||
if id == toAdd then
|
||||
return -1
|
||||
end
|
||||
|
@ -221,13 +241,18 @@ local function findInsert(types: { i53 }, toAdd: i53)
|
|||
return i
|
||||
end
|
||||
end
|
||||
return count + 1
|
||||
return #types + 1
|
||||
end
|
||||
|
||||
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
||||
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)
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -237,88 +262,108 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
|
|||
end
|
||||
|
||||
local function ensureEdge(archetype: Archetype, componentId: i53)
|
||||
if not archetype.edges[componentId] then
|
||||
archetype.edges[componentId] = {} :: any
|
||||
local edges = archetype.edges
|
||||
local edge = edges[componentId]
|
||||
if not edge then
|
||||
edge = {} :: any
|
||||
edges[componentId] = edge
|
||||
end
|
||||
return archetype.edges[componentId]
|
||||
return edge
|
||||
end
|
||||
|
||||
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
||||
if not from then
|
||||
if not world.ROOT_ARCHETYPE then
|
||||
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
end
|
||||
from = world.ROOT_ARCHETYPE
|
||||
if not from then
|
||||
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
|
||||
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
|
||||
if not ROOT_ARCHETYPE then
|
||||
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
|
||||
end
|
||||
from = ROOT_ARCHETYPE
|
||||
end
|
||||
|
||||
local edge = ensureEdge(from, componentId)
|
||||
|
||||
if not edge.add then
|
||||
edge.add = findArchetypeWith(world, from, componentId)
|
||||
local add = edge.add
|
||||
if not add then
|
||||
-- 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
|
||||
|
||||
return edge.add
|
||||
return add
|
||||
end
|
||||
|
||||
local function ensureRecord(entityIndex, entityId: i53): Record
|
||||
local id = entityId
|
||||
if not entityIndex[id] then
|
||||
entityIndex[id] = {}
|
||||
local record = entityIndex[entityId]
|
||||
|
||||
if not record then
|
||||
record = {}
|
||||
entityIndex[entityId] = record
|
||||
end
|
||||
return entityIndex[id] :: Record
|
||||
|
||||
return record :: Record
|
||||
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 sourceArchetype = record.archetype
|
||||
local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
|
||||
local from = record.archetype
|
||||
local to = archetypeTraverseAdd(world, componentId, from)
|
||||
|
||||
if sourceArchetype == destinationArchetype then
|
||||
local archetypeRecord = destinationArchetype.records[componentId]
|
||||
destinationArchetype.columns[archetypeRecord][record.row] = data
|
||||
if from == to then
|
||||
-- If the archetypes are the same it can avoid moving the entity
|
||||
-- and just set the data directly.
|
||||
local archetypeRecord = to.records[componentId]
|
||||
from.columns[archetypeRecord][record.row] = data
|
||||
-- Should fire an OnSet event here.
|
||||
return
|
||||
end
|
||||
|
||||
if sourceArchetype then
|
||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
||||
if from then
|
||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
||||
moveEntity(world.entityIndex, entityId, record, to)
|
||||
else
|
||||
if #destinationArchetype.types > 0 then
|
||||
newEntity(entityId, record, destinationArchetype)
|
||||
onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId })
|
||||
if #to.types > 0 then
|
||||
-- When there is no previous archetype it should create the archetype
|
||||
newEntity(entityId, record, to)
|
||||
onNotifyAdd(world, to, from, record.row, {componentId})
|
||||
end
|
||||
end
|
||||
|
||||
local archetypeRecord = destinationArchetype.records[componentId]
|
||||
destinationArchetype.columns[archetypeRecord][record.row] = data
|
||||
local archetypeRecord = to.records[componentId]
|
||||
to.columns[archetypeRecord][record.row] = data
|
||||
end
|
||||
|
||||
local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
|
||||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
||||
local edge = ensureEdge(from, componentId)
|
||||
|
||||
|
||||
if not edge.remove then
|
||||
local to = table.clone(from.types)
|
||||
local remove = edge.remove
|
||||
if not remove then
|
||||
local to = table.clone(from.types)
|
||||
table.remove(to, table.find(to, componentId))
|
||||
edge.remove = ensureArchetype(world, to, from)
|
||||
remove = ensureArchetype(world, to, from)
|
||||
edge.remove = remove :: never
|
||||
end
|
||||
|
||||
return edge.remove
|
||||
return remove
|
||||
end
|
||||
|
||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||
local record = ensureRecord(world.entityIndex, entityId)
|
||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||
local entityIndex = world.entityIndex
|
||||
local record = ensureRecord(entityIndex, entityId)
|
||||
local sourceArchetype = record.archetype
|
||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
||||
|
||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
||||
moveEntity(entityIndex, entityId, record, destinationArchetype)
|
||||
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 archetypeRecord = componentIndex[componentId].sparse[archetype.id]
|
||||
local archetypeRecord = archetype.records[componentId]
|
||||
|
||||
if not archetypeRecord then
|
||||
return nil
|
||||
|
@ -329,35 +374,35 @@ end
|
|||
|
||||
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
||||
local id = entityId
|
||||
local componentIndex = world.componentIndex
|
||||
local record = world.entityIndex[id]
|
||||
if not record then
|
||||
return nil
|
||||
end
|
||||
|
||||
local va = get(componentIndex, record, a)
|
||||
local va = get(record, a)
|
||||
|
||||
if b == nil then
|
||||
return va
|
||||
elseif c == nil then
|
||||
return va, get(componentIndex, record, b)
|
||||
return va, get(record, b)
|
||||
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
|
||||
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
|
||||
error("args exceeded")
|
||||
end
|
||||
end
|
||||
|
||||
local function noop(self: Query, ...: i53): () -> (number, ...any)
|
||||
return function()
|
||||
end :: any
|
||||
-- the less creation the better
|
||||
local function actualNoOperation() end
|
||||
local function noop(_self: Query, ...: i53): () -> (number, ...any)
|
||||
return actualNoOperation :: any
|
||||
end
|
||||
|
||||
local EmptyQuery = {
|
||||
__iter = noop,
|
||||
without = noop
|
||||
__iter = noop;
|
||||
without = noop;
|
||||
}
|
||||
EmptyQuery.__index = EmptyQuery
|
||||
setmetatable(EmptyQuery, EmptyQuery)
|
||||
|
@ -365,138 +410,136 @@ setmetatable(EmptyQuery, EmptyQuery)
|
|||
export type Query = typeof(EmptyQuery)
|
||||
|
||||
function World.query(world: World, ...: i53): Query
|
||||
local compatibleArchetypes = {}
|
||||
local components = { ... }
|
||||
local archetypes = world.archetypes
|
||||
local queryLength = #components
|
||||
|
||||
if queryLength == 0 then
|
||||
-- breaking?
|
||||
if (...) == nil then
|
||||
error("Missing components")
|
||||
end
|
||||
|
||||
local compatibleArchetypes = {}
|
||||
local length = 0
|
||||
|
||||
local components = {...}
|
||||
local archetypes = world.archetypes
|
||||
local queryLength = #components
|
||||
|
||||
local firstArchetypeMap
|
||||
local componentIndex = world.componentIndex
|
||||
|
||||
for i, componentId in components do
|
||||
for _, componentId in components do
|
||||
local map = componentIndex[componentId]
|
||||
if not map then
|
||||
return EmptyQuery
|
||||
end
|
||||
|
||||
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
|
||||
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
|
||||
firstArchetypeMap = map
|
||||
end
|
||||
end
|
||||
|
||||
local i = 0
|
||||
for id in firstArchetypeMap.sparse do
|
||||
local archetype = archetypes[id]
|
||||
local archetypeRecords = archetype.records
|
||||
local indices = {}
|
||||
local indices = {}
|
||||
local skip = false
|
||||
|
||||
for j, componentId in components do
|
||||
|
||||
for i, componentId in components do
|
||||
local index = archetypeRecords[componentId]
|
||||
if not index then
|
||||
if not index then
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
indices[j] = archetypeRecords[componentId]
|
||||
indices[i] = index
|
||||
end
|
||||
|
||||
if skip then
|
||||
if skip then
|
||||
continue
|
||||
end
|
||||
i += 1
|
||||
table.insert(compatibleArchetypes, { archetype, indices })
|
||||
|
||||
length += 1
|
||||
compatibleArchetypes[length] = {archetype, indices}
|
||||
end
|
||||
|
||||
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
||||
if not lastArchetype then
|
||||
if not lastArchetype then
|
||||
return EmptyQuery
|
||||
end
|
||||
|
||||
|
||||
local preparedQuery = {}
|
||||
preparedQuery.__index = preparedQuery
|
||||
|
||||
function preparedQuery:without(...)
|
||||
local components = { ... }
|
||||
for i = #compatibleArchetypes, 1, -1 do
|
||||
function preparedQuery:without(...)
|
||||
local withoutComponents = {...}
|
||||
for i = #compatibleArchetypes, 1, -1 do
|
||||
local archetype = compatibleArchetypes[i][1]
|
||||
local records = archetype.records
|
||||
local shouldRemove = false
|
||||
for _, componentId in components do
|
||||
if archetype.records[componentId] then
|
||||
|
||||
for _, componentId in withoutComponents do
|
||||
if records[componentId] then
|
||||
shouldRemove = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if shouldRemove then
|
||||
|
||||
if shouldRemove then
|
||||
table.remove(compatibleArchetypes, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
||||
if not lastArchetype then
|
||||
if not lastArchetype then
|
||||
return EmptyQuery
|
||||
end
|
||||
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
local lastRow
|
||||
local queryOutput = {}
|
||||
|
||||
|
||||
function preparedQuery:__iter()
|
||||
return function()
|
||||
function preparedQuery:__iter()
|
||||
return function()
|
||||
local archetype = compatibleArchetype[1]
|
||||
local row = next(archetype.entities, lastRow)
|
||||
while row == nil do
|
||||
while row == nil do
|
||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
||||
if lastArchetype == nil then
|
||||
return
|
||||
if lastArchetype == nil then
|
||||
return
|
||||
end
|
||||
archetype = compatibleArchetype[1]
|
||||
row = next(archetype.entities, row)
|
||||
end
|
||||
lastRow = row
|
||||
|
||||
|
||||
local entityId = archetype.entities[row :: number]
|
||||
local columns = archetype.columns
|
||||
local tr = compatibleArchetype[2]
|
||||
|
||||
if queryLength == 1 then
|
||||
if queryLength == 1 then
|
||||
return entityId, columns[tr[1]][row]
|
||||
elseif queryLength == 2 then
|
||||
elseif queryLength == 2 then
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row]
|
||||
elseif queryLength == 3 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row]
|
||||
elseif queryLength == 4 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row],
|
||||
columns[tr[4]][row]
|
||||
elseif queryLength == 5 then
|
||||
return entityId,
|
||||
elseif queryLength == 3 then
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
|
||||
elseif queryLength == 4 then
|
||||
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[2]][row],
|
||||
columns[tr[3]][row],
|
||||
columns[tr[4]][row],
|
||||
columns[tr[5]][row]
|
||||
elseif queryLength == 6 then
|
||||
return entityId,
|
||||
elseif queryLength == 6 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row],
|
||||
columns[tr[4]][row],
|
||||
columns[tr[5]][row],
|
||||
columns[tr[6]][row]
|
||||
elseif queryLength == 7 then
|
||||
return entityId,
|
||||
elseif queryLength == 7 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row],
|
||||
|
@ -504,8 +547,8 @@ function World.query(world: World, ...: i53): Query
|
|||
columns[tr[5]][row],
|
||||
columns[tr[6]][row],
|
||||
columns[tr[7]][row]
|
||||
elseif queryLength == 8 then
|
||||
return entityId,
|
||||
elseif queryLength == 8 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row],
|
||||
|
@ -516,8 +559,8 @@ function World.query(world: World, ...: i53): Query
|
|||
columns[tr[8]][row]
|
||||
end
|
||||
|
||||
for i in components do
|
||||
queryOutput[i] = tr[i][row]
|
||||
for i in components do
|
||||
queryOutput[i] = columns[tr[i]][row]
|
||||
end
|
||||
|
||||
return entityId, unpack(queryOutput, 1, queryLength)
|
||||
|
@ -527,72 +570,90 @@ function World.query(world: World, ...: i53): Query
|
|||
return setmetatable({}, preparedQuery) :: any
|
||||
end
|
||||
|
||||
function World.component(world: World)
|
||||
local componentId = world.nextComponentId + 1
|
||||
if componentId > HI_COMPONENT_ID then
|
||||
error("Too many components")
|
||||
function World.component(world: World)
|
||||
local componentId = world.nextComponentId + 1
|
||||
if componentId > HI_COMPONENT_ID then
|
||||
-- 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
|
||||
world.nextComponentId = componentId
|
||||
return componentId
|
||||
end
|
||||
|
||||
function World.entity(world: World)
|
||||
world.nextEntityId += 1
|
||||
return world.nextEntityId + REST
|
||||
local nextEntityId = world.nextEntityId + 1
|
||||
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
|
||||
|
||||
function World.observer(world: World, ...)
|
||||
local componentIds = { ... }
|
||||
|
||||
local componentIds = {...}
|
||||
local idsCount = #componentIds
|
||||
local hooks = world.hooks
|
||||
|
||||
return {
|
||||
event = function(event)
|
||||
local hook = world.hooks[event]
|
||||
world.hooks[event] = nil
|
||||
event = function(event)
|
||||
local hook = hooks[event]
|
||||
hooks[event] = nil
|
||||
|
||||
local last, change
|
||||
return function()
|
||||
return function()
|
||||
last, change = next(hook, last)
|
||||
if not last then
|
||||
if not last then
|
||||
return
|
||||
end
|
||||
|
||||
local matched = false
|
||||
|
||||
while not matched do
|
||||
local ids = change.ids
|
||||
|
||||
while not matched do
|
||||
local skip = false
|
||||
for _, id in change.ids do
|
||||
if not table.find(componentIds, id) then
|
||||
for _, id in ids do
|
||||
if not table.find(componentIds, id) then
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if skip then
|
||||
|
||||
if skip then
|
||||
last, change = next(hook, last)
|
||||
ids = change.ids
|
||||
continue
|
||||
end
|
||||
|
||||
matched = true
|
||||
end
|
||||
|
||||
local queryOutput = {}
|
||||
|
||||
local queryOutput = table.create(idsCount)
|
||||
local row = change.offset
|
||||
local archetype = change.archetype
|
||||
local columns = archetype.columns
|
||||
local archetypeRecords = archetype.records
|
||||
for _, id in componentIds do
|
||||
table.insert(queryOutput, columns[archetypeRecords[id]][row])
|
||||
for index, id in componentIds do
|
||||
queryOutput[index] = columns[archetypeRecords[id]][row]
|
||||
end
|
||||
|
||||
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
|
||||
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
|
||||
end
|
||||
end
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
return table.freeze({
|
||||
World = World,
|
||||
ON_ADD = ON_ADD,
|
||||
ON_REMOVE = ON_REMOVE,
|
||||
ON_SET = ON_SET
|
||||
World = World;
|
||||
ON_ADD = ON_ADD;
|
||||
ON_REMOVE = ON_REMOVE;
|
||||
ON_SET = ON_SET;
|
||||
})
|
||||
|
|
186
mkdocs.yml
Normal file
186
mkdocs.yml
Normal 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
|
1421
newMatter.lua
1421
newMatter.lua
File diff suppressed because it is too large
Load diff
1567
oldMatter.lua
1567
oldMatter.lua
File diff suppressed because it is too large
Load diff
4
selene.toml
Normal file
4
selene.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
std = "roblox"
|
||||
|
||||
[lints]
|
||||
global_usage = "allow"
|
5
stylua.toml
Normal file
5
stylua.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
column_width = 120
|
||||
quote_style = "ForceDouble"
|
||||
|
||||
[sort_requires]
|
||||
enabled = true
|
|
@ -11,9 +11,6 @@
|
|||
},
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"DevPackages": {
|
||||
"$path": "DevPackages"
|
||||
},
|
||||
"Lib": {
|
||||
"$path": "lib"
|
||||
},
|
||||
|
@ -26,15 +23,6 @@
|
|||
"mirror": {
|
||||
"$path": "mirror"
|
||||
}
|
||||
},
|
||||
"TestService": {
|
||||
"$properties": {
|
||||
"ExecuteWithStudioRun": true
|
||||
},
|
||||
"$className": "TestService",
|
||||
"run": {
|
||||
"$path": "tests.server.lua"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
3
testez-companion.toml
Normal file
3
testez-companion.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
roots = ["ServerStorage"]
|
||||
|
||||
[extraOptions]
|
|
@ -1,9 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
require(ReplicatedStorage.DevPackages.TestEZ).TestBootstrap:run({
|
||||
ReplicatedStorage.Lib,
|
||||
nil,
|
||||
{
|
||||
noXpcallByDefault = true,
|
||||
},
|
||||
})
|
115
tests/test1.lua
115
tests/test1.lua
|
@ -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
353
tests/world.lua
Normal 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()
|
||||
|
14
wally.toml
14
wally.toml
|
@ -1,15 +1,7 @@
|
|||
[package]
|
||||
name = "ukendio/jecs"
|
||||
version = "0.0.0-prototype.rc.3"
|
||||
version = "0.1.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = ["**"]
|
||||
include = ["default.project.json", "lib", "wally.toml", "README.md"]
|
||||
|
||||
[dev-dependencies]
|
||||
TestEZ = "roblox/testez@0.4.1"
|
||||
Matter = "matter-ecs/matter@0.8.0"
|
||||
ecr = "centau/ecr@0.8.0"
|
||||
|
||||
|
||||
|
||||
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
|
||||
exclude = ["**"]
|
Loading…
Reference in a new issue