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

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

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

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

View file

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

View file

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

31
bench.project.json Normal file
View file

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

View file

@ -1,35 +1,33 @@
--!optimize 2
--!native
local testkit = require('../testkit')
local testkit = require("../testkit")
local BENCH, START = testkit.benchmark()
local function TITLE(title: string)
print()
print(testkit.color.white(title))
end
local jecs = require("../mirror/init")
local jecs = require("../lib/init")
local mirror = require("../mirror/init")
local oldMatter = require("../oldMatter")
local newMatter = require("../newMatter")
type i53 = number
do TITLE (testkit.color.white_underline("Jecs query"))
do
TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.World.new()
do TITLE "one component in common"
local function view_bench(
world: jecs.World,
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
)
do
TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
BENCH("1 component", function()
for _ in world:query(A) do end
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
for _ in world:query(A, B) do
end
end)
BENCH("4 component", function()
@ -38,7 +36,31 @@ do TITLE (testkit.color.white_underline("Jecs query"))
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
for _ in world:query(A, B, C, D, E, F, G, H) do
end
end)
local e = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
BENCH("Update Data", function()
for _ = 1, 100 do
world:set(e, A, false)
world:set(e, B, false)
world:set(e, C, false)
world:set(e, D, false)
world:set(e, E, false)
world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
@ -57,7 +79,7 @@ do TITLE (testkit.color.white_underline("Jecs query"))
local added = 0
local archetypes = {}
for i = 1, 2^16-2 do
for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity()
local combination = ""
@ -68,15 +90,15 @@ do TITLE (testkit.color.white_underline("Jecs query"))
end
if flip() then
combination ..= "C"
ecs:set(entity, D3, { value = true })
ecs:set(entity, D3, {value = true})
end
if flip() then
combination ..= "D"
ecs:set(entity, D4, { value = true})
ecs:set(entity, D4, {value = true})
end
if flip() then
combination ..= "E"
ecs:set(entity, D5, { value = true})
ecs:set(entity, D5, {value = true})
end
if flip() then
combination ..= "F"
@ -84,45 +106,44 @@ do TITLE (testkit.color.white_underline("Jecs query"))
end
if flip() then
combination ..= "G"
ecs:set(entity, D7, { value = true})
ecs:set(entity, D7, {value = true})
end
if flip() then
combination ..= "H"
ecs:set(entity, D8, {value = true})
end
if #combination == 7 then
added += 1
ecs:set(entity, D1, { value = true})
ecs:set(entity, D1, {value = true})
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do a+= 1 end
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end
do TITLE(testkit.color.white_underline("OldMatter query"))
local ecs = oldMatter.World.new()
local component = oldMatter.component
do TITLE "one component in common"
local function view_bench(
world: jecs.World,
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
)
do
TITLE(testkit.color.white_underline("Mirror query"))
local ecs = mirror.World.new()
do
TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
BENCH("1 component", function()
for _ in world:query(A) do end
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
for _ in world:query(A, B) do
end
end)
BENCH("4 component", function()
@ -131,18 +152,42 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
for _ in world:query(A, B, C, D, E, F, G, H) do
end
end)
local e = world:entity()
world:set(e, A, true)
world:set(e, B, true)
world:set(e, C, true)
world:set(e, D, true)
world:set(e, E, true)
world:set(e, F, true)
world:set(e, G, true)
world:set(e, H, true)
BENCH("Update Data", function()
for _ = 1, 100 do
world:set(e, A, false)
world:set(e, B, false)
world:set(e, C, false)
world:set(e, D, false)
world:set(e, E, false)
world:set(e, F, false)
world:set(e, G, false)
world:set(e, H, false)
end
end)
end
local D1 = component()
local D2 = component()
local D3 = component()
local D4 = component()
local D5 = component()
local D6 = component()
local D7 = component()
local D8 = component()
local D1 = ecs:component()
local D2 = ecs:component()
local D3 = ecs:component()
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local function flip()
return math.random() >= 0.15
@ -150,150 +195,52 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
local added = 0
local archetypes = {}
for i = 1, 2^16-2 do
local entity = ecs:spawn()
for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity()
local combination = ""
if flip() then
combination ..= "B"
ecs:insert(entity, D2({value = true}))
ecs:set(entity, D2, {value = true})
end
if flip() then
combination ..= "C"
ecs:insert(entity, D3({value = true}))
ecs:set(entity, D3, {value = true})
end
if flip() then
combination ..= "D"
ecs:insert(entity, D4({value = true}))
ecs:set(entity, D4, {value = true})
end
if flip() then
combination ..= "E"
ecs:insert(entity, D5({value = true}))
ecs:set(entity, D5, {value = true})
end
if flip() then
combination ..= "F"
ecs:insert(entity, D6({value = true}))
ecs:set(entity, D6, {value = true})
end
if flip() then
combination ..= "G"
ecs:insert(entity, D7({value = true}))
ecs:set(entity, D7, {value = true})
end
if flip() then
combination ..= "H"
ecs:insert(entity, D8({value = true}))
ecs:set(entity, D8, {value = true})
end
if #combination == 7 then
added += 1
ecs:insert(entity, D1({value = true}))
ecs:set(entity, D1, {value = true})
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do a+= 1 end
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end
do TITLE(testkit.color.white_underline("NewMatter query"))
local ecs = newMatter.World.new()
local component = newMatter.component
do TITLE "one component in common"
local function view_bench(
world: jecs.World,
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
)
BENCH("1 component", function()
for _ in world:query(A) do end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
end)
BENCH("4 component", function()
for _ in world:query(A, B, C, D) do
end
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
end)
end
local D1 = component()
local D2 = component()
local D3 = component()
local D4 = component()
local D5 = component()
local D6 = component()
local D7 = component()
local D8 = component()
local function flip()
return math.random() >= 0.15
end
local added = 0
local archetypes = {}
for i = 1, 2^16-2 do
local entity = ecs:spawn()
local combination = ""
if flip() then
combination ..= "B"
ecs:insert(entity, D2({value = true}))
end
if flip() then
combination ..= "C"
ecs:insert(entity, D3({value = true}))
end
if flip() then
combination ..= "D"
ecs:insert(entity, D4({value = true}))
end
if flip() then
combination ..= "E"
ecs:insert(entity, D5({value = true}))
end
if flip() then
combination ..= "F"
ecs:insert(entity, D6({value = true}))
end
if flip() then
combination ..= "G"
ecs:insert(entity, D7({value = true}))
end
if flip() then
combination ..= "H"
ecs:insert(entity, D8({value = true}))
end
if #combination == 7 then
added += 1
ecs:insert(entity, D1({value = true}))
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do a+= 1 end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end

View file

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

View file

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

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

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

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

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

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

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

6
jecs_darkmode.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 1.2 KiB

6
jecs_lightmode.svg Normal file
View file

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

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load diff

View file

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

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

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

186
mkdocs.yml Normal file
View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

4
selene.toml Normal file
View file

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

5
stylua.toml Normal file
View file

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

View file

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

3
testez-companion.toml Normal file
View file

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

View file

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

View file

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

353
tests/world.lua Normal file
View file

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

View file

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