mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 17:40:02 +00:00
Merge branch 'main' of https://github.com/kalrnlo/jecs
This commit is contained in:
commit
ca8e1376ca
26 changed files with 3855 additions and 1252 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
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -46,6 +46,10 @@ Packages
|
||||||
wally.lock
|
wally.lock
|
||||||
WallyPatches
|
WallyPatches
|
||||||
|
|
||||||
|
# Typescript
|
||||||
|
/node_modules
|
||||||
|
/include
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
roblox.toml
|
roblox.toml
|
||||||
sourcemap.json
|
sourcemap.json
|
||||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 jecs authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
65
README.md
65
README.md
|
@ -12,7 +12,7 @@ Just an ECS
|
||||||
jecs is a stupidly fast Entity Component System (ECS).
|
jecs is a stupidly fast Entity Component System (ECS).
|
||||||
|
|
||||||
- Entity Relationships as first class citizens
|
- Entity Relationships as first class citizens
|
||||||
- Process tens of thousands of entities with ease every frame
|
- Iterate 350,000 entities at 60 frames per second
|
||||||
- Type-safe [Luau](https://luau-lang.org/) API
|
- Type-safe [Luau](https://luau-lang.org/) API
|
||||||
- Zero-dependency package
|
- Zero-dependency package
|
||||||
- Optimized for column-major operations
|
- Optimized for column-major operations
|
||||||
|
@ -22,47 +22,40 @@ jecs is a stupidly fast Entity Component System (ECS).
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local world = World.new()
|
local world = jecs.World.new()
|
||||||
|
local pair = jecs.pair
|
||||||
|
|
||||||
local player = world:entity()
|
local ChildOf = world:component()
|
||||||
local opponent = world:entity()
|
local Name = world:component()
|
||||||
|
|
||||||
local Health = world:component()
|
local function parent(entity)
|
||||||
local Position = world:component()
|
return world:target(entity, ChildOf)
|
||||||
-- Notice how components can just be entities as well?
|
end
|
||||||
-- It allows you to model relationships easily!
|
local function getName(entity)
|
||||||
local Damage = world:entity()
|
return world:get(entity, Name)
|
||||||
local DamagedBy = 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 opponentId, opponentPosition, damage in world:query(Position, Damage) do
|
|
||||||
if playerId == opponentId then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
if (playerPosition - opponentPosition).Magnitude < 5 then
|
|
||||||
totalDamage += damage
|
|
||||||
end
|
|
||||||
-- We create a pair between the relation component `DamagedBy` and the entity id of the opponent.
|
|
||||||
-- This will allow us to specifically query for damage exerted by a specific opponent.
|
|
||||||
world:set(playerId, ECS_PAIR(DamagedBy, opponentId), totalDamage)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Gets the damage inflicted by our specific opponent!
|
local alice = world:entity()
|
||||||
for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, opponent)) do
|
world:set(alice, Name, "alice")
|
||||||
world:set(playerId, health - inflicted)
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
assert(world:get(player, Health) == 79)
|
-- Output
|
||||||
|
-- "alice"
|
||||||
|
-- bob is the child of alice
|
||||||
|
-- sara is the child of alice
|
||||||
```
|
```
|
||||||
|
|
||||||
125 archetypes, 4 random components queried.
|
125 archetypes, 4 random components queried.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[tools]
|
[tools]
|
||||||
wally = "upliftgames/wally@0.3.1"
|
wally = "upliftgames/wally@0.3.2"
|
||||||
rojo = "rojo-rbx/rojo@7.4.1"
|
rojo = "rojo-rbx/rojo@7.4.1"
|
||||||
stylua = "johnnymorganz/stylua@0.19.1"
|
stylua = "johnnymorganz/stylua@0.19.1"
|
||||||
selene = "kampfkarren/selene@0.26.1"
|
selene = "kampfkarren/selene@0.26.1"
|
||||||
|
|
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,372 +0,0 @@
|
||||||
local testkit = require("../testkit")
|
|
||||||
local jecs = require("../lib/init")
|
|
||||||
local ecr = require("../DevPackages/_Index/centau_ecr@0.8.0/ecr/src/ecr")
|
|
||||||
|
|
||||||
|
|
||||||
local BENCH, START = testkit.benchmark()
|
|
||||||
|
|
||||||
local function TITLE(title: string)
|
|
||||||
print()
|
|
||||||
print(testkit.color.white(title))
|
|
||||||
end
|
|
||||||
|
|
||||||
local N = 2^16-2
|
|
||||||
|
|
||||||
type i53 = number
|
|
||||||
|
|
||||||
do TITLE "create"
|
|
||||||
BENCH("entity", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:entity()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- component benchmarks
|
|
||||||
|
|
||||||
--todo: perform the same benchmarks for multiple components.?
|
|
||||||
-- these kind of operations only support 1 component at a time, which is
|
|
||||||
-- a shame, especially for archetypes where moving components is expensive.
|
|
||||||
|
|
||||||
do TITLE "set"
|
|
||||||
BENCH("add 1 component", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local entities = {}
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
entities[i] = world:entity()
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:set(entities[i], A, i)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("change 1 component", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local entities = {}
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local e = world:entity()
|
|
||||||
world:set(e, A, 1)
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:set(e, A, 2)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
do TITLE "remove"
|
|
||||||
BENCH("1 component", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local entities = {}
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
entities[i] = id
|
|
||||||
world:set(id, A, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:remove(entities[i], A)
|
|
||||||
end
|
|
||||||
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
do TITLE "get"
|
|
||||||
BENCH("1 component", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local entities = {}
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
entities[i] = id
|
|
||||||
world:set(id, A, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
-- ? curious why the overhead is roughly 80 ns.
|
|
||||||
world:get(entities[i], A)
|
|
||||||
end
|
|
||||||
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("2 component", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local entities = {}
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
entities[i] = id
|
|
||||||
world:set(id, A, true)
|
|
||||||
world:set(id, B, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:get(entities[i], A, B)
|
|
||||||
end
|
|
||||||
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("3 component", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local entities = {}
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
entities[i] = id
|
|
||||||
world:set(id, A, true)
|
|
||||||
world:set(id, B, true)
|
|
||||||
world:set(id, C, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:get(entities[i], A, B, C)
|
|
||||||
end
|
|
||||||
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("4 component", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local entities = {}
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
local D = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
entities[i] = id
|
|
||||||
world:set(id, A, true)
|
|
||||||
world:set(id, B, true)
|
|
||||||
world:set(id, C, true)
|
|
||||||
world:set(id, D, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:get(entities[i], A, B, C, D)
|
|
||||||
end
|
|
||||||
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
do TITLE (testkit.color.white_underline("Jecs query"))
|
|
||||||
|
|
||||||
local function count(query: () -> ())
|
|
||||||
local n = 0
|
|
||||||
for _ in query do
|
|
||||||
n += 1
|
|
||||||
end
|
|
||||||
return n
|
|
||||||
end
|
|
||||||
|
|
||||||
local function flip()
|
|
||||||
return math.random() > 0.5
|
|
||||||
end
|
|
||||||
|
|
||||||
local function view_bench(
|
|
||||||
world: jecs.World,
|
|
||||||
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53
|
|
||||||
)
|
|
||||||
|
|
||||||
BENCH("1 component", function()
|
|
||||||
START(count(world:query(A)))
|
|
||||||
for _ in world:query(A) do end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("2 component", function()
|
|
||||||
START(count(world:query(A, B)))
|
|
||||||
for _ in world:query(A, B) do end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("4 component", function()
|
|
||||||
START(count(world:query(A, B, C, D)))
|
|
||||||
for _ in world:query(A, B, C, D) do end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("8 component", function()
|
|
||||||
START(count(world:query(A, B, C, D, E, F, G, H)))
|
|
||||||
for _ in world:query(A, B, C, D, E, F, G, H) do end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
do TITLE "random components"
|
|
||||||
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
local D = world:component()
|
|
||||||
local E = world:component()
|
|
||||||
local F = world:component()
|
|
||||||
local G = world:component()
|
|
||||||
local H = world:component()
|
|
||||||
local I = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
if flip() then world:set(id, A, true) end
|
|
||||||
if flip() then world:set(id, B, true) end
|
|
||||||
if flip() then world:set(id, C, true) end
|
|
||||||
if flip() then world:set(id, D, true) end
|
|
||||||
if flip() then world:set(id, E, true) end
|
|
||||||
if flip() then world:set(id, F, true) end
|
|
||||||
if flip() then world:set(id, G, true) end
|
|
||||||
if flip() then world:set(id, H, true) end
|
|
||||||
if flip() then world:set(id, I, true) end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(world, A, B, C, D, E, F, G, H, I)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
do TITLE "one component in common"
|
|
||||||
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
local D = world:component()
|
|
||||||
local E = world:component()
|
|
||||||
local F = world:component()
|
|
||||||
local G = world:component()
|
|
||||||
local H = world:component()
|
|
||||||
local I = world:component()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world:entity()
|
|
||||||
local a = true
|
|
||||||
if flip() then world:set(id, B, true) else a = false end
|
|
||||||
if flip() then world:set(id, C, true) else a = false end
|
|
||||||
if flip() then world:set(id, D, true) else a = false end
|
|
||||||
if flip() then world:set(id, E, true) else a = false end
|
|
||||||
if flip() then world:set(id, F, true) else a = false end
|
|
||||||
if flip() then world:set(id, G, true) else a = false end
|
|
||||||
if flip() then world:set(id, H, true) else a = false end
|
|
||||||
if flip() then world:set(id, I, true) else a = false end
|
|
||||||
if a then world:set(id, A, true) end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(world, A, B, C, D, E, F, G, H, I)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
do TITLE (testkit.color.white_underline("ECR query"))
|
|
||||||
|
|
||||||
local A = ecr.component()
|
|
||||||
local B = ecr.component()
|
|
||||||
local C = ecr.component()
|
|
||||||
local D = ecr.component()
|
|
||||||
local E = ecr.component()
|
|
||||||
local F = ecr.component()
|
|
||||||
local G = ecr.component()
|
|
||||||
local H = ecr.component()
|
|
||||||
local I = ecr.component()
|
|
||||||
|
|
||||||
local function count(query: () -> ())
|
|
||||||
local n = 0
|
|
||||||
for _ in query do
|
|
||||||
n += 1
|
|
||||||
end
|
|
||||||
return n
|
|
||||||
end
|
|
||||||
|
|
||||||
local function flip()
|
|
||||||
return math.random() > 0.5
|
|
||||||
end
|
|
||||||
|
|
||||||
local function view_bench(
|
|
||||||
world: ecr.Registry,
|
|
||||||
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53
|
|
||||||
)
|
|
||||||
|
|
||||||
BENCH("1 component", function()
|
|
||||||
START(count(world:view(A)))
|
|
||||||
for _ in world:view(A) do end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("2 component", function()
|
|
||||||
START(count(world:view(A, B)))
|
|
||||||
for _ in world:view(A, B) do end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("4 component", function()
|
|
||||||
START(count(world:view(A, B, C, D)))
|
|
||||||
for _ in world:view(A, B, C, D) do end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("8 component", function()
|
|
||||||
START(count(world:view(A, B, C, D, E, F, G, H)))
|
|
||||||
for _ in world:view(A, B, C, D, E, F, G, H) do end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
do TITLE "random components"
|
|
||||||
local world = ecr.registry()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world.create()
|
|
||||||
if flip() then world:set(id, A, true) end
|
|
||||||
if flip() then world:set(id, B, true) end
|
|
||||||
if flip() then world:set(id, C, true) end
|
|
||||||
if flip() then world:set(id, D, true) end
|
|
||||||
if flip() then world:set(id, E, true) end
|
|
||||||
if flip() then world:set(id, F, true) end
|
|
||||||
if flip() then world:set(id, G, true) end
|
|
||||||
if flip() then world:set(id, H, true) end
|
|
||||||
if flip() then world:set(id, I, true) end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(world, A, B, C, D, E, F, G, H, I)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
do TITLE "one component in common"
|
|
||||||
|
|
||||||
local world = ecr.registry()
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local id = world.create()
|
|
||||||
local a = true
|
|
||||||
if flip() then world:set(id, B, true) else a = false end
|
|
||||||
if flip() then world:set(id, C, true) else a = false end
|
|
||||||
if flip() then world:set(id, D, true) else a = false end
|
|
||||||
if flip() then world:set(id, E, true) else a = false end
|
|
||||||
if flip() then world:set(id, F, true) else a = false end
|
|
||||||
if flip() then world:set(id, G, true) else a = false end
|
|
||||||
if flip() then world:set(id, H, true) else a = false end
|
|
||||||
if flip() then world:set(id, I, true) else a = false end
|
|
||||||
if a then world:set(id, A, true) end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(world, A, B, C, D, E, F, G, H, I)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -2,7 +2,6 @@
|
||||||
--!native
|
--!native
|
||||||
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
local rgb = require(ReplicatedStorage.rgb)
|
|
||||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||||
local jecs = require(ReplicatedStorage.Lib)
|
local jecs = require(ReplicatedStorage.Lib)
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
local rgb = require(ReplicatedStorage.rgb)
|
local rgb = require(ReplicatedStorage.rgb)
|
||||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter)
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr)
|
||||||
local newWorld = Matter.World.new()
|
local newWorld = Matter.World.new()
|
||||||
|
|
||||||
local jecs = require(ReplicatedStorage.Lib)
|
local jecs = require(ReplicatedStorage.Shim)
|
||||||
local mirror = require(ReplicatedStorage.mirror)
|
local mirror = require(ReplicatedStorage.mirror)
|
||||||
local mcs = mirror.World.new()
|
local mcs = mirror.World.new()
|
||||||
local ecs = jecs.World.new()
|
local ecs = jecs.World.new()
|
||||||
|
@ -177,6 +177,13 @@ return {
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
Matter = function()
|
||||||
|
local matched = 0
|
||||||
|
for entityId, firstComponent in newWorld:query(A1, A4, A6, A8) do
|
||||||
|
matched += 1
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
ECR = function()
|
ECR = function()
|
||||||
local matched = 0
|
local matched = 0
|
||||||
for entityId, firstComponent in registry2:view(B1, B4, B6, B8) do
|
for entityId, firstComponent in registry2:view(B1, B4, B6, B8) do
|
||||||
|
|
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 |
BIN
image-3.png
BIN
image-3.png
Binary file not shown.
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 76 KiB |
153
lib/index.d.ts
vendored
Normal file
153
lib/index.d.ts
vendored
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
type Query<T extends unknown[]> = {
|
||||||
|
without: (...components: Entity[]) => Query<T>;
|
||||||
|
} & IterableFunction<LuaTuple<[Entity, ...T]>>;
|
||||||
|
|
||||||
|
// Utility Types
|
||||||
|
export type Entity<T = unknown> = number & { __nominal_type_dont_use: T };
|
||||||
|
export type EntityType<T> = T extends Entity<infer A> ? A : never;
|
||||||
|
export type InferComponents<A extends Entity[]> = {
|
||||||
|
[K in keyof A]: EntityType<A[K]>;
|
||||||
|
};
|
||||||
|
type Nullable<T extends unknown[]> = {
|
||||||
|
[K in keyof T]: T[K] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class World {
|
||||||
|
/**
|
||||||
|
* Creates a new World
|
||||||
|
*/
|
||||||
|
constructor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new entity
|
||||||
|
* @returns Entity
|
||||||
|
*/
|
||||||
|
entity(): Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new entity located in the first 256 ids.
|
||||||
|
* These should be used for static components for fast access.
|
||||||
|
* @returns Entity<T>
|
||||||
|
*/
|
||||||
|
component<T = unknown>(): Entity<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the target of a relationship. For example, when a user calls
|
||||||
|
* `world.target(id, ChildOf(parent))`, you will obtain the parent entity.
|
||||||
|
* @param id Entity
|
||||||
|
* @param relation The Relationship
|
||||||
|
* @returns The Parent Entity if it exists
|
||||||
|
*/
|
||||||
|
target(id: Entity, relation: Entity): Entity | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an entity and all its related components and relationships.
|
||||||
|
* @param id Entity to be destroyed
|
||||||
|
*/
|
||||||
|
delete(id: Entity): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a component to the entity with no value
|
||||||
|
* @param id Target Entity
|
||||||
|
* @param component Component
|
||||||
|
*/
|
||||||
|
add<T>(id: Entity, component: Entity<T>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns a value to a component on the given entity
|
||||||
|
* @param id Target Entity
|
||||||
|
* @param component Target Component
|
||||||
|
* @param data Component Data
|
||||||
|
*/
|
||||||
|
set<T>(id: Entity, component: Entity<T>, data: T): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a component from the given entity
|
||||||
|
* @param id Target Entity
|
||||||
|
* @param component Target Component
|
||||||
|
*/
|
||||||
|
remove(id: Entity, component: Entity): void;
|
||||||
|
|
||||||
|
// Manually typed out get since there is a hard limit.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the value of one component. This value may be undefined.
|
||||||
|
* @param id Target Entity
|
||||||
|
* @param component Target Component
|
||||||
|
* @returns Data associated with the component if it exists
|
||||||
|
*/
|
||||||
|
get<A>(id: number, component: Entity<A>): A | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the value of two components. This value may be undefined.
|
||||||
|
* @param id Target Entity
|
||||||
|
* @param component Target Component 1
|
||||||
|
* @param component2 Target Component 2
|
||||||
|
* @returns Data associated with the components if it exists
|
||||||
|
*/
|
||||||
|
get<A, B>(
|
||||||
|
id: number,
|
||||||
|
component: Entity<A>,
|
||||||
|
component2: Entity<B>
|
||||||
|
): LuaTuple<Nullable<[A, B]>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the value of three components. This value may be undefined.
|
||||||
|
* @param id Target Entity
|
||||||
|
* @param component Target Component 1
|
||||||
|
* @param component2 Target Component 2
|
||||||
|
* @param component3 Target Component 3
|
||||||
|
* @returns Data associated with the components if it exists
|
||||||
|
*/
|
||||||
|
get<A, B, C>(
|
||||||
|
id: number,
|
||||||
|
component: Entity<A>,
|
||||||
|
component2: Entity<B>,
|
||||||
|
component3: Entity<C>
|
||||||
|
): LuaTuple<Nullable<[A, B, C]>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the value of four components. This value may be undefined.
|
||||||
|
* @param id Target Entity
|
||||||
|
* @param component Target Component 1
|
||||||
|
* @param component2 Target Component 2
|
||||||
|
* @param component3 Target Component 3
|
||||||
|
* @param component4 Target Component 4
|
||||||
|
* @returns Data associated with the components if it exists
|
||||||
|
*/
|
||||||
|
get<A, B, C, D>(
|
||||||
|
id: number,
|
||||||
|
component: Entity<A>,
|
||||||
|
component2: Entity<B>,
|
||||||
|
component3: Entity<C>,
|
||||||
|
component4: Entity<D>
|
||||||
|
): LuaTuple<Nullable<[A, B, C, D]>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches the world for entities that match a given query
|
||||||
|
* @param components Queried Components
|
||||||
|
* @returns Iterable function
|
||||||
|
*/
|
||||||
|
query<T extends Entity[]>(...components: T): Query<InferComponents<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a composite key.
|
||||||
|
* @param pred The first entity
|
||||||
|
* @param obj The second entity
|
||||||
|
* @returns The composite key
|
||||||
|
*/
|
||||||
|
export const pair: (pred: Entity, obj: Entity) => Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the entity is a composite key
|
||||||
|
* @param e The entity to check
|
||||||
|
* @returns If the entity is a pair
|
||||||
|
*/
|
||||||
|
export const IS_PAIR: (e: Entity) => boolean;
|
||||||
|
|
||||||
|
export const OnAdd: Entity;
|
||||||
|
export const OnRemove: Entity;
|
||||||
|
export const OnSet: Entity;
|
||||||
|
export const Wildcard: Entity;
|
||||||
|
export const Rest: Entity;
|
596
lib/init.lua
596
lib/init.lua
|
@ -6,38 +6,57 @@
|
||||||
type i53 = number
|
type i53 = number
|
||||||
type i24 = number
|
type i24 = number
|
||||||
|
|
||||||
type Ty = {i53}
|
type Ty = { i53 }
|
||||||
type ArchetypeId = number
|
type ArchetypeId = number
|
||||||
|
|
||||||
type Column = {any}
|
type Column = { any }
|
||||||
|
|
||||||
type Archetype = {
|
type Archetype = {
|
||||||
id: number,
|
id: number,
|
||||||
edges: {
|
edges: {
|
||||||
[i24]: {
|
[i53]: {
|
||||||
add: Archetype,
|
add: Archetype,
|
||||||
remove: Archetype,
|
remove: Archetype,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
types: Ty,
|
types: Ty,
|
||||||
type: string | number,
|
type: string | number,
|
||||||
entities: {number},
|
entities: { number },
|
||||||
columns: {Column},
|
columns: { Column },
|
||||||
records: {},
|
records: { [number]: number },
|
||||||
}
|
}
|
||||||
|
|
||||||
type Record = {
|
type Record = {
|
||||||
archetype: Archetype,
|
archetype: Archetype,
|
||||||
row: number,
|
row: number,
|
||||||
dense: i24,
|
dense: i24,
|
||||||
|
componentRecord: ArchetypeMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}}
|
type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } }
|
||||||
type ComponentIndex = {[i24]: ArchetypeMap}
|
|
||||||
|
|
||||||
type ArchetypeRecord = number
|
type ArchetypeRecord = number
|
||||||
type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
|
--[[
|
||||||
type Archetypes = {[ArchetypeId]: Archetype}
|
TODO:
|
||||||
|
{
|
||||||
|
index: number,
|
||||||
|
count: number,
|
||||||
|
column: number
|
||||||
|
}
|
||||||
|
|
||||||
|
]]
|
||||||
|
|
||||||
|
type ArchetypeMap = {
|
||||||
|
cache: { ArchetypeRecord },
|
||||||
|
first: ArchetypeMap,
|
||||||
|
second: ArchetypeMap,
|
||||||
|
parent: ArchetypeMap,
|
||||||
|
size: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentIndex = { [i24]: ArchetypeMap }
|
||||||
|
|
||||||
|
type Archetypes = { [ArchetypeId]: Archetype }
|
||||||
|
|
||||||
type ArchetypeDiff = {
|
type ArchetypeDiff = {
|
||||||
added: Ty,
|
added: Ty,
|
||||||
|
@ -75,6 +94,7 @@ local function addFlags(isPair: boolean)
|
||||||
return typeFlags
|
return typeFlags
|
||||||
end
|
end
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
local function newId(source: number, target: number): number
|
local function newId(source: number, target: number): number
|
||||||
return ((source * 2^28) + target) * ECS_ID_FLAGS_MASK
|
return ((source * 2^28) + target) * ECS_ID_FLAGS_MASK
|
||||||
end
|
end
|
||||||
|
@ -87,20 +107,44 @@ local function separate(entity: number): (number, number, number)
|
||||||
local type_flags = entity % 0x10
|
local type_flags = entity % 0x10
|
||||||
local entity = entity // ECS_ID_FLAGS_MASK
|
local entity = entity // ECS_ID_FLAGS_MASK
|
||||||
return new_entity // ECS_ENTITY_MASK, new_entity % ECS_GENERATION_MASK, type_flags
|
return new_entity // ECS_ENTITY_MASK, new_entity % ECS_GENERATION_MASK, type_flags
|
||||||
|
=======
|
||||||
|
local function ECS_COMBINE(source: number, target: number): i53
|
||||||
|
local e = source * 268435456 + target * ECS_ID_FLAGS_MASK
|
||||||
|
return e
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ECS_IS_PAIR(e: number)
|
||||||
|
if e > ECS_ENTITY_MASK then
|
||||||
|
return (e % 2 ^ 4) // FLAGS_PAIR ~= 0
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
>>>>>>> eae51988a9e3ca45e39ebcfdbea0f9f8706bd3cd
|
||||||
end
|
end
|
||||||
|
|
||||||
-- HIGH 24 bits LOW 24 bits
|
-- HIGH 24 bits LOW 24 bits
|
||||||
local function ECS_GENERATION(e: i53)
|
local function ECS_GENERATION(e: i53)
|
||||||
|
<<<<<<< HEAD
|
||||||
return (e // 0x10) % ECS_GENERATION_MASK
|
return (e // 0x10) % ECS_GENERATION_MASK
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ECS_ID(e: i53)
|
local function ECS_ID(e: i53)
|
||||||
return (e // 0x10) // ECS_ENTITY_MASK
|
return (e // 0x10) // ECS_ENTITY_MASK
|
||||||
|
=======
|
||||||
|
if e > ECS_ENTITY_MASK then
|
||||||
|
e = e // 0x10
|
||||||
|
return e % ECS_GENERATION_MASK
|
||||||
|
end
|
||||||
|
return 0
|
||||||
|
>>>>>>> eae51988a9e3ca45e39ebcfdbea0f9f8706bd3cd
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ECS_GENERATION_INC(e: i53)
|
local function ECS_GENERATION_INC(e: i53)
|
||||||
local id, generation, flags = separate(e)
|
if e > ECS_ENTITY_MASK then
|
||||||
|
local flags = e // 0x10
|
||||||
|
local id = flags // ECS_ENTITY_MASK
|
||||||
|
local generation = flags % ECS_GENERATION_MASK
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
return newId(id, generation + 1) + flags
|
return newId(id, generation + 1) + flags
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -132,21 +176,60 @@ end
|
||||||
|
|
||||||
local function getAlive(entityIndex: EntityIndex, id: i53)
|
local function getAlive(entityIndex: EntityIndex, id: i53)
|
||||||
return entityIndex.dense[id]
|
return entityIndex.dense[id]
|
||||||
|
=======
|
||||||
|
return ECS_COMBINE(id, generation + 1) + flags
|
||||||
|
end
|
||||||
|
return ECS_COMBINE(e, 1)
|
||||||
|
>>>>>>> eae51988a9e3ca45e39ebcfdbea0f9f8706bd3cd
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ecs_get_source(entityIndex, e)
|
-- FIRST gets the high ID
|
||||||
assert(ECS_IS_PAIR(e))
|
local function ECS_ENTITY_T_HI(e: i53): i24
|
||||||
return getAlive(entityIndex, ECS_PAIR_FIRST(e))
|
if e > ECS_ENTITY_MASK then
|
||||||
end
|
e = e // 0x10
|
||||||
local function ecs_get_target(entityIndex, e)
|
return e % ECS_ENTITY_MASK
|
||||||
assert(ECS_IS_PAIR(e))
|
end
|
||||||
return getAlive(entityIndex, ECS_PAIR_SECOND(e))
|
return e
|
||||||
end
|
end
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
local function nextEntityId(entityIndex, index: i24): i53
|
local function nextEntityId(entityIndex, index: i24): i53
|
||||||
local id = newId(index, 0)
|
local id = newId(index, 0)
|
||||||
|
=======
|
||||||
|
-- SECOND
|
||||||
|
local function ECS_ENTITY_T_LO(e: i53): i24
|
||||||
|
if e > ECS_ENTITY_MASK then
|
||||||
|
e = e // 0x10
|
||||||
|
return e // ECS_ENTITY_MASK
|
||||||
|
end
|
||||||
|
return e
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ECS_PAIR(pred: i53, obj: i53): i53
|
||||||
|
return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + addFlags(--[[isPair]] true) :: i53
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getAlive(entityIndex: EntityIndex, id: i24)
|
||||||
|
local entityId = entityIndex.dense[id]
|
||||||
|
return entityId
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
|
||||||
|
local function ECS_PAIR_RELATION(entityIndex, e)
|
||||||
|
return getAlive(entityIndex, ECS_ENTITY_T_HI(e))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
|
||||||
|
local function ECS_PAIR_OBJECT(entityIndex, e)
|
||||||
|
return getAlive(entityIndex, ECS_ENTITY_T_LO(e))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nextEntityId(entityIndex: EntityIndex, index: i24): i53
|
||||||
|
--local id = ECS_COMBINE(index, 0)
|
||||||
|
local id = index
|
||||||
|
>>>>>>> eae51988a9e3ca45e39ebcfdbea0f9f8706bd3cd
|
||||||
entityIndex.sparse[id] = {
|
entityIndex.sparse[id] = {
|
||||||
dense = index
|
dense = index,
|
||||||
} :: Record
|
} :: Record
|
||||||
entityIndex.dense[index] = id
|
entityIndex.dense[index] = id
|
||||||
|
|
||||||
|
@ -198,7 +281,7 @@ local function transitionArchetype(
|
||||||
sourceEntities[sourceRow] = e2
|
sourceEntities[sourceRow] = e2
|
||||||
end
|
end
|
||||||
|
|
||||||
sourceEntities[movedAway] = nil
|
sourceEntities[movedAway] = nil :: any
|
||||||
destinationEntities[destinationRow] = e1
|
destinationEntities[destinationRow] = e1
|
||||||
|
|
||||||
local record1 = sparse[e1]
|
local record1 = sparse[e1]
|
||||||
|
@ -222,7 +305,7 @@ local function newEntity(entityId: i53, record: Record, archetype: Archetype)
|
||||||
return record
|
return record
|
||||||
end
|
end
|
||||||
|
|
||||||
local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype)
|
local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Record, to: Archetype)
|
||||||
local sourceRow = record.row
|
local sourceRow = record.row
|
||||||
local from = record.archetype
|
local from = record.archetype
|
||||||
local destinationRow = archetypeAppend(entityId, to)
|
local destinationRow = archetypeAppend(entityId, to)
|
||||||
|
@ -231,58 +314,75 @@ local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archet
|
||||||
record.row = destinationRow
|
record.row = destinationRow
|
||||||
end
|
end
|
||||||
|
|
||||||
local function hash(arr): string | number
|
local function hash(arr): string
|
||||||
return table.concat(arr, "_")
|
return table.concat(arr, "_")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function createArchetypeRecord(componentIndex, id, componentId, i)
|
local function ensureComponentRecord(
|
||||||
|
componentIndex: ComponentIndex,
|
||||||
|
archetypeId: number,
|
||||||
|
componentId: number,
|
||||||
|
i: number
|
||||||
|
): ArchetypeMap
|
||||||
local archetypesMap = componentIndex[componentId]
|
local archetypesMap = componentIndex[componentId]
|
||||||
|
|
||||||
if not archetypesMap then
|
if not archetypesMap then
|
||||||
archetypesMap = {size = 0, sparse = {}}
|
archetypesMap = ({ size = 0, cache = {} } :: any) :: ArchetypeMap
|
||||||
componentIndex[componentId] = archetypesMap
|
componentIndex[componentId] = archetypesMap
|
||||||
end
|
end
|
||||||
archetypesMap.sparse[id] = i
|
|
||||||
|
archetypesMap.cache[archetypeId] = i
|
||||||
|
archetypesMap.size += 1
|
||||||
|
|
||||||
|
return archetypesMap
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
|
local function ECS_ID_IS_WILDCARD(e)
|
||||||
|
assert(ECS_IS_PAIR(e))
|
||||||
|
local first = ECS_ENTITY_T_HI(e)
|
||||||
|
local second = ECS_ENTITY_T_LO(e)
|
||||||
|
return first == WILDCARD or second == WILDCARD
|
||||||
|
end
|
||||||
|
|
||||||
|
local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype
|
||||||
local ty = hash(types)
|
local ty = hash(types)
|
||||||
|
|
||||||
local id = world.nextArchetypeId + 1
|
local id = world.nextArchetypeId + 1
|
||||||
world.nextArchetypeId = id
|
world.nextArchetypeId = id
|
||||||
|
|
||||||
local length = #types
|
local length = #types
|
||||||
local columns = table.create(length)
|
local columns = (table.create(length) :: any) :: { Column }
|
||||||
|
local componentIndex = world.componentIndex
|
||||||
|
|
||||||
local records = {}
|
local records = {}
|
||||||
local componentIndex = world.componentIndex
|
|
||||||
local entityIndex = world.entityIndex
|
|
||||||
for i, componentId in types do
|
for i, componentId in types do
|
||||||
createArchetypeRecord(componentIndex, id, componentId, i)
|
ensureComponentRecord(componentIndex, id, componentId, i)
|
||||||
records[componentId] = i
|
records[componentId] = i
|
||||||
columns[i] = {}
|
|
||||||
|
|
||||||
if ECS_IS_PAIR(componentId) then
|
if ECS_IS_PAIR(componentId) then
|
||||||
local first = ecs_get_source(entityIndex, componentId)
|
local relation = ECS_PAIR_RELATION(world.entityIndex, componentId)
|
||||||
local second = ecs_get_target(entityIndex, componentId)
|
local object = ECS_PAIR_OBJECT(world.entityIndex, componentId)
|
||||||
local firstPair = ECS_PAIR(first, WILDCARD)
|
|
||||||
local secondPair = ECS_PAIR(WILDCARD, second)
|
local idr_r = ECS_PAIR(relation, WILDCARD)
|
||||||
createArchetypeRecord(componentIndex, id, firstPair, i)
|
ensureComponentRecord(componentIndex, id, idr_r, i)
|
||||||
createArchetypeRecord(componentIndex, id, secondPair, i)
|
records[idr_r] = i
|
||||||
records[firstPair] = i
|
|
||||||
records[secondPair] = i
|
local idr_t = ECS_PAIR(WILDCARD, object)
|
||||||
|
ensureComponentRecord(componentIndex, id, idr_t, i)
|
||||||
|
records[idr_t] = i
|
||||||
end
|
end
|
||||||
|
columns[i] = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local archetype = {
|
local archetype: Archetype = {
|
||||||
columns = columns;
|
columns = columns,
|
||||||
edges = {};
|
edges = {},
|
||||||
entities = {};
|
entities = {},
|
||||||
id = id;
|
id = id,
|
||||||
records = records;
|
records = records,
|
||||||
type = ty;
|
type = ty,
|
||||||
types = types;
|
types = types,
|
||||||
}
|
}
|
||||||
|
|
||||||
world.archetypeIndex[ty] = archetype
|
world.archetypeIndex[ty] = archetype
|
||||||
world.archetypes[id] = archetype
|
world.archetypes[id] = archetype
|
||||||
|
|
||||||
|
@ -293,25 +393,27 @@ local World = {}
|
||||||
World.__index = World
|
World.__index = World
|
||||||
function World.new()
|
function World.new()
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
archetypeIndex = {};
|
archetypeIndex = {} :: { [string]: Archetype },
|
||||||
archetypes = {} :: Archetypes;
|
archetypes = {} :: Archetypes,
|
||||||
componentIndex = {} :: ComponentIndex;
|
componentIndex = {} :: ComponentIndex,
|
||||||
entityIndex = {
|
entityIndex = {
|
||||||
dense = {},
|
dense = {} :: { [i24]: i53 },
|
||||||
sparse = {}
|
sparse = {} :: { [i53]: Record },
|
||||||
} :: EntityIndex;
|
} :: EntityIndex,
|
||||||
hooks = {
|
hooks = {
|
||||||
[ON_ADD] = {};
|
[ON_ADD] = {},
|
||||||
};
|
},
|
||||||
nextArchetypeId = 0;
|
nextArchetypeId = 0,
|
||||||
nextComponentId = 0;
|
nextComponentId = 0,
|
||||||
nextEntityId = 0;
|
nextEntityId = 0,
|
||||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||||
}, World)
|
}, World)
|
||||||
self.ROOT_ARCHETYPE = archetypeOf(self, {})
|
self.ROOT_ARCHETYPE = archetypeOf(self, {})
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
export type World = typeof(World.new())
|
||||||
|
|
||||||
function World.component(world: World)
|
function World.component(world: World)
|
||||||
local componentId = world.nextComponentId + 1
|
local componentId = world.nextComponentId + 1
|
||||||
if componentId > HI_COMPONENT_ID then
|
if componentId > HI_COMPONENT_ID then
|
||||||
|
@ -329,8 +431,32 @@ function World.entity(world: World)
|
||||||
return nextEntityId(world.entityIndex, entityId + REST)
|
return nextEntityId(world.entityIndex, entityId + REST)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- TODO:
|
||||||
|
-- should have an additional `index` parameter which selects the nth target
|
||||||
|
-- this is important when an entity can have multiple relationships with the same target
|
||||||
|
function World.target(world: World, entity: i53, relation: i24): i24?
|
||||||
|
local entityIndex = world.entityIndex
|
||||||
|
local record = entityIndex.sparse[entity]
|
||||||
|
local archetype = record.archetype
|
||||||
|
if not archetype then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)]
|
||||||
|
if not componentRecord then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local archetypeRecord = componentRecord.cache[archetype.id]
|
||||||
|
if not archetypeRecord then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord])
|
||||||
|
end
|
||||||
|
|
||||||
-- should reuse this logic in World.set instead of swap removing in transition archetype
|
-- should reuse this logic in World.set instead of swap removing in transition archetype
|
||||||
local function destructColumns(columns, count, row)
|
local function destructColumns(columns: { Column }, count: number, row: number)
|
||||||
if row == count then
|
if row == count then
|
||||||
for _, column in columns do
|
for _, column in columns do
|
||||||
column[count] = nil
|
column[count] = nil
|
||||||
|
@ -343,46 +469,58 @@ local function destructColumns(columns, count, row)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeDelete(entityIndex, record: Record, entityId: i53, destruct: boolean)
|
local function archetypeDelete(world: World, id: i53)
|
||||||
|
local componentIndex = world.componentIndex
|
||||||
|
local archetypesMap = componentIndex[id]
|
||||||
|
local archetypes = world.archetypes
|
||||||
|
if archetypesMap then
|
||||||
|
for archetypeId in archetypesMap.cache do
|
||||||
|
for _, entity in archetypes[archetypeId].entities do
|
||||||
|
world:remove(entity, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
componentIndex[id] = nil :: any
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function World.delete(world: World, entityId: i53)
|
||||||
|
local record = world.entityIndex.sparse[entityId]
|
||||||
|
if not record then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local entityIndex = world.entityIndex
|
||||||
local sparse, dense = entityIndex.sparse, entityIndex.dense
|
local sparse, dense = entityIndex.sparse, entityIndex.dense
|
||||||
local archetype = record.archetype
|
local archetype = record.archetype
|
||||||
local row = record.row
|
local row = record.row
|
||||||
|
|
||||||
|
archetypeDelete(world, entityId)
|
||||||
|
-- TODO: should traverse linked )component records to pairs including entityId
|
||||||
|
archetypeDelete(world, ECS_PAIR(entityId, WILDCARD))
|
||||||
|
archetypeDelete(world, ECS_PAIR(WILDCARD, entityId))
|
||||||
|
|
||||||
|
if archetype then
|
||||||
local entities = archetype.entities
|
local entities = archetype.entities
|
||||||
local last = #entities
|
local last = #entities
|
||||||
|
|
||||||
local entityToMove = entities[last]
|
|
||||||
|
|
||||||
if row ~= last then
|
if row ~= last then
|
||||||
|
local entityToMove = entities[last]
|
||||||
dense[record.dense] = entityToMove
|
dense[record.dense] = entityToMove
|
||||||
sparse[entityToMove] = record
|
sparse[entityToMove] = record
|
||||||
end
|
end
|
||||||
|
|
||||||
sparse[entityId] = nil
|
entities[row], entities[last] = entities[last], nil :: any
|
||||||
dense[#dense] = nil
|
|
||||||
|
|
||||||
entities[row], entities[last] = entities[last], nil
|
|
||||||
|
|
||||||
local columns = archetype.columns
|
local columns = archetype.columns
|
||||||
|
|
||||||
if not destruct then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
destructColumns(columns, last, row)
|
destructColumns(columns, last, row)
|
||||||
end
|
|
||||||
|
|
||||||
function World.delete(world: World, entityId: i53)
|
|
||||||
local entityIndex = world.entityIndex
|
|
||||||
local record = entityIndex.sparse[entityId]
|
|
||||||
if not record then
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
archetypeDelete(entityIndex, record, entityId, true)
|
|
||||||
|
sparse[entityId] = nil :: any
|
||||||
|
dense[#dense] = nil :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
export type World = typeof(World.new())
|
local function ensureArchetype(world: World, types, prev): Archetype
|
||||||
|
|
||||||
local function ensureArchetype(world: World, types, prev)
|
|
||||||
if #types < 1 then
|
if #types < 1 then
|
||||||
return world.ROOT_ARCHETYPE
|
return world.ROOT_ARCHETYPE
|
||||||
end
|
end
|
||||||
|
@ -396,7 +534,7 @@ local function ensureArchetype(world: World, types, prev)
|
||||||
return archetypeOf(world, types, prev)
|
return archetypeOf(world, types, prev)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function findInsert(types: {i53}, toAdd: i53)
|
local function findInsert(types: { i53 }, toAdd: i53)
|
||||||
for i, id in types do
|
for i, id in types do
|
||||||
if id == toAdd then
|
if id == toAdd then
|
||||||
return -1
|
return -1
|
||||||
|
@ -414,7 +552,7 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
|
||||||
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
||||||
-- point in the types array.
|
-- point in the types array.
|
||||||
|
|
||||||
local destinationType = table.clone(node.types)
|
local destinationType = table.clone(node.types) :: { i53 }
|
||||||
local at = findInsert(types, componentId)
|
local at = findInsert(types, componentId)
|
||||||
if at == -1 then
|
if at == -1 then
|
||||||
-- If it finds a duplicate, it just means it is the same archetype so it can return it
|
-- If it finds a duplicate, it just means it is the same archetype so it can return it
|
||||||
|
@ -499,7 +637,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc
|
||||||
|
|
||||||
local remove = edge.remove
|
local remove = edge.remove
|
||||||
if not remove then
|
if not remove then
|
||||||
local to = table.clone(from.types)
|
local to = table.clone(from.types) :: { i53 }
|
||||||
local at = table.find(to, componentId)
|
local at = table.find(to, componentId)
|
||||||
if not at then
|
if not at then
|
||||||
return from
|
return from
|
||||||
|
@ -526,6 +664,10 @@ end
|
||||||
-- Keeping the function as small as possible to enable inlining
|
-- Keeping the function as small as possible to enable inlining
|
||||||
local function get(record: Record, componentId: i24)
|
local function get(record: Record, componentId: i24)
|
||||||
local archetype = record.archetype
|
local archetype = record.archetype
|
||||||
|
if not archetype then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
local archetypeRecord = archetype.records[componentId]
|
local archetypeRecord = archetype.records[componentId]
|
||||||
|
|
||||||
if not archetypeRecord then
|
if not archetypeRecord then
|
||||||
|
@ -535,7 +677,7 @@ local function get(record: Record, componentId: i24)
|
||||||
return archetype.columns[archetypeRecord][record.row]
|
return archetype.columns[archetypeRecord][record.row]
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): any
|
||||||
local id = entityId
|
local id = entityId
|
||||||
local record = world.entityIndex.sparse[id]
|
local record = world.entityIndex.sparse[id]
|
||||||
if not record then
|
if not record then
|
||||||
|
@ -559,33 +701,35 @@ end
|
||||||
|
|
||||||
-- the less creation the better
|
-- the less creation the better
|
||||||
local function actualNoOperation() end
|
local function actualNoOperation() end
|
||||||
local function noop(_self: Query, ...: i53): () -> (number, ...any)
|
local function noop(_self: Query, ...): () -> ()
|
||||||
return actualNoOperation :: any
|
return actualNoOperation :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
local EmptyQuery = {
|
local EmptyQuery = {
|
||||||
__iter = noop;
|
__iter = noop,
|
||||||
without = noop;
|
without = noop,
|
||||||
}
|
}
|
||||||
EmptyQuery.__index = EmptyQuery
|
EmptyQuery.__index = EmptyQuery
|
||||||
setmetatable(EmptyQuery, EmptyQuery)
|
setmetatable(EmptyQuery, EmptyQuery)
|
||||||
|
|
||||||
export type Query = typeof(EmptyQuery)
|
export type Query = typeof(EmptyQuery)
|
||||||
|
|
||||||
function World.query(world: World, ...: i53): Query
|
type CompatibleArchetype = { archetype: Archetype, indices: { number } }
|
||||||
|
|
||||||
|
function World.query(world: World, ...): Query
|
||||||
-- breaking?
|
-- breaking?
|
||||||
if (...) == nil then
|
if (...) == nil then
|
||||||
error("Missing components")
|
error("Missing components")
|
||||||
end
|
end
|
||||||
|
|
||||||
local compatibleArchetypes = {}
|
local compatibleArchetypes: { CompatibleArchetype } = {}
|
||||||
local length = 0
|
local length = 0
|
||||||
|
|
||||||
local components = {...}
|
local components = { ... }
|
||||||
local archetypes = world.archetypes
|
local archetypes = world.archetypes
|
||||||
local queryLength = #components
|
local queryLength = #components
|
||||||
|
|
||||||
local firstArchetypeMap
|
local firstArchetypeMap: ArchetypeMap
|
||||||
local componentIndex = world.componentIndex
|
local componentIndex = world.componentIndex
|
||||||
|
|
||||||
for _, componentId in components do
|
for _, componentId in components do
|
||||||
|
@ -599,9 +743,10 @@ function World.query(world: World, ...: i53): Query
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for id in firstArchetypeMap.sparse do
|
for id in firstArchetypeMap.cache do
|
||||||
local archetype = archetypes[id]
|
local archetype = archetypes[id]
|
||||||
local archetypeRecords = archetype.records
|
local archetypeRecords = archetype.records
|
||||||
|
|
||||||
local indices = {}
|
local indices = {}
|
||||||
local skip = false
|
local skip = false
|
||||||
|
|
||||||
|
@ -611,6 +756,7 @@ function World.query(world: World, ...: i53): Query
|
||||||
skip = true
|
skip = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
-- index should be index.offset
|
||||||
indices[i] = index
|
indices[i] = index
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -619,64 +765,46 @@ function World.query(world: World, ...: i53): Query
|
||||||
end
|
end
|
||||||
|
|
||||||
length += 1
|
length += 1
|
||||||
compatibleArchetypes[length] = {archetype, indices}
|
compatibleArchetypes[length] = {
|
||||||
|
archetype = archetype,
|
||||||
|
indices = indices,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
local lastArchetype = 1
|
||||||
if not lastArchetype then
|
local compatibleArchetype: CompatibleArchetype = compatibleArchetypes[lastArchetype]
|
||||||
|
|
||||||
|
if not compatibleArchetype then
|
||||||
return EmptyQuery
|
return EmptyQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
local preparedQuery = {}
|
local preparedQuery = {}
|
||||||
preparedQuery.__index = preparedQuery
|
preparedQuery.__index = preparedQuery
|
||||||
|
|
||||||
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 withoutComponents do
|
|
||||||
if records[componentId] then
|
|
||||||
shouldRemove = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if shouldRemove then
|
|
||||||
table.remove(compatibleArchetypes, i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
|
||||||
if not lastArchetype then
|
|
||||||
return EmptyQuery
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
local lastRow
|
|
||||||
local queryOutput = {}
|
local queryOutput = {}
|
||||||
|
|
||||||
function preparedQuery:__iter()
|
local i = 1
|
||||||
return function()
|
|
||||||
local archetype = compatibleArchetype[1]
|
local function queryNext()
|
||||||
local row: number = next(archetype.entities, lastRow) :: number
|
local archetype = compatibleArchetype.archetype
|
||||||
while row == nil do
|
local entityId = archetype.entities[i]
|
||||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
|
||||||
if lastArchetype == nil then
|
while entityId == nil do
|
||||||
|
lastArchetype += 1
|
||||||
|
if lastArchetype > #compatibleArchetypes then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
archetype = compatibleArchetype[1]
|
compatibleArchetype = compatibleArchetypes[lastArchetype]
|
||||||
row = next(archetype.entities, row) :: number
|
archetype = compatibleArchetype.archetype
|
||||||
|
i = 1
|
||||||
|
entityId = archetype.entities[i]
|
||||||
end
|
end
|
||||||
lastRow = row
|
|
||||||
|
|
||||||
local entityId = archetype.entities[row :: number]
|
local row = i
|
||||||
|
i+=1
|
||||||
|
|
||||||
local columns = archetype.columns
|
local columns = archetype.columns
|
||||||
local tr = compatibleArchetype[2]
|
local tr = compatibleArchetype.indices
|
||||||
|
|
||||||
if queryLength == 1 then
|
if queryLength == 1 then
|
||||||
return entityId, columns[tr[1]][row]
|
return entityId, columns[tr[1]][row]
|
||||||
|
@ -726,23 +854,58 @@ function World.query(world: World, ...: i53): Query
|
||||||
queryOutput[i] = columns[tr[i]][row]
|
queryOutput[i] = columns[tr[i]][row]
|
||||||
end
|
end
|
||||||
|
|
||||||
return entityId, unpack(queryOutput, 1, queryLength)
|
return entityId, unpack(queryOutput :: any, 1, queryLength)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function preparedQuery:__iter()
|
||||||
|
return queryNext
|
||||||
|
end
|
||||||
|
|
||||||
|
function preparedQuery:next()
|
||||||
|
return queryNext()
|
||||||
|
end
|
||||||
|
|
||||||
|
function preparedQuery:without(...)
|
||||||
|
local withoutComponents = { ... }
|
||||||
|
for i = #compatibleArchetypes, 1, -1 do
|
||||||
|
local archetype = compatibleArchetypes[i].archetype
|
||||||
|
local records = archetype.records
|
||||||
|
local shouldRemove = false
|
||||||
|
|
||||||
|
for _, componentId in withoutComponents do
|
||||||
|
if records[componentId] then
|
||||||
|
shouldRemove = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if shouldRemove then
|
||||||
|
table.remove(compatibleArchetypes, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lastArchetype, compatibleArchetype = next(compatibleArchetypes :: any)
|
||||||
|
if not lastArchetype then
|
||||||
|
return EmptyQuery
|
||||||
|
end
|
||||||
|
|
||||||
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
return setmetatable({}, preparedQuery) :: any
|
return setmetatable({}, preparedQuery) :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.__iter(world: World): () -> (number?, unknown?)
|
function World.__iter(world: World): () -> any
|
||||||
local dense = world.entityIndex.dense
|
local dense = world.entityIndex.dense
|
||||||
local sparse = world.entityIndex.sparse
|
local sparse = world.entityIndex.sparse
|
||||||
local last
|
local last
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
local lastEntity, entityId = next(dense, last)
|
local lastEntity: number?, entityId: number = next(dense, last)
|
||||||
if not lastEntity then
|
if not lastEntity then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
last = lastEntity
|
last = lastEntity
|
||||||
|
|
||||||
local record = sparse[entityId]
|
local record = sparse[entityId]
|
||||||
|
@ -766,24 +929,153 @@ function World.__iter(world: World): () -> (number?, unknown?)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return table.freeze({
|
-- __nominal_type_dont_use could not be any or T as it causes a type error
|
||||||
World = World;
|
-- or produces a union
|
||||||
|
export type Entity<T = any> = number & { __nominal_type_dont_use: T }
|
||||||
|
export type Pair = number
|
||||||
|
|
||||||
OnAdd = ON_ADD;
|
export type QueryShim<T...> = typeof(setmetatable({
|
||||||
OnRemove = ON_REMOVE;
|
without = function(...): QueryShim<T...>
|
||||||
OnSet = ON_SET;
|
return nil :: any
|
||||||
Wildcard = WILDCARD,
|
end,
|
||||||
w = WILDCARD,
|
}, {
|
||||||
|
__iter = function(): () -> (number, T...)
|
||||||
|
return nil :: any
|
||||||
|
end,
|
||||||
|
}))
|
||||||
|
export type WorldShim = typeof(setmetatable(
|
||||||
|
{} :: {
|
||||||
|
|
||||||
|
--- Creates a new entity
|
||||||
|
entity: (WorldShim) -> Entity,
|
||||||
|
--- Creates a new entity located in the first 256 ids.
|
||||||
|
--- These should be used for static components for fast access.
|
||||||
|
component: <T>(WorldShim) -> Entity<T>,
|
||||||
|
--- Gets the target of an relationship. For example, when a user calls
|
||||||
|
--- `world:target(id, ChildOf(parent))`, you will obtain the parent entity.
|
||||||
|
target: (WorldShim, id: Entity, relation: Entity) -> Entity?,
|
||||||
|
--- Deletes an entity and all it's related components and relationships.
|
||||||
|
delete: (WorldShim, id: Entity) -> (),
|
||||||
|
|
||||||
|
--- Adds a component to the entity with no value
|
||||||
|
add: <T>(WorldShim, id: Entity, component: Entity<T>) -> (),
|
||||||
|
--- Assigns a value to a component on the given entity
|
||||||
|
set: <T>(WorldShim, id: Entity, component: Entity<T>, data: T) -> (),
|
||||||
|
--- Removes a component from the given entity
|
||||||
|
remove: (WorldShim, id: Entity, component: Entity) -> (),
|
||||||
|
--- Retrieves the value of up to 4 components. These values may be nil.
|
||||||
|
get: (<A>(WorldShim, id: any, Entity<A>) -> A)
|
||||||
|
& (<A, B>(WorldShim, id: Entity, Entity<A>, Entity<B>) -> (A, B))
|
||||||
|
& (<A, B, C>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>) -> (A, B, C))
|
||||||
|
& <A, B, C, D>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> (A, B, C, D),
|
||||||
|
|
||||||
|
--- Searches the world for entities that match a given query
|
||||||
|
query: (<A>(WorldShim, Entity<A>) -> QueryShim<A>)
|
||||||
|
& (<A, B>(WorldShim, Entity<A>, Entity<B>) -> QueryShim<A, B>)
|
||||||
|
& (<A, B, C>(WorldShim, Entity<A>, Entity<B>, Entity<C>) -> QueryShim<A, B, C>)
|
||||||
|
& (<A, B, C, D>(WorldShim, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> QueryShim<A, B, C, D>)
|
||||||
|
& (<A, B, C, D, E>(
|
||||||
|
WorldShim,
|
||||||
|
Entity<A>,
|
||||||
|
Entity<B>,
|
||||||
|
Entity<C>,
|
||||||
|
Entity<D>,
|
||||||
|
Entity<E>
|
||||||
|
) -> QueryShim<A, B, C, D, E>)
|
||||||
|
& (<A, B, C, D, E, F>(
|
||||||
|
WorldShim,
|
||||||
|
Entity<A>,
|
||||||
|
Entity<B>,
|
||||||
|
Entity<C>,
|
||||||
|
Entity<D>,
|
||||||
|
Entity<E>,
|
||||||
|
Entity<F>
|
||||||
|
) -> QueryShim<A, B, C, D, E, F>)
|
||||||
|
& (<A, B, C, D, E, F, G>(
|
||||||
|
WorldShim,
|
||||||
|
Entity<A>,
|
||||||
|
Entity<B>,
|
||||||
|
Entity<C>,
|
||||||
|
Entity<D>,
|
||||||
|
Entity<E>,
|
||||||
|
Entity<F>,
|
||||||
|
Entity<G>
|
||||||
|
) -> QueryShim<A, B, C, D, E, F, G>)
|
||||||
|
& (<A, B, C, D, E, F, G, H>(
|
||||||
|
WorldShim,
|
||||||
|
Entity<A>,
|
||||||
|
Entity<B>,
|
||||||
|
Entity<C>,
|
||||||
|
Entity<D>,
|
||||||
|
Entity<E>,
|
||||||
|
Entity<F>,
|
||||||
|
Entity<G>,
|
||||||
|
Entity<H>
|
||||||
|
) -> QueryShim<A, B, C, D, E, F, G, H>)
|
||||||
|
& (<A, B, C, D, E, F, G, H, I>(
|
||||||
|
WorldShim,
|
||||||
|
Entity<A>,
|
||||||
|
Entity<B>,
|
||||||
|
Entity<C>,
|
||||||
|
Entity<D>,
|
||||||
|
Entity<E>,
|
||||||
|
Entity<F>,
|
||||||
|
Entity<G>,
|
||||||
|
Entity<H>,
|
||||||
|
Entity<I>
|
||||||
|
) -> QueryShim<A, B, C, D, E, F, G, H, I>)
|
||||||
|
& (<A, B, C, D, E, F, G, H, I, J>(
|
||||||
|
WorldShim,
|
||||||
|
Entity<A>,
|
||||||
|
Entity<B>,
|
||||||
|
Entity<C>,
|
||||||
|
Entity<D>,
|
||||||
|
Entity<E>,
|
||||||
|
Entity<F>,
|
||||||
|
Entity<G>,
|
||||||
|
Entity<H>,
|
||||||
|
Entity<I>,
|
||||||
|
Entity<J>
|
||||||
|
) -> QueryShim<A, B, C, D, E, F, G, H, I, J>)
|
||||||
|
& (<A, B, C, D, E, F, G, H, I, J, K>(
|
||||||
|
WorldShim,
|
||||||
|
Entity<A>,
|
||||||
|
Entity<B>,
|
||||||
|
Entity<C>,
|
||||||
|
Entity<D>,
|
||||||
|
Entity<E>,
|
||||||
|
Entity<F>,
|
||||||
|
Entity<G>,
|
||||||
|
Entity<H>,
|
||||||
|
Entity<I>,
|
||||||
|
Entity<J>,
|
||||||
|
Entity<K>,
|
||||||
|
...Entity<any>
|
||||||
|
) -> QueryShim<A, B, C, D, E, F, G, H, I, J, K>),
|
||||||
|
},
|
||||||
|
{} :: {
|
||||||
|
__iter: (world: WorldShim) -> () -> (number, { [unknown]: unknown? }),
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
return table.freeze({
|
||||||
|
World = (World :: any) :: { new: () -> WorldShim },
|
||||||
|
|
||||||
|
OnAdd = (ON_ADD :: any) :: Entity,
|
||||||
|
OnRemove = (ON_REMOVE :: any) :: Entity,
|
||||||
|
OnSet = (ON_SET :: any) :: Entity,
|
||||||
|
Wildcard = (WILDCARD :: any) :: Entity,
|
||||||
|
w = (WILDCARD :: any) :: Entity,
|
||||||
Rest = REST,
|
Rest = REST,
|
||||||
|
|
||||||
ECS_ID = ECS_ID,
|
|
||||||
IS_PAIR = ECS_IS_PAIR,
|
IS_PAIR = ECS_IS_PAIR,
|
||||||
|
ECS_ID = ECS_ENTITY_T_LO,
|
||||||
ECS_PAIR = ECS_PAIR,
|
ECS_PAIR = ECS_PAIR,
|
||||||
ECS_GENERATION_INC = ECS_GENERATION_INC,
|
ECS_GENERATION_INC = ECS_GENERATION_INC,
|
||||||
ECS_GENERATION = ECS_GENERATION,
|
ECS_GENERATION = ECS_GENERATION,
|
||||||
ecs_get_target = ecs_get_target,
|
ECS_PAIR_RELATION = ECS_PAIR_RELATION,
|
||||||
ecs_get_source = ecs_get_source,
|
ECS_PAIR_OBJECT = ECS_PAIR_OBJECT,
|
||||||
|
|
||||||
pair = ECS_PAIR,
|
pair = (ECS_PAIR :: any) :: <R, T>(pred: Entity, obj: Entity) -> number,
|
||||||
getAlive = getAlive,
|
getAlive = getAlive,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,382 +0,0 @@
|
||||||
local jecs = require(script.Parent)
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
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 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()
|
|
||||||
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, a in world:query(Tag, AnotherTag) 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("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()
|
|
||||||
|
|
||||||
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
|
|
||||||
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("should allow iterating the whole world", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
local A, B = world:entity(), world:entity()
|
|
||||||
|
|
||||||
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
|
|
||||||
expect(data[A]).to.be.ok()
|
|
||||||
expect(data[B]).to.never.be.ok()
|
|
||||||
elseif id == eB then
|
|
||||||
expect(data[B]).to.be.ok()
|
|
||||||
expect(data[A]).to.never.be.ok()
|
|
||||||
elseif id == eAB then
|
|
||||||
expect(data[A]).to.be.ok()
|
|
||||||
expect(data[B]).to.be.ok()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(count).to.equal(5)
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should allow querying for relations", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local Eats = world:entity()
|
|
||||||
local Apples = world:entity()
|
|
||||||
local bob = world:entity()
|
|
||||||
|
|
||||||
world:set(bob, jecs.pair(Eats, Apples), true)
|
|
||||||
for e, bool in world:query(jecs.pair(Eats, Apples)) do
|
|
||||||
expect(e).to.equal(bob)
|
|
||||||
expect(bool).to.equal(bool)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should allow wildcards in queries", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local Eats = world:entity()
|
|
||||||
local Apples = world:entity()
|
|
||||||
local bob = world:entity()
|
|
||||||
|
|
||||||
world:set(bob, jecs.pair(Eats, Apples), "bob eats apples")
|
|
||||||
for e, data in world:query(jecs.pair(Eats, jecs.w)) do
|
|
||||||
expect(e).to.equal(bob)
|
|
||||||
expect(data).to.equal("bob eats apples")
|
|
||||||
end
|
|
||||||
for e, data in world:query(jecs.pair(jecs.w, Apples)) do
|
|
||||||
expect(e).to.equal(bob)
|
|
||||||
expect(data).to.equal("bob eats apples")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
it("should match against multiple pairs", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local pair = jecs.pair
|
|
||||||
local Eats = world:entity()
|
|
||||||
local Apples = world:entity()
|
|
||||||
local Oranges =world:entity()
|
|
||||||
local bob = world:entity()
|
|
||||||
local alice = world:entity()
|
|
||||||
|
|
||||||
world:set(bob, pair(Eats, Apples), "bob eats apples")
|
|
||||||
world:set(alice, pair(Eats, Oranges), "alice eats oranges")
|
|
||||||
|
|
||||||
local w = jecs.Wildcard
|
|
||||||
|
|
||||||
local count = 0
|
|
||||||
for e, data in world:query(pair(Eats, w)) do
|
|
||||||
count += 1
|
|
||||||
if e == bob then
|
|
||||||
expect(data).to.equal("bob eats apples")
|
|
||||||
else
|
|
||||||
expect(data).to.equal("alice eats oranges")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(count).to.equal(2)
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
for e, data in world:query(pair(w, Apples)) do
|
|
||||||
count += 1
|
|
||||||
expect(data).to.equal("bob eats apples")
|
|
||||||
end
|
|
||||||
expect(count).to.equal(1)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
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
|
2199
package-lock.json
generated
Normal file
2199
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
43
package.json
Normal file
43
package.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "@rbxts/jecs",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Stupidly fast Entity Component System",
|
||||||
|
"main": "lib/init.lua",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ukendio/jecs.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "rbxtsc",
|
||||||
|
"watch": "rbxtsc -w",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Ukendio",
|
||||||
|
"contributors": [
|
||||||
|
"Ukendio",
|
||||||
|
"EncodedVenom"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/ukendio/jecs",
|
||||||
|
"license": "MIT",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"lib/"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rbxts/compiler-types": "^2.3.0-types.1",
|
||||||
|
"@rbxts/types": "^1.0.781",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
||||||
|
"@typescript-eslint/parser": "^5.8.0",
|
||||||
|
"eslint": "^8.5.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-roblox-ts": "^0.0.32",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"roblox-ts": "^2.3.0",
|
||||||
|
"typescript": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,18 +22,6 @@
|
||||||
},
|
},
|
||||||
"mirror": {
|
"mirror": {
|
||||||
"$path": "mirror"
|
"$path": "mirror"
|
||||||
},
|
|
||||||
"DevPackages": {
|
|
||||||
"$path": "DevPackages"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TestService": {
|
|
||||||
"$properties": {
|
|
||||||
"ExecuteWithStudioRun": true
|
|
||||||
},
|
|
||||||
"$className": "TestService",
|
|
||||||
"run": {
|
|
||||||
"$path": "tests.server.lua"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
|
|
||||||
require(ReplicatedStorage.DevPackages.TestEZ).TestBootstrap:run({
|
|
||||||
ReplicatedStorage.Lib,
|
|
||||||
nil,
|
|
||||||
{
|
|
||||||
noXpcallByDefault = true,
|
|
||||||
},
|
|
||||||
})
|
|
140
tests/world.lua
140
tests/world.lua
|
@ -1,19 +1,30 @@
|
||||||
local testkit = require("../testkit")
|
|
||||||
local jecs = require("../lib/init")
|
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_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
|
||||||
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
|
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
|
||||||
local IS_PAIR = jecs.IS_PAIR
|
local IS_PAIR = jecs.IS_PAIR
|
||||||
local ECS_PAIR = jecs.ECS_PAIR
|
local ECS_PAIR = jecs.ECS_PAIR
|
||||||
local getAlive = jecs.getAlive
|
local getAlive = jecs.getAlive
|
||||||
local ecs_get_source = jecs.ecs_get_source
|
local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION
|
||||||
local ecs_get_target = jecs.ecs_get_target
|
local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT
|
||||||
|
|
||||||
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
|
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
|
local N = 10
|
||||||
|
|
||||||
TEST("world", function()
|
TEST("world", function()
|
||||||
do CASE "should be iterable"
|
do
|
||||||
|
CASE("should be iterable")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
|
@ -45,7 +56,8 @@ TEST("world", function()
|
||||||
CHECK(count == 3 + 2)
|
CHECK(count == 3 + 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should query all matching entities"
|
do
|
||||||
|
CASE("should query all matching entities")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
|
@ -55,7 +67,9 @@ TEST("world", function()
|
||||||
local id = world:entity()
|
local id = world:entity()
|
||||||
|
|
||||||
world:set(id, A, true)
|
world:set(id, A, true)
|
||||||
if i > 5 then world:set(id, B, true) end
|
if i > 5 then
|
||||||
|
world:set(id, B, true)
|
||||||
|
end
|
||||||
entities[i] = id
|
entities[i] = id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -64,10 +78,10 @@ TEST("world", function()
|
||||||
end
|
end
|
||||||
|
|
||||||
CHECK(#entities == 0)
|
CHECK(#entities == 0)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should query all matching entities when irrelevant component is removed"
|
do
|
||||||
|
CASE("should query all matching entities when irrelevant component is removed")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
|
@ -81,7 +95,9 @@ TEST("world", function()
|
||||||
-- https://github.com/Ukendio/jecs/pull/15
|
-- https://github.com/Ukendio/jecs/pull/15
|
||||||
world:set(id, B, true)
|
world:set(id, B, true)
|
||||||
world:set(id, A, true)
|
world:set(id, A, true)
|
||||||
if i > 5 then world:remove(id, B) end
|
if i > 5 then
|
||||||
|
world:remove(id, B)
|
||||||
|
end
|
||||||
entities[i] = id
|
entities[i] = id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -94,7 +110,8 @@ TEST("world", function()
|
||||||
CHECK(added == N)
|
CHECK(added == N)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should query all entities without B"
|
do
|
||||||
|
CASE("should query all entities without B")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
|
@ -109,7 +126,6 @@ TEST("world", function()
|
||||||
else
|
else
|
||||||
world:set(id, B, true)
|
world:set(id, B, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
for id in world:query(A):without(B) do
|
for id in world:query(A):without(B) do
|
||||||
|
@ -117,10 +133,10 @@ TEST("world", function()
|
||||||
end
|
end
|
||||||
|
|
||||||
CHECK(#entities == 0)
|
CHECK(#entities == 0)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should allow setting components in arbitrary order"
|
do
|
||||||
|
CASE("should allow setting components in arbitrary order")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local Health = world:entity()
|
local Health = world:entity()
|
||||||
|
@ -133,7 +149,8 @@ TEST("world", function()
|
||||||
CHECK(world:get(id, Poison) == 5)
|
CHECK(world:get(id, Poison) == 5)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should allow deleting components"
|
do
|
||||||
|
CASE("should allow deleting components")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local Health = world:entity()
|
local Health = world:entity()
|
||||||
|
@ -152,10 +169,10 @@ TEST("world", function()
|
||||||
CHECK(world:get(id, Health) == nil)
|
CHECK(world:get(id, Health) == nil)
|
||||||
CHECK(world:get(id1, Poison) == 500)
|
CHECK(world:get(id1, Poison) == 500)
|
||||||
CHECK(world:get(id1, Health) == 50)
|
CHECK(world:get(id1, Health) == 50)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should allow remove that doesn't exist on entity"
|
do
|
||||||
|
CASE("should allow remove that doesn't exist on entity")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local Health = world:entity()
|
local Health = world:entity()
|
||||||
|
@ -169,7 +186,8 @@ TEST("world", function()
|
||||||
CHECK(world:get(id, Health) == 50)
|
CHECK(world:get(id, Health) == 50)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should increment generation"
|
do
|
||||||
|
CASE("should increment generation")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local e = world:entity()
|
local e = world:entity()
|
||||||
CHECK(ECS_ID(e) == 1 + jecs.Rest)
|
CHECK(ECS_ID(e) == 1 + jecs.Rest)
|
||||||
|
@ -179,7 +197,8 @@ TEST("world", function()
|
||||||
CHECK(ECS_GENERATION(e) == 1) -- 1
|
CHECK(ECS_GENERATION(e) == 1) -- 1
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should get alive from index in the dense array"
|
do
|
||||||
|
CASE("should get alive from index in the dense array")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local _e = world:entity()
|
local _e = world:entity()
|
||||||
local e2 = world:entity()
|
local e2 = world:entity()
|
||||||
|
@ -189,11 +208,13 @@ TEST("world", function()
|
||||||
|
|
||||||
local pair = ECS_PAIR(e2, e3)
|
local pair = ECS_PAIR(e2, e3)
|
||||||
CHECK(IS_PAIR(pair) == true)
|
CHECK(IS_PAIR(pair) == true)
|
||||||
CHECK(ecs_get_source(world.entityIndex, pair) == e2)
|
|
||||||
CHECK(ecs_get_target(world.entityIndex, pair) == e3)
|
CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2)
|
||||||
|
CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should allow querying for relations"
|
do
|
||||||
|
CASE("should allow querying for relations")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local Eats = world:entity()
|
local Eats = world:entity()
|
||||||
local Apples = world:entity()
|
local Apples = world:entity()
|
||||||
|
@ -206,7 +227,8 @@ TEST("world", function()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should allow wildcards in queries"
|
do
|
||||||
|
CASE("should allow wildcards in queries")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local Eats = world:entity()
|
local Eats = world:entity()
|
||||||
local Apples = world:entity()
|
local Apples = world:entity()
|
||||||
|
@ -225,11 +247,12 @@ TEST("world", function()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "should match against multiple pairs"
|
do
|
||||||
|
CASE("should match against multiple pairs")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local Eats = world:entity()
|
local Eats = world:entity()
|
||||||
local Apples = world:entity()
|
local Apples = world:entity()
|
||||||
local Oranges =world:entity()
|
local Oranges = world:entity()
|
||||||
local bob = world:entity()
|
local bob = world:entity()
|
||||||
local alice = world:entity()
|
local alice = world:entity()
|
||||||
|
|
||||||
|
@ -256,6 +279,75 @@ TEST("world", function()
|
||||||
end
|
end
|
||||||
CHECK(count == 1)
|
CHECK(count == 1)
|
||||||
end
|
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)
|
end)
|
||||||
|
|
||||||
FINISH()
|
FINISH()
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// required
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "Roact.createElement",
|
||||||
|
"jsxFragmentFactory": "Roact.Fragment",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"noLib": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
"typeRoots": ["node_modules/@rbxts"],
|
||||||
|
|
||||||
|
// configurable
|
||||||
|
"rootDir": "lib",
|
||||||
|
"outDir": "out",
|
||||||
|
"baseUrl": "lib",
|
||||||
|
"incremental": true,
|
||||||
|
"tsBuildInfoFile": "out/tsconfig.tsbuildinfo",
|
||||||
|
|
||||||
|
"moduleDetection": "force"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ukendio/jecs"
|
name = "ukendio/jecs"
|
||||||
version = "0.1.0-rc.6"
|
version = "0.1.0"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
|
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
|
||||||
exclude = ["**"]
|
exclude = ["**"]
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
TestEZ = "roblox/testez@0.4.1"
|
|
Loading…
Reference in a new issue