This commit is contained in:
kalrnlo 2024-06-23 19:10:00 -04:00
commit ca8e1376ca
26 changed files with 3855 additions and 1252 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

4
.gitignore vendored
View file

@ -46,6 +46,10 @@ Packages
wally.lock
WallyPatches
# Typescript
/node_modules
/include
# Misc
roblox.toml
sourcemap.json

21
LICENSE Normal file
View 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.

View file

@ -12,7 +12,7 @@ Just an ECS
jecs is a stupidly fast Entity Component System (ECS).
- 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
- Zero-dependency package
- Optimized for column-major operations
@ -22,47 +22,40 @@ jecs is a stupidly fast Entity Component System (ECS).
### Example
```lua
local world = World.new()
local world = jecs.World.new()
local pair = jecs.pair
local player = world:entity()
local opponent = world:entity()
local ChildOf = world:component()
local Name = world:component()
local Health = world:component()
local Position = world:component()
-- Notice how components can just be entities as well?
-- It allows you to model relationships easily!
local Damage = world:entity()
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
local function parent(entity)
return world:target(entity, ChildOf)
end
local function getName(entity)
return world:get(entity, Name)
end
-- Gets the damage inflicted by our specific opponent!
for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, opponent)) do
world:set(playerId, health - inflicted)
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
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.

View file

@ -1,5 +1,5 @@
[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"

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,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

View file

@ -2,7 +2,6 @@
--!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)

View file

@ -3,11 +3,11 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local rgb = require(ReplicatedStorage.rgb)
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter)
local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr)
local newWorld = Matter.World.new()
local jecs = require(ReplicatedStorage.Lib)
local jecs = require(ReplicatedStorage.Shim)
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local ecs = jecs.World.new()
@ -177,6 +177,13 @@ return {
end
end,
Matter = function()
local matched = 0
for entityId, firstComponent in newWorld:query(A1, A4, A6, A8) do
matched += 1
end
end,
ECR = function()
local matched = 0
for entityId, firstComponent in registry2:view(B1, B4, B6, B8) do

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 76 KiB

153
lib/index.d.ts vendored Normal file
View 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;

File diff suppressed because it is too large Load diff

View file

@ -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
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

2199
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

43
package.json Normal file
View 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"
}
}

View file

@ -22,18 +22,6 @@
},
"mirror": {
"$path": "mirror"
},
"DevPackages": {
"$path": "DevPackages"
}
},
"TestService": {
"$properties": {
"ExecuteWithStudioRun": true
},
"$className": "TestService",
"run": {
"$path": "tests.server.lua"
}
}
}

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,261 +1,353 @@
local testkit = require("../testkit")
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_get_source = jecs.ecs_get_source
local ecs_get_target = jecs.ecs_get_target
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)
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
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
-- 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()
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()
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
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
for id in world:query(A) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
CHECK(#entities == 0)
end
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()
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()
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
-- 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
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
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()
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()
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
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
for id in world:query(A):without(B) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
CHECK(#entities == 0)
do
CASE("should allow setting components in arbitrary order")
local world = jecs.World.new()
end
local Health = world:entity()
local Poison = world:component()
do CASE "should allow setting components in arbitrary order"
local world = jecs.World.new()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
local Health = world:entity()
local Poison = world:component()
CHECK(world:get(id, Poison) == 5)
end
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
do
CASE("should allow deleting components")
local world = jecs.World.new()
CHECK(world:get(id, Poison) == 5)
end
local Health = world:entity()
local Poison = world:component()
do CASE "should allow deleting components"
local world = jecs.World.new()
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)
local Health = world:entity()
local Poison = world:component()
world:delete(id)
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)
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
world:delete(id)
do
CASE("should allow remove that doesn't exist on entity")
local world = jecs.World.new()
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == nil)
CHECK(world:get(id1, Poison) == 500)
CHECK(world:get(id1, Health) == 50)
local Health = world:entity()
local Poison = world:component()
end
local id = world:entity()
world:set(id, Health, 50)
world:remove(id, Poison)
do CASE "should allow remove that doesn't exist on entity"
local world = jecs.World.new()
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
local Health = world:entity()
local Poison = world:component()
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
local id = world:entity()
world:set(id, Health, 50)
world:remove(id, Poison)
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(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
CHECK(IS_PAIR(world:entity()) == false)
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
local pair = ECS_PAIR(e2, e3)
CHECK(IS_PAIR(pair) == true)
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(ECS_PAIR_RELATION(world.entityIndex, pair) == e2)
CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3)
end
CHECK(IS_PAIR(world:entity()) == false)
do
CASE("should allow querying for relations")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
local pair = ECS_PAIR(e2, e3)
CHECK(IS_PAIR(pair) == true)
CHECK(ecs_get_source(world.entityIndex, pair) == e2)
CHECK(ecs_get_target(world.entityIndex, pair) == e3)
end
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 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 allow wildcards in queries")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
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
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
CHECK(count == 2)
count = 0
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
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 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()
FINISH()

26
tsconfig.json Normal file
View 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"
}
}

View file

@ -1,10 +1,7 @@
[package]
name = "ukendio/jecs"
version = "0.1.0-rc.6"
version = "0.1.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
exclude = ["**"]
[dev-dependencies]
TestEZ = "roblox/testez@0.4.1"
exclude = ["**"]