├── .luaurc
├── todo.md
├── default.project.json
├── .gitignore
├── docs
├── .vitepress
│ ├── theme
│ │ ├── index.js
│ │ └── vars.css
│ └── config.ts
├── package.json
├── index.md
├── tut
│ ├── partition.md
│ ├── tags.md
│ ├── context.md
│ ├── entity-type.md
│ ├── storage.md
│ ├── groups.md
│ └── crash-course.md
├── public
│ ├── logo.svg
│ └── favicon.svg
├── api
│ ├── Group.md
│ ├── Handle.md
│ ├── Pool.md
│ ├── Signal.md
│ ├── Queue.md
│ ├── View.md
│ ├── restrictions.md
│ ├── Observer.md
│ ├── ecr.md
│ └── Registry.md
└── wiki
│ ├── entity-identifier.md
│ └── component-storage.md
├── wally.toml
├── .github
└── workflows
│ ├── unit-test.yml
│ └── deploy.yml
├── LICENSE.md
├── README.md
├── CHANGELOG.md
└── test
├── testkit.luau
├── benchmarks.luau
└── tests.luau
/.luaurc:
--------------------------------------------------------------------------------
1 | { "languageMode": "strict" }
2 |
3 |
--------------------------------------------------------------------------------
/todo.md:
--------------------------------------------------------------------------------
1 | # To do
2 |
3 | - Non-owning groups
4 |
--------------------------------------------------------------------------------
/default.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecr",
3 | "tree": { "$path": "src/ecr.luau" }
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | sourcemap.json
2 | remarks.luau
3 | .vscode
4 | _local
5 | aftman.toml
6 |
7 | docs/.vitepress/dist
8 | docs/.vitepress/cache
9 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.js:
--------------------------------------------------------------------------------
1 | // .vitepress/theme/index.js
2 | import DefaultTheme from 'vitepress/theme'
3 | import './vars.css'
4 | export default DefaultTheme
5 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/vars.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --vp-c-brand: #ff3030;
3 |
4 | --vp-c-brand-dark: #ff3030;
5 | --vp-c-brand-darker: #350606;
6 | --vp-c-brand-darkest: #fb4848;
7 | --vp-c-brand-light: #fb4848;
8 | }
9 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 |
4 | "scripts": {
5 | "docs:dev": "vitepress dev",
6 | "docs:build": "vitepress build",
7 | "docs:preview": "vitepress preview"
8 | },
9 |
10 | "devDependencies": {
11 | "vitepress": "1.0.0-rc.4"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/wally.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "centau/ecr"
3 | description = "An ECS library for Luau."
4 | version = "0.9.0"
5 | registry = "https://github.com/UpliftGames/wally-index"
6 | realm = "shared"
7 | include = ["src", "LICENSE.md", "default.project.json"]
8 | exclude = ["CHANGELOG.md", ".gitignore", ".github", ".gitattributes", "test", "docs", "README.md", ".luaurc"]
9 |
10 | [dependencies]
11 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: ECR
7 | text: ""
8 | tagline: A Luau ECS library.
9 | image:
10 | src: /logo.svg
11 | alt: ECR
12 | actions:
13 | - theme: brand
14 | text: Tutorials
15 | link: /tut/crash-course
16 | - theme: alt
17 | text: API Reference
18 | link: /api/ecr
19 |
20 | features:
21 | - title: In Development
22 | details: Not recommended for production use.
23 | ---
24 |
25 |
--------------------------------------------------------------------------------
/.github/workflows/unit-test.yml:
--------------------------------------------------------------------------------
1 | name: unit-test
2 | on:
3 | push:
4 | pull_request:
5 |
6 | jobs:
7 | unit-test:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout repo
11 | uses: actions/checkout@v3
12 |
13 | - name: Install Luau zip
14 | uses: robinraju/release-downloader@v1.6
15 | with:
16 | repository: Roblox/luau
17 | tag: "0.655"
18 | fileName: luau-ubuntu.zip
19 | out-file-path: bin
20 |
21 | - name: Unzip Luau
22 | run: unzip bin/luau-ubuntu.zip -d bin
23 |
24 | - name: Run unit tests
25 | run: bin/luau test/tests.luau
26 |
--------------------------------------------------------------------------------
/docs/tut/partition.md:
--------------------------------------------------------------------------------
1 | # Partitions
2 |
3 | Currently, a registry by default can create entities occupying a range
4 | between [1, 65534].
5 | You can optionally restrict this range to avoid id conflicts when copying
6 | entities from one registry to another.
7 |
8 | The most common example of this is replicating server entities into a client
9 | registry, while also being able to create client entities. The partitions can be
10 | set up like so:
11 |
12 | ```lua
13 | local server_registry = ecr.registry(1, 1000)
14 | local client_registry = ecr.registry(1001, 2000)
15 | ```
16 |
17 | This ensures that ids returned by `registry:create()` will never overlap. You
18 | can still create entities outside of the range with an explicit
19 | `registry:create(id)`.
20 |
--------------------------------------------------------------------------------
/docs/public/logo.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/docs/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/docs/tut/tags.md:
--------------------------------------------------------------------------------
1 | # Tags
2 |
3 | Tags are special component types that store no value.
4 |
5 | A tag can be created with:
6 |
7 | ```lua
8 | local Tag = ecr.tag()
9 | ```
10 |
11 | Tags types are used in the same way as any other component type. They are useful
12 | for marking entities in some state, and are more efficient than something like
13 | `ecr.component() :: true`.
14 |
15 | Example usage:
16 |
17 | ```lua
18 | local Selected = ecr.tag()
19 |
20 | registry:add(id, Selected)
21 | registry:has(id, Selected) -- true
22 |
23 | registry:remove(id, Selected)
24 | registry:has(id, Selected) -- false
25 | ```
26 |
27 | To check if an entity has a tag, favor `Registry:has()` over `Registry:get()`,
28 | since tags have no value and will return `nil`.
29 |
30 | You can check if a component type is a tag type or not with `ecr.is_tag(ctype)`.
31 |
--------------------------------------------------------------------------------
/docs/api/Group.md:
--------------------------------------------------------------------------------
1 | # Group
2 |
3 | Fast iterator for viewing entities and components in a registry.
4 |
5 | ```lua
6 | type ecr.Group
7 | ```
8 |
9 | ## Iteration
10 |
11 | Iterates over all entities in the group.
12 |
13 | - **Type**
14 |
15 | ```lua
16 | for id: Entity, ...: T... in Group do
17 | ```
18 |
19 | - **Details**
20 |
21 | The entity followed by the component values are returned.
22 |
23 | Components can be added, changed and removed during iteration.
24 | Components added during iteration are not returned for that iteration.
25 |
26 | ::: warning
27 | During iteration, adding or removing components from entities not currently
28 | being iterated can invalidate the iterator.
29 | :::
30 |
31 | ## Length
32 |
33 | Returns the amount of entities in the group.
34 |
35 | - **Type**
36 |
37 | ```lua
38 | #Group: number
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/tut/context.md:
--------------------------------------------------------------------------------
1 | # Context
2 |
3 | All registries have a special entity, using a special id `ecr.context`, called
4 | the *context entity*.
5 |
6 | If you ever need to store general data about the world (like a round timer,
7 | chosen map, etc) the context entity is a place to do this. Being an entity,
8 | systems can still act on it saving you from having separate logic for normal
9 | entity data and context data (like automatic replication).
10 |
11 | This entity does not exist by default, it is automatically created the first
12 | time [`Registry:context()`](../api/Registry#context) is called, subsequent calls
13 | return the same entity.
14 |
15 | ```lua
16 | local Round = ecr.component() :: number
17 |
18 | registry:context():set(Round, 1)
19 |
20 | -- or, if you prefer
21 |
22 | registry:create(ecr.context)
23 | registry:set(ecr.context, Round, 1)
24 | ```
25 |
26 | This entity is still like any other, can show up in views, can still be
27 | destroyed (and later recreated), and is affected by
28 | [`Registry:clear()`](../api/Registry#clear).
29 |
--------------------------------------------------------------------------------
/docs/api/Handle.md:
--------------------------------------------------------------------------------
1 | # Handle
2 |
3 | Thin wrapper around an entity and its registry.
4 |
5 | ```lua
6 | type ecr.Handle
7 | ```
8 |
9 | ## Properties
10 |
11 | ### registry
12 |
13 | The registry the entity belongs to.
14 |
15 | - **Type**
16 |
17 | ```lua
18 | Handle.registry: Registry
19 | ```
20 |
21 | --------------------------------------------------------------------------------
22 |
23 | ### entity
24 |
25 | The entity the handle refers to.
26 |
27 | - **Type**
28 |
29 | ```lua
30 | Handle.entity: entity
31 | ```
32 |
33 | ## Methods
34 |
35 | The `Handle` class wraps the following registry methods:
36 |
37 | - destroy()
38 | - has_none()
39 | - add()
40 | - set()
41 | - insert()
42 | - patch()
43 | - has()
44 | - get()
45 | - try_get()
46 | - remove()
47 |
48 | The `set()` and `insert()` method will also return the handle it is called
49 | on.
50 |
51 | - **Example**
52 |
53 | ```lua
54 | local e = registry:handle()
55 |
56 | e:set(A, 1)
57 | :set(B, 2)
58 |
59 | print(e:get(A, B)) --> 1, 2
60 | ```
61 |
--------------------------------------------------------------------------------
/docs/tut/entity-type.md:
--------------------------------------------------------------------------------
1 | # Entity Type
2 |
3 | Entities are represented by a special component type, `ecr.entity`.
4 |
5 | This can be used in the same way as other components types, except instead of
6 | representing components, represents entities.
7 |
8 | This type cannot be used to modify the registry, methods like `add()`, `set()`,
9 | `remove()` do not work with this type.
10 |
11 | ## Get all entities
12 |
13 | You can get all entities that currently exist in the registry with:
14 |
15 | ```lua
16 | for id in registry:view(ecr.entity) do
17 | print(id)
18 | end
19 |
20 | -- or
21 |
22 | local entities = registry:storage(ecr.entity).entities
23 | ```
24 |
25 | ## Exclude-only views
26 |
27 | You can get all entities without specific components with:
28 |
29 | ```lua
30 | for entity in registry:view(ecr.entity):exclude(...) do end
31 | ```
32 |
33 | ## Listeners
34 |
35 | You can listen to when an entity is created with:
36 |
37 | ```lua
38 | registry:on_add(ecr.entity):connect(function)
39 | ```
40 |
41 | And when an entity is destroyed with:
42 |
43 | ```lua
44 | registry:on_remove(ecr.entity):connect(function)
45 | ```
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 centau
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/api/Pool.md:
--------------------------------------------------------------------------------
1 | # Pool
2 |
3 | The container used by the registry internally to store entities and component
4 | values for each component type.
5 |
6 | ## Properties
7 |
8 | ### size
9 |
10 | The amount of entities contained in the pool.
11 |
12 | - **Type**
13 |
14 | ```lua
15 | Pool.size: number
16 | ```
17 |
18 | --------------------------------------------------------------------------------
19 |
20 | ### entities
21 |
22 | A buffer of all entities with the given component type.
23 |
24 | - **Type**
25 |
26 | ```lua
27 | Pool.entities: buffer
28 | ```
29 |
30 | - **Details**
31 |
32 | 0-indexed.
33 |
34 | Sorted in the same order as [`Pool.values`](Pool#values).
35 |
36 | - i.e. `entities[n]`'s component value is located at `values[n + 1]`.
37 |
38 | --------------------------------------------------------------------------------
39 |
40 | ### values
41 |
42 | An array of all values for the given component type.
43 |
44 | - **Type**
45 |
46 | ```lua
47 | Pool.values: Array
48 | ```
49 |
50 | - **Details**
51 |
52 | 1-indexed.
53 |
54 | Sorted in the same order as [`Pool.entities`](Pool#entities.md).
55 |
--------------------------------------------------------------------------------
/docs/api/Signal.md:
--------------------------------------------------------------------------------
1 | # Signal
2 |
3 | Manage listeners to an event.
4 |
5 | ```lua
6 | type ecr.Signal
7 | ```
8 |
9 | ## Methods
10 |
11 | ### connect()
12 |
13 | Connects a given function to the signal to be called whenever the signal is
14 | fired.
15 |
16 | - **Type**
17 |
18 | ```lua
19 | function Signal:connect((T...) -> ()): Connection
20 | ```
21 |
22 | - **Details**
23 |
24 | New connections made within a listener will not be ran until the next time
25 | the signal is fired.
26 |
27 | ## Connection
28 |
29 | ```lua
30 | type ecr.Connection
31 | ```
32 |
33 | ### disconnect()
34 |
35 | Disconnects a listener from a signal.
36 |
37 | - **Type**
38 |
39 | ```lua
40 | function Connection:disconnect()
41 | ```
42 |
43 | - **Details**
44 |
45 | ::: warning
46 | Disconnecting other listeners from within a listener is not allowed.
47 | Disconnecting a listenener from within itself is allowed.
48 | :::
49 |
50 | --------------------------------------------------------------------------------
51 |
52 | ### Reconnect()
53 |
54 | Reconnects a listener to a signal.
55 |
56 | - **Type**
57 |
58 | ```lua
59 | function Connection:reconnect()
60 | ```
61 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: site-deploy
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | permissions:
7 | contents: read
8 | pages: write
9 | id-token: write
10 |
11 | concurrency:
12 | group: pages
13 | cancel-in-progress: false
14 |
15 | defaults:
16 | run:
17 | working-directory: docs
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout
24 | uses: actions/checkout@v3
25 | with:
26 | fetch-depth: 0
27 | - name: Setup Node
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: 18
31 | - name: Setup Pages
32 | uses: actions/configure-pages@v3
33 | - name: Install dependencies
34 | run: npm install
35 | - name: Build with VitePress
36 | run: npm run docs:build
37 | - name: Upload artifact
38 | uses: actions/upload-pages-artifact@v2
39 | with:
40 | path: docs/.vitepress/dist
41 |
42 | deploy:
43 | environment:
44 | name: github-pages
45 | url: ${{ steps.deployment.outputs.page_url }}
46 | needs: build
47 | runs-on: ubuntu-latest
48 | name: Deploy
49 | steps:
50 | - name: Deploy to GitHub Pages
51 | id: deployment
52 | uses: actions/deploy-pages@v2
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | #
10 |
11 |
12 |
13 | ### ⚠️ This library is in early stages of development with breaking changes being made often.
14 |
15 | ECR is a Luau ECS library.
16 |
17 | - A library and not a framework
18 | - Uses typechecking.
19 | - Sparse-set based storage that can support perfect SoA.
20 | - Carefully optimized memory usage and performance.
21 | - Signals for detecting changes to components.
22 |
23 | ## Getting started
24 |
25 | Read the [crash course](https://centau.github.io/ecr/tut/crash-course) for a
26 | brief introduction to the library.
27 |
28 | ## Code sample
29 |
30 | ```lua
31 | local ecr = require(ecr)
32 |
33 | -- define components
34 | local Position = ecr.component() :: Vector3
35 | local Velocity = ecr.component() :: Vector3
36 |
37 | -- define a system
38 | local function update_physics(world: ecr.Registry, dt: number)
39 | for id, pos, vel in world:view(Position, Velocity) do
40 | world:set(id, Position, pos + vel*dt)
41 | end
42 | end
43 |
44 | -- instantiate the world
45 | local world = ecr.registry()
46 |
47 | -- create entities and assign components
48 | for i = 1, 10 do
49 | local id = world:create()
50 | world:set(id, Position, Vector3.new(i, 1, 1))
51 | world:set(id, Velocity, Vector3.new(10, 0, 0))
52 | end
53 |
54 | -- run system
55 | update_physics(world, 1/60)
56 | ```
57 |
--------------------------------------------------------------------------------
/docs/api/Queue.md:
--------------------------------------------------------------------------------
1 | # Queue
2 |
3 | Queues values to be processed later.
4 |
5 | ```lua
6 | type ecr.Queue
7 | ```
8 |
9 | ## Methods
10 |
11 | ### add()
12 |
13 | Adds a set of values to a queue.
14 |
15 | - **Type**
16 |
17 | ```lua
18 | function Queue:add(...: T...)
19 | ```
20 |
21 | - **Details**
22 |
23 | Each time this method is called, the size of the queue increases by one.
24 |
25 | All arguments given are later returned together in the same iteration.
26 |
27 | Queues are FIFO.
28 |
29 | ::: warning
30 | The first value in the argument list cannot be `nil` since that will cause
31 | iteration to stop early.
32 | :::
33 |
34 | --------------------------------------------------------------------------------
35 |
36 | ### clear()
37 |
38 | Clears the queue.
39 |
40 | - **Type**
41 |
42 | ```lua
43 | function Queue:clear()
44 | ```
45 |
46 | ## Iteration
47 |
48 | Iterates all values added to the queue.
49 |
50 | - **Type**
51 |
52 | ```lua
53 | for ...: T... in Queue do
54 | ```
55 |
56 | - **Details**
57 |
58 | The queue returns in sets of values passed to `Queue:add()` in the same
59 | order it was called in.
60 |
61 | The queue automatically clears itself after iteration. `Queue:iter()`
62 | returns an iterator that will not automatically clear on completion.
63 |
64 | ::: warning
65 | Adding values during iteration will cause them to be cleared when iteration
66 | completes and they will never be iterated.
67 | :::
68 |
--------------------------------------------------------------------------------
/docs/api/View.md:
--------------------------------------------------------------------------------
1 | # View
2 |
3 | Iterator for viewing entities and components in a registry.
4 |
5 | ```lua
6 | type ecr.View
7 | ```
8 |
9 | ## Methods
10 |
11 | ### exclude()
12 |
13 | Excludes entities with the given components from the view.
14 |
15 | - **Type**
16 |
17 | ```lua
18 | function View:exclude(components: ...unknown): View
19 | ```
20 |
21 | - **Details**
22 |
23 | Entities with *any* of the excluded components, will not be returned during
24 | iteration.
25 |
26 | --------------------------------------------------------------------------------
27 |
28 | ## Iteration
29 |
30 | Iterates all entities in the view.
31 |
32 | - **Type**
33 |
34 | ```lua
35 | for id: Entity, ...: T... in View do
36 | ```
37 |
38 | - **Details**
39 |
40 | The entity followed by the component values are returned.
41 |
42 | Components can be added, changed and removed during iteration.
43 | Components added during iteration are not returned for that iteration.
44 |
45 | ::: warning
46 | During iteration, adding or removing components from entities not currently
47 | being iterated can invalidate the iterator.
48 | :::
49 |
50 | ## Length
51 |
52 | Returns the amount of entities in the view.
53 |
54 | - **Type**
55 |
56 | ```lua
57 | #View: number
58 | ```
59 |
60 | - **Details**
61 |
62 | For single component views, this returns the exact amount of entities in the
63 | view.
64 |
65 | For multiple component views, this returns an estimated amount of entities.
66 | This estimate will not be less than the actual amount of entities.
67 |
--------------------------------------------------------------------------------
/docs/tut/storage.md:
--------------------------------------------------------------------------------
1 | # Storage
2 |
3 | Each component type in the registry has its own [pool](../api/Pool.md). A pool
4 | stores every entity that has that component type, and their corresponding value
5 | for that component type. Pools are the underlying containers that the registry
6 | directly modifies.
7 |
8 | ECR was designed with transparent access to data in mind, and so, you can access
9 | these pools directly if needed.
10 |
11 | ```lua
12 | type Pool = {
13 | -- amount of entities in the pool (so amount that have the component type)
14 | size: number,
15 |
16 | -- buffer (used as an array) of all entities in the pool
17 | entities: buffer,
18 |
19 | -- array of all component values (value for entities[i] is values[i])
20 | values: Array
21 | }
22 | ```
23 |
24 | A pool for a type is retrieved like so:
25 |
26 | ```lua
27 | local pool = registry:storage(ctype)
28 | ```
29 |
30 | You can read from pools, but you cannot write to pools, with the exception
31 | of writing to values of `Pool.values`. The registry maintains the size and
32 | values of `Pool.entities`, so those should not be changed.
33 |
34 | Buffers aren't nice to work with, so you can use
35 | [`ecr.buffer_to_array()`](../api/ecr#buffer_to_array) to access entities easier.
36 |
37 | ```lua
38 | local entities = ecr.buffer_to_array(pool.entities, pool.size)
39 | ```
40 |
41 | This can be useful for e.g:
42 |
43 | - Getting all entities and components to replicate a registry from server to
44 | client for the first time they join.
45 |
46 | - Modifying values directly for performance systems.
47 |
48 | If needed you can also get all pools inside the registry via an iterator.
49 |
50 | ```lua
51 | for ctype, pool in registry:storage() do
52 | ```
53 |
--------------------------------------------------------------------------------
/docs/api/restrictions.md:
--------------------------------------------------------------------------------
1 | # Restrictions
2 |
3 | The API omits sanity checks in areas that would be costly to do so, allowing you
4 | to run into problems like iterator invalidation and undefined behavior. This
5 | means that there are certain restrictions on what you can do. All restrictions
6 | are documented here and at the relevant API references.
7 |
8 | ## Signals
9 |
10 | - Listeners cannot disconnect other listeners. Listeners can disconnect
11 | themselves.
12 |
13 | - The registry cannot be modified within a listener, you cannot add or remove
14 | components, and create or destroy entities.
15 |
16 | ## Modifying During Iteration
17 |
18 | This applies to the iteration of views, observers and groups.
19 |
20 | - During iteration, adding or removing components from entities not currently
21 | being iterated can *invalidate the iterator*.
22 |
23 | ## Pools
24 |
25 | [`storage()`](Registry#storage.md) returns the underlying storages used
26 | by the registry to store entities and components. You can use these for more
27 | direct access than views and also modify the values of `Pool.values` directly.
28 |
29 | - Changing `Pool.entities` or `Pool.size` is *undefined behavior*.
30 |
31 | ## Releasing Ids
32 |
33 | - Destroying an entity that still has components with
34 | [`release()`](Registry#release.md) is *undefined behavior*.
35 |
36 | ## Multithreading
37 |
38 | - Components can be added or removed during iteration as long as it is the only
39 | thread doing so for that set of components.
40 |
41 | This does not apply if a component in that set belongs to a group that has
42 | other group components being used in another thread, e.g One thread can
43 | add/remove component `X` and another thread can add/remove `Y` and `Z` as long
44 | as `X` is not grouped with `Y` or `Z`.
45 |
46 | - The same set of components can be iterated by multiple threads at the same
47 | time if they are reading only.
48 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitepress"
2 |
3 | // https://vitepress.dev/reference/site-config
4 | export default defineConfig({
5 | title: "ECR",
6 | titleTemplate: ":title - A Luau ECS library",
7 | description: "A Luau ECS library.",
8 | base: "/ecr/",
9 | head: [["link", { rel: "icon", href: "/ecr/favicon.svg" }]],
10 | ignoreDeadLinks: true,
11 |
12 | markdown: {
13 | theme: "poimandres"
14 | },
15 |
16 | themeConfig: {
17 | logo: "/logo.svg",
18 | siteTitle: false,
19 |
20 | // https://vitepress.dev/reference/default-theme-config
21 | nav: [
22 | { text: "Home", link: "/" },
23 | { text: "Tutorials", link: "/tut/crash-course" },
24 | { text: "API", link: "/api/ecr"},
25 | { text: "GitHub", link: "https://github.com/centau/ecr" }
26 | ],
27 |
28 | outline: "deep",
29 |
30 | sidebar: {
31 | "/api/": [
32 | {
33 | text: "API",
34 | items: [
35 | { text: "ecr", link: "/api/ecr" },
36 | { text: "Registry", link: "/api/Registry" },
37 | { text: "View", link: "/api/View" },
38 | { text: "Observer", link: "/api/Observer" },
39 | { text: "Group", link: "/api/Group" },
40 | { text: "Handle", link: "/api/Handle" },
41 | { text: "Signal", link: "/api/Signal" },
42 | { text: "Queue", link: "/api/Queue" },
43 | { text: "Pool", link: "/api/Pool" },
44 | { text: "Restrictions", link: "/api/restrictions" },
45 | ]
46 | }
47 | ],
48 |
49 | "/tut/": [
50 | {
51 | text: "Tutorials",
52 | items: [
53 | { text: "Crash Course", link: "/tut/crash-course" },
54 | { text: "Tags", link: "/tut/tags" },
55 | { text: "Entity Type", link: "/tut/entity-type" },
56 | { text: "Context", link: "/tut/context" },
57 | { text: "Pools", link: "/tut/storage" },
58 | { text: "Groups", link: "/tut/groups" },
59 | { text: "Partitions", link: "/tut/partition" }
60 | ]
61 | }
62 | ],
63 | }
64 | }
65 | })
66 |
--------------------------------------------------------------------------------
/docs/api/Observer.md:
--------------------------------------------------------------------------------
1 | # Observer
2 |
3 | Tracks component changes, and can be cleared at will.
4 |
5 | ```lua
6 | type ecr.Observer
7 | ```
8 |
9 | ## Methods
10 |
11 | ### exclude()
12 |
13 | Excludes entities with the given components from the observer.
14 |
15 | - **Type**
16 |
17 | ```lua
18 | function Observer:exclude(components: ...unknown): Observer
19 | ```
20 |
21 | - **Details**
22 |
23 | Entities with *any* of the excluded components, will not be returned during
24 | iteration.
25 |
26 | --------------------------------------------------------------------------------
27 |
28 | ### disconnect()
29 |
30 | Disconnects the observer, stopping any new changes from being tracked
31 |
32 | - **Type**
33 |
34 | ```lua
35 | function Observer:disconnect(): Observer
36 | ```
37 |
38 | The observer must be empty before it is disconnected.
39 |
40 | ::: warning
41 | This must be called for the observer to be garbage collected.
42 | :::
43 |
44 | --------------------------------------------------------------------------------
45 |
46 | ### reconnect()
47 |
48 | Reconnects the Observer and allows it to track changes again.
49 |
50 | - **Type**
51 |
52 | ```lua
53 | function Observer:reconnect(): Observer
54 | ```
55 |
56 | --------------------------------------------------------------------------------
57 |
58 | ### clear()
59 |
60 | Clears all stored entities.
61 |
62 | - **Type**
63 |
64 | ```lua
65 | function Observer:clear(): Observer
66 | ```
67 |
68 | ## Iteration
69 |
70 | Iterates all entities in the observer.
71 |
72 | - **Type**
73 |
74 | ```lua
75 | for id: Entity, ...: T... in Observer do
76 | ```
77 |
78 | - **Details**
79 |
80 | The observer will return entities that had any of the given components added
81 | or changed since it was last cleared and still have all given components at
82 | the time of iteration.
83 |
84 | The entity followed by the latest component values are returned.
85 |
86 | Components can be added, changed and removed during iteration.
87 | Components added during iteration are not returned for that iteration.
88 |
89 | Will automatically clear the observer on completion. `Observer:iter()`
90 | returns an iterator that will not automatically clear on completion.
91 |
92 | ::: warning
93 | Adding values during iteration can cause them to be cleared when iteration
94 | ends and they will never be iterated.
95 | :::
96 |
97 | ::: warning
98 | During iteration, adding or removing components from entities not currently
99 | being iterated can invalidate the iterator.
100 | :::
101 |
102 | ## Length
103 |
104 | Returns the amount of entities in the observer.
105 |
106 | - **Type**
107 |
108 | ```lua
109 | #Observer: number
110 | ```
111 |
--------------------------------------------------------------------------------
/docs/tut/groups.md:
--------------------------------------------------------------------------------
1 | # Groups
2 |
3 | Groups are a technique used to optimize iteration over a set of component types.
4 | Groups achieve perfect [SoA](https://en.wikipedia.org/wiki/AoS_and_SoA) for the
5 | best iteration performance at the cost of more expensive addition and removal of
6 | group-owned components.
7 |
8 | ## Creation
9 |
10 | Groups can be created as followed:
11 |
12 | ```lua
13 | registry:group(A, B, C)
14 | ```
15 |
16 | This creates a new group with components `A`, `B` and `C`.
17 | These 3 components are now said to be *owned* by the group.
18 |
19 | Each component type can only be owned by a single group. The following code
20 | would result in an error:
21 |
22 | ```lua
23 | registry:group(A, B)
24 | registry:group(B, D)
25 | ```
26 |
27 | This errors because 2 different groups cannot claim ownership of `B`.
28 |
29 | Groups do not have to be stored aside when they are created, the first time a
30 | group is created it is stored permanently inside the registry, future
31 | `registry:group()` calls will just return the same group for the same set of
32 | components.
33 |
34 | Once a group is created, it will ensure that its owned components are aligned
35 | with each other in memory whenever a component is added or removed from an
36 | entity. This means the only performance cost grouping imposes is the addition
37 | and removal of group-owned components on entities. Changing the values
38 | of already added group-owned components is unaffected.
39 |
40 | ## Usage
41 |
42 | A group is iterated in the same way as a view.
43 |
44 | ```lua
45 | for id, position, velocity in registry:group(Position, Velocity) do
46 | registry:set(id, Position, position + velocity*dt)
47 | end
48 | ```
49 |
50 | Components can be added, changed and removed during iteration.
51 |
52 | The exact size of a group can also be read:
53 |
54 | ```lua
55 | local size = #registry:group(A, B)
56 | ```
57 |
58 | ## Perfect SoA
59 |
60 | Since groups guarantee alignment of its owned components, they can be modified
61 | together directly.
62 |
63 | ```lua
64 | local Position = ecr.component(Vector3.new)
65 | local Velocity = ecr.component(Vector3.new)
66 |
67 | local function update_positions(dt: number)
68 | local n = #registry:group(Position, Velocity)
69 | local positions = registry:storage(Position).values
70 | local velocities = registry:storage(Velocity).values
71 |
72 | for i = 1, n do
73 | positions[i] += velocities[i] * dt
74 | end
75 | ```
76 |
77 | It is important not to exceed the size of the group `n` or you will act on
78 | entities outside of the group, where the `i`th component values may no longer
79 | correspond with each other.
80 |
81 | ## Limitations
82 |
83 | - As mentioned before, each component type can only be owned by one group,
84 | groups cannot share components so you need to profile to determine where the
85 | most benefit is gained.
86 |
87 | - In a rare case, during the iteration of a view including a group-owned
88 | component, an entity joining the group because a group-owned component was
89 | added will invalidate the view iterator.
90 |
91 | This is due to how groups organise their components in memory. This can be
92 | avoided if you:
93 | 1. defer adding components to after iteration instead of during iteration.
94 | 2. know that adding those group-owned components will not cause the entity
95 | to enter the group.
96 |
97 | Views can detect and will error if the above rules are broken so you do not need
98 | to worry about it until it happens.
99 |
100 | ## When to group
101 |
102 | While views are fast, in certain situations like where a view contains many
103 | components, iteration of a view may be the bottleneck of a system.
104 | In such cases, by grouping components together, iteration becomes as fast as
105 | possible, removing the bottleneck.
106 |
107 | You should only use grouping when you have profiled and identified that the
108 | iteration of a view is a system bottleneck.
109 |
--------------------------------------------------------------------------------
/docs/api/ecr.md:
--------------------------------------------------------------------------------
1 | # ECR
2 |
3 | ## Functions
4 |
5 | ### registry()
6 |
7 | Creates a new registry.
8 |
9 | - **Type**
10 |
11 | ```lua
12 | function ecr.registry(): Registry
13 | function ecr.registry(start: number, end: number): Registry
14 | ```
15 |
16 | - **Details**
17 |
18 | If specified, the entity id space can be restricted. This is useful for
19 | ensuring ids are not in conflict when copying entities from one registry to
20 | another.
21 |
22 | --------------------------------------------------------------------------------
23 |
24 | ### component()
25 |
26 | Creates a new component type.
27 |
28 | - **Type**
29 |
30 | ```lua
31 | function ecr.component(): unknown
32 | function ecr.component(constructor: () -> T): T
33 | ```
34 |
35 | - **Details**
36 |
37 | Returns a unique id representing a new component type.
38 |
39 | Can be given a constructor which can be invoked when
40 | [`registry:add()`](Registry#add.md) or
41 | [`registry:patch()`](Registry#patch.md) is used.
42 |
43 | - **Example**
44 |
45 | No constructor.
46 |
47 | ```lua
48 | local Position = ecr.component() :: Vector3
49 | local Health = ecr.component() :: number
50 | ```
51 |
52 | With constructor.
53 |
54 | ```lua
55 | local Position = ecr.component(Vector3.new)
56 |
57 | local Health = ecr.component(function()
58 | return {
59 | Current = 100,
60 | Max = 100
61 | }
62 | end)
63 | ```
64 |
65 | --------------------------------------------------------------------------------
66 |
67 | ### tag()
68 |
69 | Creates a new tag component type.
70 |
71 | - **Type**
72 |
73 | ```lua
74 | function ecr.tag(): nil
75 | ```
76 |
77 | - **Details**
78 |
79 | Returns a unique id representing a new component type.
80 |
81 | Tag components are a special type of component that have no value.
82 |
83 | --------------------------------------------------------------------------------
84 |
85 | ### is_tag()
86 |
87 | Checks if a component type is a tag.
88 |
89 | - **Type**
90 |
91 | ```lua
92 | function ecr.is_tag(ctype: T): boolean
93 | ```
94 |
95 | --------------------------------------------------------------------------------
96 |
97 | ### queue()
98 |
99 | Creates a new queue.
100 |
101 | - **Type**
102 |
103 | ```lua
104 | function ecr.queue(): Queue<...unknown>
105 | function ecr.queue(signal: ISignal) -> Queue
106 |
107 | type ISignal =
108 | { connect: (ISignal, (T...) -> ()) -> () } |
109 | { Connect: (ISignal, (T...) -> ()) -> () } |
110 | ((T...) -> ()) -> ()
111 | ```
112 |
113 | - **Details**
114 |
115 | Can accept any signal object that matches the interface to
116 | automatically connect a callback where any arguments it is called with are
117 | added to the queue.
118 |
119 | --------------------------------------------------------------------------------
120 |
121 | ### name()
122 |
123 | Associates names with components for debugging.
124 |
125 | - **Type**
126 |
127 | ```lua
128 | function ecr.name(names: T & Map) -> T
129 | ```
130 |
131 | - **Details**
132 |
133 | Allows for errors raised to display the component name instead of its
134 | argument position.
135 |
136 | --------------------------------------------------------------------------------
137 |
138 | ### buffer_to_array()
139 |
140 | Converts a buffer of entities into an array of entities.
141 |
142 | - **Type**
143 |
144 | ```lua
145 | function ecr.buffer_to_array(buf: buffer, size: number, arr: Array?) -> Array
146 | ```
147 |
148 | - **Details**
149 |
150 | Copies the first `size` ids from the buffer to a target array.
151 |
152 | If no target array is given, one will be created.
153 |
154 | --------------------------------------------------------------------------------
155 |
156 | ### array_to_buffer()
157 |
158 | Converts an array of entities into a buffer of entities.
159 |
160 | - **Type**
161 |
162 | ```lua
163 | function ecr.buffer_to_array(arr: Array, size: number, buf: buffer?) -> buffer
164 | ```
165 |
166 | - **Details**
167 |
168 | Copies the first `size` ids from an array to a target buffer.
169 |
170 | If no target buffer is given, one will be created.
171 |
172 | --------------------------------------------------------------------------------
173 |
174 | ### buffer_to_buffer()
175 |
176 | Copies a buffer of entities into a buffer of entities.
177 |
178 | - **Type**
179 |
180 | ```lua
181 | function ecr.buffer_to_buffer(source: buffer, size: number, target: buffer?) -> buffer
182 | ```
183 |
184 | - **Details**
185 |
186 | Copies the first `size` ids from a buffer to a target buffer.
187 |
188 | If no target buffer is given, one will be created.
189 |
190 | ## Constants
191 |
192 | ### entity
193 |
194 | Special component type that represents entities in a registry.
195 |
196 | - **Type**
197 |
198 | ```lua
199 | ecr.entity: entity
200 | ```
201 |
202 | --------------------------------------------------------------------------------
203 |
204 | ### context
205 |
206 | The context entity id.
207 |
208 | - **Type**
209 |
210 | ```lua
211 | ecr.context: entity
212 | ```
213 |
214 | --------------------------------------------------------------------------------
215 |
216 | ### null
217 |
218 | The null entity id.
219 |
220 | - **Type**
221 |
222 | ```lua
223 | ecr.null: entity
224 | ```
225 |
226 | - **Details**
227 |
228 | Attempting to use this entity with a registry will error.
229 |
230 | The following expression will always return `false`:
231 |
232 | ```lua
233 | registry:contains(ecr.null)
234 | ```
235 |
236 | --------------------------------------------------------------------------------
237 |
238 | ### id_size
239 |
240 | The size of the entity id in bytes.
241 |
242 | - **Type**
243 |
244 | ```lua
245 | ecr.id_size: number
246 | ```
247 |
--------------------------------------------------------------------------------
/docs/wiki/entity-identifier.md:
--------------------------------------------------------------------------------
1 | # Entity Identifiers
2 |
3 | In ECS, an "entity" is just an abstract thing to mentally associate components
4 | with. In implementation, an entity identifier is just a key used to access
5 | internal storage of components.
6 |
7 | They can be thought of as pointers but with extra safety checks.
8 |
9 | Ids must be able to uniquely identify entities, that is no two entities can
10 | exist with the same id at the same time.
11 |
12 | ## Simple Approach
13 |
14 | A simple implementation would be to keep track of a counter; each time a new
15 | entity is created, this counter is incremented and its value returned as a new
16 | id. There would be no concern for the counter overflowing either. Numbers in
17 | Luau are represented using 64-bit floats which can represent `2^64`, i.e.
18 | `1.8446744e+19` unique values, making it very difficult (impossible?) to
19 | overflow.
20 |
21 | This approach has drawbacks, however. Using these ids as an array index will
22 | result in steady-growth of memory usage as you create more and more ids as the
23 | arrays must grow to fit the largest id. You can solve this by using a hashmap
24 | instead which does not need to grow with the largest id, but this sacrifices
25 | speed to save memory usage, as hashing is much slower than array indexing.
26 |
27 | > todo: add benchmark
28 |
29 | ## ECR's Approach
30 |
31 | ECR splits ids into 2 parts using bit manipulation.
32 | The lower part is called the *key* and the upper part is called the *version*.
33 |
34 | The key part is used as an array index and can be reused when other entities are
35 | destroyed and their keys become free.
36 |
37 | The version part is used to keep the *overall* id unique. If we only used the
38 | key part as the entire id, we run into issues with storing ids of dead entities,
39 | and having new entities reusing the exact same id, resulting in being unable to
40 | differentiate between a dead entity and an alive entity. So each time a new
41 | entity reuses a key, the version part of the id is incremented so that the
42 | overall id remains unique.
43 |
44 | Example using 8-bit ids with 4 bits for the key and 4 bit for the version:
45 |
46 | ```lua
47 | 0001 0001 = 17 -- id with key = 1, version = 1
48 | 0010 0001 = 33 -- id with key = 1, version = 2
49 | 0001 0010 = 18 -- id with key = 2, version = 1
50 | ```
51 |
52 | Notice how despite reusing a key, we can keep overall ids unique by separating
53 | the id into key and version parts.
54 |
55 | ### Tracking Unused Keys
56 |
57 | There are a few ways to keep track of unused keys for reuse. One way is to use a
58 | stack to track unused keys, pushing onto the stack when an entity is destroyed
59 | and popping from the stack when a new entity is created. ECR instead uses an
60 | array with a linked-list built into it.
61 |
62 | Each index of the array corresponds to a key `k` (`array[k]` corresponds to key
63 | `k`).
64 |
65 |
66 | Implementation
67 |
68 | ---
69 |
70 | Say we have a function `ID(key, version)` which creates an id using the given
71 | key and version.
72 |
73 | When key `k` is in use, `array[k] = ID(k, v)` where
74 | `v` is how many times that key has been used.
75 |
76 | When key `k` is not in use, `array[k] = ID(k_next, v)` where
77 | `k_next` is the *next key not in use* or `0`, that is, a pointer to the next
78 | free key or nothing,
79 | and `v` is still how many times key `k` has been used.
80 |
81 | We keep track of the head of the list using a variable `head` that holds the
82 | first unused key. If an unused key `k` is at the tail of the list, then
83 | `array[k] = ID(0, v)`. A key can never be `0` (arrays in Luau start at 1) so
84 | we use `0` to signify nothing. If the list is empty then `head` is set to `0`.
85 |
86 | When we want to create a new id, we check if `head ~= 0`. If `head == 0` then
87 | we just create a brand new key `#array + 1`. Otherwise, we know that we have an
88 | unused key `k` where `head = k`. We know that `array[k] = ID(next_k, v)`.
89 | So we set `head = next_k` and set `array[k] = ID(k, v)` and return `ID(k, v)`.
90 |
91 | When we want to destroy an id with key `k` we do `array[k] = ID(head, v+1)`
92 | and set `head = k`.
93 |
94 | Difficult to grasp at first but the advantage of this approach is that we can
95 | quickly see if a given key is in use or not instead of searching through a stack
96 | of unused keys, as well as only needing a single array index for each entity,
97 | since both key and version are stored at the one index.
98 |
99 | ---
100 |
101 |
102 |
103 | ### Bit Manipulation and Floats
104 |
105 | In Luau, numbers are 64-bit floating points. The format for these floats are
106 | specified in the IEEE-754 standard. Due to how these floats work, they can
107 | perfectly represent any 53-bit integer with no rounding issues. ECR splits the
108 | 53 bits `[19:0]` for the key and `[52:20]` for the version.
109 |
110 | Extracting the lower 20 bits to get the key is done with a single bitwise `AND`
111 | operation (`bit32.band(id, mask)`) with a 20-bit bitmask
112 | (a very fast operation). As the `bit32` library only works with the lower 32
113 | bits, we cannot access bits `[52:20]` directly. We must instead shift the upper
114 | bits down by 20 places via division by a power of 2.
115 |
116 | Creating an id and getting the key and version of an id looks like:
117 |
118 | ```lua
119 | local KEY_MASK = 2^20 - 1
120 | local LSHIFT = 2^20
121 | local RSHIFT = 1 / LSHIFT
122 |
123 | local function ID_CREATE(key, version)
124 | return version * LSHIFT + key
125 | end
126 |
127 | local function ID_KEY(id)
128 | return bit32.band(id, KEY_MASK)
129 | end
130 |
131 | local function ID_VERSION(id)
132 | return (id - ID_KEY(id)) * RSHIFT
133 | end
134 | ```
135 |
136 | Some things to note are:
137 |
138 | - instead of diving by a power of 2, we multiply by the reciprocal of a power of
139 | 2 as multiplication is faster than division.
140 |
141 | - to extract the version we first do `id - ID_KEY(id)` before shifting, so that
142 | the lower bits become all `0`s. If we skip this step, then the key part would
143 | become a fraction which introduces floating point rounding errors, making it
144 | so we can no longer recover the version accurately.
145 |
146 | Using 20 bits for the key part gives us `2^20 - 1 = 1,048,575` keys (we can't
147 | use 0) which is why that is the limit of the amount of entities you can create
148 | at once.
149 |
150 | Using 33 bits for the version part gives us `2^33 - 1 = 8,589,934,592` versions
151 | (we start versions at 1) so each key can be reused over 8 billion times.
152 |
153 | In the unlikely case that a key has been reused that many times, the key is
154 | permanently deprecated and will not be reused again (as simple as not adding
155 | the key back to the head of the list when it is removed).
156 |
--------------------------------------------------------------------------------
/docs/wiki/component-storage.md:
--------------------------------------------------------------------------------
1 | # Component Storage
2 |
3 | The choice of how components are stored in memory is what defines the
4 | performance of an ECS. There are many different ways to do this, with two
5 | popular approaches being:
6 |
7 | - Archetype based storage
8 | - Sparse set based storage
9 |
10 | Archetype based storages are generally known to have fast iteration in
11 | combinations of components while Sparse set based ECSs are known for fast adding
12 | and removing of components.
13 |
14 | ECR uses a sparse set approach which will now be covered in detail.
15 |
16 | ## The Sparse set
17 |
18 | Firstly, ignore the ECS side of things and just try to understand the sparse set
19 | on its own.
20 |
21 | The sparse set is a datastructure used to store a set of integers.
22 |
23 | The time complexities of a sparse set are as such:
24 |
25 | - Insertion `O(1)`
26 | - Removal `O(1)`
27 | - Lookup `O(1)`
28 | - Iteration `O(n)` where `n` is the amount of elements in the set.
29 |
30 | A sparse set is composed of 2 arrays:
31 |
32 | - Sparse arrray.
33 |
34 | Maps an integer to a dense array index.
35 |
36 | - Dense array (contrary to the name).
37 |
38 | Stores all integers in the set without any spaces.
39 |
40 | An implementation in Luau looks like so:
41 |
42 | ```lua
43 | type Set = {
44 | size: number
45 | sparse: Array
46 | dense: Array
47 | }
48 |
49 | local function has(self: Set, k: number): boolean
50 | return self.sparse[k] ~= nil
51 | end
52 |
53 | local function insert(self: Set, k: number)
54 | if not self.sparse[k] then -- do nothing if k is already in set
55 | local n = self.size + 1; self.size = n
56 | self.sparse[k] = n
57 | self.dense[n] = k
58 | end
59 | end
60 |
61 | local function remove(self: Set, k: number)
62 | local i = self.sparse[k]
63 | if i then -- do nothing if k is not in set
64 | local n = self.size; self.size = n - 1
65 |
66 | -- to remove from a sparse set,
67 | -- we move the last element in the dense array
68 | -- to the position of k in the dense array
69 | -- and update the sparse array to show the new
70 | -- position of the moved element and remove k
71 |
72 | local last = self.dense[n]
73 |
74 | self.dense[i] = last
75 | self.dense[n] = nil
76 |
77 | self.sparse[last] = i
78 | self.sparse[k] = nil
79 | end
80 | end
81 | ```
82 |
83 | The main drawback of a sparse set is that the sparse array must grow to
84 | accomodate the largest key in the set. This can cause wasted memory usage
85 | when the set contains a small amount of integers with a large value.
86 |
87 | This can be mitigated using techniques such as paging, but Luau automatically
88 | handles sparse arrays for us by converting them into a hashmap when it is
89 | sufficiently sparse to save memory. While a hashmap normally isn't the best
90 | solution for this, it is better than any custom Luau solution since this is
91 | built into Luau and is executed natively.
92 |
93 | ## Pools
94 |
95 | Now, back to the ECS. The pool datastructure in ECR is a modified sparse set.
96 |
97 | ```lua
98 | type Pool = {
99 | size: number,
100 | map: Array, -- sparse array
101 | entities: Array, -- dense array
102 | values: Array -- second dense array
103 | }
104 | ```
105 |
106 | This structure uses a second dense array that is kept sorted the same as
107 | the first. The `map` array maps an id to an internal index used to access
108 | a dense array of ids and a dense array of corresponding component values.
109 |
110 | When adding and removing ids, whatever operation done to the `entities` array is
111 | also done to the `values` array so that `entities[i]` corresponds to `values[i]`.
112 |
113 | The way in which `map` maps an id to an internal index is by extracting a key
114 | from the id and using that key as an index for the `map` array.
115 | Refer to [here](entity-identifier.md) for specifics on keys and ids.
116 |
117 | In an ECS, each component type has its own pool. Given the properties of a
118 | sparse set, this makes adding, removing, changing and checking for components
119 | very fast operations. We have the best possible random-access and
120 | sequential-access speeds because of the sparse and dense arrays. However,
121 | because component pools are independent from each other, this approach suffers
122 | when querying for multiple component types.
123 |
124 | ### Single component query
125 |
126 | A query for a single component type is the best case scenario. To do so, you
127 | simply iterate over the `entities` and `values` array of a pool as a straight
128 | array with no cache misses.
129 |
130 | ### Multiple component query
131 |
132 | Multiple component queries, while still fast, become slower the more components
133 | that are involved in the query. To do so, you pick the smallest pool to iterate
134 | along its entities (as an entity must contain every component in the query, we
135 | know that every entity we need still exists in this pool so we iterate that to
136 | reduce the amount of random checking we must do), while doing lookups in all
137 | other pools to check if the entity has all other components, skipping that
138 | entity if it does not. While this does introduce cache misses, it isn't as bad
139 | as it sounds because of the very fast random-access property of sparse sets.
140 |
141 | This drawback can also be mitigated using a technique called *grouping* which I
142 | won't get into now.
143 |
144 | Another thing to note is that while iteration of multiple components is
145 | a large argument against the sparse-set ECS, more complex queries naturally
146 | run more complex code in the loop body, making the overhead of iterating
147 | multiple components less significant in many cases. In the less likely cases
148 | where the iteration itself is the bottleneck, grouping can be used to eliminate
149 | this entirely.
150 |
151 | ## Stale references
152 |
153 | This section assumes familiarity with [entity ids](entity-identifier.md).
154 |
155 | Consider the case where we have ids `ID(k=1, v=1)` and `ID(k=1, v=2)`. Both
156 | of these ids would refer to the same internal index in the pool because the
157 | pool only uses the key part and completely disregards the version. This leads to
158 | issues such as destroyed ids being able to access data for new ids.
159 |
160 | This can be prevented by checking the `entities` array to get the version when
161 | performing a lookup. This however turns a lookup operation from a single array
162 | index to two array indexes, which increases the chance of cache miss and harms
163 | performance.
164 |
165 | ECR instead stores the id version inside the `map` array as well.
166 | Since the `map` array just stores the internal index for the dense arrays,
167 | it is known that this internal index will never exceed the largest id key. This
168 | is taken advantage of by also using the upper bits to store the id version.
169 | This way we can get the internal index *and* the version using a single array
170 | index. Then it is just a matter of checking if the version is equal to the
171 | version of the id we are performing a lookup for.
172 |
--------------------------------------------------------------------------------
/docs/tut/crash-course.md:
--------------------------------------------------------------------------------
1 | # Crash Course
2 |
3 | ECR is a sparse-set based ECS library heavily inspired by
4 | [EnTT](https://github.com/skypjack/entt).
5 |
6 | This is a brief introduction on how to use the library and some of the concepts
7 | involved with it.
8 |
9 | ## Registry
10 |
11 | The registry, or often called a *world*, is a container for entities and their
12 | components.
13 |
14 | ```lua
15 | local registry = ecr.registry()
16 | ```
17 |
18 | ## Entities
19 |
20 | An entity represents an object in the world and is referenced using a unique id.
21 |
22 | ```lua
23 | local id: ecr.entity = registry:create()
24 | registry:contains(id) -- true
25 |
26 | registry:destroy(id)
27 | registry:contains(id) -- false
28 | ```
29 |
30 | Entities can be freely created and destroyed.
31 |
32 | ## Components
33 |
34 | A component is data that can be added to entities.
35 |
36 | There is the *component type*, which represents a type of data, and there is
37 | the *component value*, which is a value for a type added to an entity.
38 |
39 | Component types are created by `ecr.component()`, which returns an id
40 | representing that type. This can be typecasted to the type of value it
41 | represents.
42 |
43 | ```lua
44 | local Name = ecr.component() :: string
45 | local Health = ecr.component() :: number
46 | ```
47 |
48 | Entities can have any amount of components added to or removed from them,
49 | whenever. Like tables, they can have any amount of key-value pairs, where the
50 | component type is the key, and the component value is the value. Entities
51 | initially have no components when created, and will have all their components
52 | removed when destroyed.
53 |
54 | ```lua
55 | registry:set(id, Health, 100) -- adds a new component with a value of 100
56 | registry:get(id, Health) -- 100
57 |
58 | registry:remove(id, Health) -- removes the component
59 | registry:has(id, Health) -- false
60 | ```
61 |
62 | Component values cannot be `nil`, components should be removed instead.
63 |
64 | ## Views
65 |
66 | You can get all entities that have a specific set of components by using a view.
67 |
68 | Views can include any amounts of components. A view only returns entities that
69 | have *at least* all the components included.
70 |
71 | ```lua
72 | for id, position, velocity in registry:view(Position, Velocity) do
73 | world:set(id, Position, position + velocity * 1/60)
74 | end
75 | ```
76 |
77 | You can add or remove components and create or destroy entities during
78 | iteration.
79 |
80 | Components added or entities created during iteration will not be returned
81 | during that iteration.
82 |
83 | You can also exclude component types from views. Any entities that have an
84 | excluded type will not be included in the view.
85 |
86 | ```lua
87 | local view = registry:view(A):exclude(B)
88 | ```
89 |
90 | Views are cheap to create and do not store their own state, so they do not need
91 | to be stored aside, and can be created on the fly as needed.
92 |
93 | ## Signals
94 |
95 | The registry contains three different signals for when a component type is
96 | added, changed or removed for any entity.
97 |
98 | ```lua
99 | registry:on_add(type):connect(listener)
100 | registry:on_change(type):connect(listener)
101 | registry:on_remove(type):connect(listener)
102 | ```
103 |
104 | All three listeners are called with:
105 |
106 | 1. The entity being acted on.
107 | 2. The new component value (always `nil` in the case of `on_remove`).
108 |
109 | `on_add` is fired *after* the component is added.
110 |
111 | `on_change` and `on_remove` is fired *before* the component is changed or
112 | removed, so you can still retrieve the old value if needed.
113 |
114 | You cannot modify the registry within a listener, they should only be used to
115 | help track entities or clean up values.
116 |
117 | ## Observers
118 |
119 | An observer is similar to a view, except it only returns entities whose
120 | components have been added or changed and still have those components at the
121 | time of iteration.
122 |
123 | An observer can be created using `Registry:track()`.
124 |
125 | ```lua
126 | local observer = registry:track(Position, Model)
127 |
128 | return function()
129 | for entity, position, model in observer do
130 | print("changed: ", position, model)
131 | end
132 | end
133 | ```
134 |
135 | After iterating, the observer automatically clears so that only fresh changes
136 | are iterated.
137 |
138 | Observers provide a concise way to act only on a subset of entities that had
139 | components updated since the last time a system ran.
140 |
141 | Unlike a view, observers do store their own state, and must be stored aside to
142 | keep track over time.
143 |
144 | ## Example Usage
145 |
146 | All component types are defined in a single file to keep things organised. All
147 | component types must also be defined before the registry using them is created.
148 |
149 | ::: code-group
150 |
151 | ```lua [cts.luau]
152 | local ecr = require(ecr)
153 |
154 | local cts = {
155 | Health = ecr.component() :: number,
156 | Poisoned = ecr.component() :: number
157 | }
158 |
159 | ecr.name(cts)
160 |
161 | return cts
162 | ```
163 |
164 | :::
165 |
166 | `ecr.name()` can be used to associate names with components, for clearer error
167 | messages when debugging.
168 |
169 | The library doesn't have any bult-in support for systems, the user is free to
170 | do this however they please.
171 |
172 | Examples using plain functions:
173 |
174 | ::: code-group
175 |
176 | ```lua [deal_poison_damage.luau]
177 | local cts = require(cts)
178 |
179 | return function(world: ecr.Registry, dt: number)
180 | for id, health in world:view(cts.Health, cts.Poisoned) do
181 | world:set(id, health, health - 10 * dt)
182 | end
183 | end
184 | ```
185 |
186 | ```lua [reduce_poison_timer.luau]
187 | local cts = require(cts)
188 |
189 | return function(world: ecr.Registry, dt: number)
190 | for id, time in world:view(cts.Poisoned) do
191 | local new_time = time - dt
192 |
193 | if new_time <= 0 then
194 | world:remove(id, cts.Poisoned)
195 | else
196 | world:set(id, cts.Poisoned, new_time)
197 | end
198 | end
199 | end
200 | ```
201 |
202 | ```lua [destroy_dead.luau]
203 | local cts = require(cts)
204 |
205 | return function(world: ecr.Registry)
206 | for id, health in world:view(cts.Health) do
207 | if health <= 0 then
208 | world:destroy(id)
209 | end
210 | end
211 | end
212 | ```
213 |
214 | :::
215 |
216 | ::: code-group
217 |
218 | ```lua [main.luau]
219 | local ecr = require(ecr)
220 | local cts = require(cts)
221 |
222 | local function loop(systems: {(ecr.Registry, number) -> ()}, world: ecr.Registry, dt: number)
223 | for _, system in systems do
224 | system(world, dt)
225 | end
226 | end
227 |
228 | local systems = {
229 | require(deal_poison_damage),
230 | require(reduce_poison_timer),
231 | require(destroy_dead)
232 | }
233 |
234 | local world = ecr.registry()
235 |
236 | for i = 1, 10 do
237 | local id = world:create()
238 | world:set(id, cts.Health, 100)
239 | world:set(id, cts.Poisoned, math.random(3, 5)) -- poison randomly for 3-5 seconds
240 | end
241 |
242 | while true do
243 | loop(systems, world, 1/60)
244 | end
245 | ```
246 |
247 | :::
248 |
249 | ## End
250 |
251 | At this point, the main concepts and features of ECR have been covered.
252 | You can read other guides on more advanced usage or view the API for more
253 | details.
254 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6 |
7 | ---
8 |
9 | ## Unreleased
10 |
11 | ### Improved
12 |
13 | - Iteration of 3 and 4 component views 3x faster.
14 |
15 | ### Fixed
16 |
17 | - Method `Registry:create(id)` causing an out of bounds buffer access if key was already in use with a different version.
18 |
19 | ---
20 |
21 | ## [0.9.0] - 2025-03-02
22 |
23 | ### Added
24 |
25 | - Method `Registry:find()`.
26 | - Method `Registry:insert()`.
27 | - Method `Registry:copy()`.
28 | - Overload `ecr.queue()` to accept a function connector.
29 | - Overload `ecr.registry()` to restrict entity range.
30 | - Function `ecr.buffer_to_buffer()`.
31 |
32 | ### Changed
33 |
34 | - Method `Registry:patch()` will return the new value.
35 |
36 | ### Removed
37 |
38 | - Method `Observer:persist()`.
39 | - Method `View:use()`.
40 |
41 | ### Fixed
42 |
43 | - Single type view not excluding properly with multiple exclude types.
44 | - `Connection:disconnect()` disconnecting the wrong listener.
45 |
46 | ---
47 |
48 | ## [0.8.0] - 2023-12-05
49 |
50 | ### Added
51 |
52 | - Constant `ecr.context`.
53 | - Constant `id_size`.
54 | - Function `ecr.array_to_buffer()`.
55 | - Function `ecr.buffer_to_array()`.
56 | - Overload for `Registry:storage()` to get an iterator for all storages.
57 |
58 | ### Changed
59 |
60 | - All components must be defined before the registry using them is created.
61 | - The context entity does not exist until `Registry:context()` is first called.
62 | - The context entity can now be destroyed.
63 | - Observers are now empty when first created.
64 | - Entity id size reduced from 8 bytes to 4 bytes.
65 | - Max entities that can exist at once is now `2^16-1` (`65,535`).
66 |
67 | - Renamed `ecr.Entity` type alias to `ecr.entity`.
68 | - Renamed `Registry:added()` to `Registry:on_add()`.
69 | - Renamed `Registry:changed()` to `Registry:on_change()`
70 | - Renamed `Registry:removing()` to `Registry:on_remove()`.
71 | - Renamed `Registry:orphaned()` to `Registry:has_none()`.
72 |
73 | - Property `Pool.entities` type is now a `buffer`.
74 |
75 | - Method `Registry:set()` can now also add tags.
76 | - Method `Observer:disconnect()` can only be called on empty observers.
77 |
78 | - Signal `Registry:on_change()` now fires *before* the component is changed.
79 | - New value still given as argument, but now can retrieve old value with `Registry:get()`.
80 |
81 | ### Removed
82 |
83 | - Method `Registry:version()`.
84 | - Method `Registry:current()`.
85 |
86 | ### Improved
87 |
88 | - Large refactor to use the new Luau `buffer` datatype. Up to 2x faster and 2-4x
89 | less memory usage across the board.
90 |
91 | ---
92 |
93 | ## [0.7.0] - 2023-07-20
94 |
95 | ### Added
96 |
97 | - `Connection:reconnect()`.
98 | - `ecr.entity` which is a built-in component that can be used to access a
99 | dedicated entity pool:
100 | - Exclude-only views `registry:view(ecr.entity):exclude(...)`.
101 | - Signals for creation and destruction of entities `registry:on_add(ecr.entity):connect()`.
102 | - Direct access to array of all entities in the registry `registry:storage(ecr.entity)`.
103 | - `ecr.is_tag()` to check if a given component is a tag type.
104 | - `Registry:context()` to store components not specific to an entity.
105 | - Methods for lower level access to pools.
106 |
107 | ### Changed
108 |
109 | - Using `Registry:set()` to set `nil` values will now error.
110 | - Use `Registry:remove()` instead.
111 | - `Registry:handle()` calls are now memoized, passing the same ids will return
112 | the same handle objects.
113 |
114 | ### Removed
115 |
116 | - Operations `#Registry` and `for id in Registry do`.
117 | - Replaced by entity storage.
118 |
119 | ---
120 |
121 | ## [0.6.1] - 2023-07-04
122 |
123 | ### Changed
124 |
125 | - `Registry:patch()` will invoke the component constructor if the entity does
126 | not have the given component.
127 |
128 | ### Fixed
129 |
130 | - `Registry:orphaned()` not erroring with destroyed id.
131 | - `ecr.queue()` clearing early.
132 |
133 | ---
134 |
135 | ## [0.6.0] - 2023-06-27
136 |
137 | ### Added
138 |
139 | - Check for iterator invalidation occuring with group misusage.
140 | - Function `ecr.tag()` to create valueless components.
141 | - Function `ecr.queue()` and the `Queue` class.
142 | - Function `ecr.name()` for associating names with component types.
143 | - Method `Registry:handle()` and the `Handle` class.
144 | - Method `Registry:try_get()`.
145 | - Method `Observer:persist()`.
146 | - Length operator for registry `#Registry`.
147 | - Iteration over registry `for id in Registry do`.
148 |
149 | ### Changed
150 |
151 | - Observers will automatically clear themselves after iteration by default.
152 | - Call `Observer:persist()` to stop this.
153 | - `Registry:get()` will now error if the entity does not have every component.
154 |
155 | ### Removed
156 |
157 | - Method `Registry:version()`.
158 | - Method `Registry:current()`.
159 | - Method `Registry:entities()`.
160 | - Method `Registry:size()`.
161 |
162 | ---
163 |
164 | ## [0.5.0] - 2023-05-14
165 |
166 | ### Changed
167 |
168 | - Connecting or disconnecting an already connected or disconnected observer will no longer error.
169 | - Excluding an already excluded component with `View:exclude()` and `Observer:exclude()` no longer errors.
170 | - Method `Registry:add()` will do nothing if the entity already has the component.
171 | - Method `Registry:orphan()` renamed to `Registry:orphaned()`.
172 | - Method `Registry:valid()` renamed to `Registry:contains()`.
173 | - Method `Registry:create()` is now guaranteed to always return unique identifiers.
174 | - Using invalid entities no longer causes undefined behavior in any method and instead errors.
175 | - `Registry:track()` will now track all components passed, not just the first one.
176 |
177 | ### Removed
178 |
179 | - Methods:
180 | - `View:each()`
181 | - `Observer:each()`
182 | - `Group:each()`
183 |
184 | ## Fixed
185 |
186 | - Undefined behavior sometimes occuring when removing grouped components.
187 | - Observers not returning up-to-date values when changing a component while it is disconnected.
188 |
189 | ---
190 |
191 | ## [0.4.0] - 2023-01-26
192 |
193 | ### Added
194 |
195 | - Component grouping and `Registry:group()`.
196 | - Method `View:use()`.
197 | - Constant `ecr.null`.
198 |
199 | ### Removed
200 |
201 | - Method `View:include()` and `Observer:include()`.
202 |
203 | ### Fixed
204 |
205 | - `Registry:add()` not firing `Registry:on_add()` signals.
206 | - Mismatch between argument list and values returned in multi-typed observers.
207 | - Observers not garbage collecting after calling `Observer:disconnect()`.
208 |
209 | ### Improved
210 |
211 | - Connection firing speed by ~70%.
212 |
213 | ---
214 |
215 | ## [0.3.0] - 2023-01-09
216 |
217 | ### Added
218 |
219 | - Overload for `Registry:create()` to create an entity with a given identifier.
220 |
221 | ### Changed
222 |
223 | - Registry signals no longer pass the registry as the first argument to listeners.
224 | - Observers no longer track entities with removed components.
225 | - Method `Registry:entities()` now creates and returns an array of only valid entities.
226 | - Function `ecr.registry()` can no longer pre-allocate memory.
227 |
228 | ### Removed
229 |
230 | - Method `Registry:capacity()`.
231 |
232 | ### Improved
233 |
234 | - Double-type view iteration speed by ~100%.
235 |
236 | ---
237 |
238 | ## [0.2.0] - 2022-12-08
239 |
240 | ### Added
241 |
242 | - Method `View:include()` and `Observer:include()`.
243 | - Method `Registry:patch()`.
244 | - Method `Registry:add()` and optional default parameter for `ecr.component()`.
245 |
246 | ### Changed
247 |
248 | - Behavior `for ... in View do` now behaves the same as `for ... in View:each() do`.
249 | - Signal diconnect API (Signal now returns a connection object to call disconnect on).
250 |
251 | ### Improved
252 |
253 | - Entity creation and release speed by ~100%.
254 | - Multi-type view iteration speed by ~60%.
255 |
256 | ## [0.1.0] - 2022-11-16
257 |
258 | - Initial release.
259 |
--------------------------------------------------------------------------------
/docs/api/Registry.md:
--------------------------------------------------------------------------------
1 | # Registry
2 |
3 | Container for entities and components.
4 |
5 | ```lua
6 | type ecr.Registry
7 | ```
8 |
9 | ## Methods
10 |
11 | ::: warning
12 | There are certain [restrictions](restrictions) with what you can do with the
13 | registry that you should be aware of.
14 | :::
15 |
16 | ### create()
17 |
18 | Creates a new entity and returns the entity id.
19 |
20 | - **Type**
21 |
22 | ```lua
23 | function Registry:create(): entity
24 | function Registry:create(id: entity): entity
25 |
26 | type entity = ecr.entity
27 | ```
28 |
29 | - **Details**
30 |
31 | An entity can be created using a specific id that was created by another
32 | registry or previously by the same registry.
33 |
34 | A previously used but now unused id may be reused every `32,000` creations.
35 |
36 | ::: warning
37 | Be wary of storing ids of destroyed entities for long periods of time or
38 | they may eventually refer to a newly created entity.
39 | :::
40 |
41 | ::: warning
42 | The total amount of entities in a registry at any given time *cannot*
43 | exceed `65,535`. Attempting to exceed this limit will throw an error.
44 | :::
45 |
46 | --------------------------------------------------------------------------------
47 |
48 | ### destroy()
49 |
50 | Removes the entity from the registry and removes all of its components.
51 |
52 | - **Type**
53 |
54 | ```lua
55 | function Registry:destroy(id: entity)
56 | ```
57 |
58 | --------------------------------------------------------------------------------
59 |
60 | ### contains()
61 |
62 | Checks if the given entity exists in the registry.
63 |
64 | - **Type**
65 |
66 | ```lua
67 | function Registry:contains(id: entity): boolean
68 | ```
69 |
70 | --------------------------------------------------------------------------------
71 |
72 | ### add()
73 |
74 | Adds all given components to an entity.
75 |
76 | - **Type**
77 |
78 | ```lua
79 | function Registry:add(id: entity, components: T...)
80 | ```
81 |
82 | - **Details**
83 |
84 | Adds the given components to the entity by using each component
85 | constructor or no value at all if the component is a tag type.
86 |
87 | Adding a component to an entity that already has the component will do
88 | nothing.
89 |
90 | --------------------------------------------------------------------------------
91 |
92 | ### set()
93 |
94 | Adds or changes an entity's component.
95 |
96 | - **Type**
97 |
98 | ```lua
99 | function Registry:set(id: entity, component: T, value: T)
100 | ```
101 |
102 | - **Details**
103 |
104 | Adds the component to the entity with the given value if the entity does not
105 | already have the component.
106 |
107 | Changes the component value for the given entity if the entity already has
108 | the component.
109 |
110 | --------------------------------------------------------------------------------
111 |
112 | ### patch()
113 |
114 | Updates an entity's component.
115 |
116 | - **Type**
117 |
118 | ```lua
119 | function Registry:patch(id: entity, component: T, patcher: (T) -> T): T
120 | ```
121 |
122 | - **Details**
123 |
124 | Takes a callback which is given the current component value as the only
125 | argument. The value returned by the callback is then set as the new value.
126 |
127 | If there is a constructor defined for the given component and the entity
128 | does not have the component, the constructor will be called and the returned
129 | value passed into the callback.
130 |
131 | - **Example**
132 |
133 | ```lua
134 | registry:patch(entity, Health, function(health)
135 | return health - 10
136 | end)
137 | ```
138 |
139 | --------------------------------------------------------------------------------
140 |
141 | ### has()
142 |
143 | Checks if an entity has all of the given components.
144 |
145 | - **Type**
146 |
147 | ```lua
148 | function Registry:has(id: entity, components: T...): boolean
149 | ```
150 |
151 | - **Details**
152 |
153 | Will return `true` only if the entity has *every* component given.
154 |
155 | --------------------------------------------------------------------------------
156 |
157 | ### get()
158 |
159 | Gets an entity's component values.
160 |
161 | - **Type**
162 |
163 | ```lua
164 | function Registry:get(id: entity, components: T...): T...
165 | ```
166 |
167 | - **Details**
168 |
169 | Will error if the entity does not have a component.
170 |
171 | --------------------------------------------------------------------------------
172 |
173 | ### try_get()
174 |
175 | Gets an entity's component value.
176 |
177 | - **Type**
178 |
179 | ```lua
180 | function Registry:try_get(id: entity, components: T): T?
181 | ```
182 |
183 | - **Details**
184 |
185 | Will return `nil` if the entity does not have a component.
186 |
187 | --------------------------------------------------------------------------------
188 |
189 | ### remove()
190 |
191 | Removes the given components from an entity.
192 |
193 | - **Type**
194 |
195 | ```lua
196 | function Registry:remove(id: entity, components: T...)
197 | ```
198 |
199 | - **Details**
200 |
201 | Will do nothing if the entity does not have a component.
202 |
203 | --------------------------------------------------------------------------------
204 |
205 | ### clear()
206 |
207 | Removes all entities and components from the registry.
208 |
209 | - **Type**
210 |
211 | ```lua
212 | function Registry:clear(components: T...)
213 | ```
214 |
215 | - **Details**
216 |
217 | If components are given, removes all given components from all entities
218 | without destroying the entities.
219 |
220 | If no components are given, then all entities in the registry will be
221 | destroyed.
222 |
223 | --------------------------------------------------------------------------------
224 |
225 | ### find()
226 |
227 | Returns the first entity found that has a component matching the given value.
228 |
229 | - **Type**
230 |
231 | ```lua
232 | function Registry:find(component: T, value: T): entity?
233 | ```
234 |
235 | - **Details**
236 |
237 | This is a linear search.
238 |
239 | --------------------------------------------------------------------------------
240 |
241 | ### copy()
242 |
243 | Copies the values of a component and pastes it into another component
244 |
245 | - **Type**
246 |
247 | ```lua
248 | function Registry:copy(copy: T, paste: T)
249 | ```
250 |
251 | - **Details**
252 |
253 | This removes any entities that don't have a value in the copied component
254 | from the pasted component. It does not fire signals when called.
255 |
256 | --------------------------------------------------------------------------------
257 |
258 | ### view()
259 |
260 | Creates a [`view`](View) with the given component types.
261 |
262 | - **Type**
263 |
264 | ```lua
265 | function Registry:view(components: T...): View
266 | ```
267 |
268 | --------------------------------------------------------------------------------
269 |
270 | ### track()
271 |
272 | Creates an [`observer`](Observer) with the given component types.
273 |
274 | - **Type**
275 |
276 | ```lua
277 | function Registry:track(...: T...): Observer
278 | ```
279 |
280 | --------------------------------------------------------------------------------
281 |
282 | ### group()
283 |
284 | Creates a [`group`](Group.md) with the given component types.
285 |
286 | - **Type**
287 |
288 | ```lua
289 | function Registry:group(...: T...): Group
290 | ```
291 |
292 | - **Details**
293 |
294 | Rearranges the internal storage of components for better iteration
295 | performance when iterated together.
296 |
297 | Groups must be mutually exclusive, i.e. each component type can only belong
298 | to a single group.
299 |
300 | ::: warning
301 | This method introduces [restrictions](../tuts/groups.md#limitations) on
302 | adding components during views.
303 | :::
304 |
305 | --------------------------------------------------------------------------------
306 |
307 | ### on_add()
308 |
309 | Returns a [signal](Signal) which is fired whenever the given component type is
310 | added to an entity.
311 |
312 | - **Type**
313 |
314 | ```lua
315 | function Registry:on_add(component: T): Signal
316 | ```
317 |
318 | The signal is fired *after* the component is changed.
319 |
320 | The listener is called with the entity and new component value.
321 |
322 | ::: warning
323 | The registry cannot be modified within a listener.
324 | :::
325 |
326 | --------------------------------------------------------------------------------
327 |
328 | ### on_change()
329 |
330 | Returns a [signal](Signal) which is fired whenever the given component type is
331 | changed for an entity.
332 |
333 | - **Type**
334 |
335 | ```lua
336 | function Registry:on_change(component: T): Signal
337 | ```
338 |
339 | The signal is fired *before* the component is changed.
340 |
341 | The listener is called with the entity and new component value.
342 |
343 | ::: warning
344 | The registry cannot be modified within a listener.
345 | :::
346 |
347 | --------------------------------------------------------------------------------
348 |
349 | ### on_remove()
350 |
351 | Returns a [signal](Signal) which is fired whenever the given component is
352 | removed from an entity.
353 |
354 | - **Type**
355 |
356 | ```lua
357 | function Registry:on_remove(component: T): Signal
358 | ```
359 |
360 | - **Details**
361 |
362 | The signal is fired *before* the component is removed.
363 |
364 | The listener is called with the entity.
365 |
366 | ::: warning
367 | The registry cannot be modified within a listener.
368 | :::
369 |
370 | --------------------------------------------------------------------------------
371 |
372 | ### handle()
373 |
374 | Returns a [handle](Handle) to an entity.
375 |
376 | - **Type**
377 |
378 | ```lua
379 | function Registry:handle(id: entity?): Handle
380 | ```
381 |
382 | - **Details**
383 |
384 | If no entity is given then a new one is created.
385 |
386 | Handles are cached so that `registry:handle(id) == registry:handle(id)` is
387 | always true.
388 |
389 | --------------------------------------------------------------------------------
390 |
391 | ### context()
392 |
393 | Returns a [handle](Handle) to the context entity.
394 |
395 | - **Type**
396 |
397 | ```lua
398 | function Registry:context(): Handle
399 | ```
400 |
401 | - **Details**
402 |
403 | Will automatically create the context entity if it does not already exist.
404 |
405 | --------------------------------------------------------------------------------
406 |
407 | ### storage()
408 |
409 | Returns the [pool](Pool) for a given component type.
410 |
411 | - **Type**
412 |
413 | ```lua
414 | function Registry:storage(component: T): Pool
415 | function Registry:storage(): () -> (unknown, Pool)
416 | ```
417 |
418 | - **Details**
419 |
420 | If called with no arguments, returns an iterator to get all component types
421 | and their corresponding pool in the registry.
422 |
423 | --------------------------------------------------------------------------------
424 |
425 | ### has_none()
426 |
427 | Checks if the given entity has no components.
428 |
429 | - **Type**
430 |
431 | ```lua
432 | function Registry:has_none(id: entity): boolean
433 | ```
434 |
435 | --------------------------------------------------------------------------------
436 |
437 | ### release()
438 |
439 | Removes the entity from the registry.
440 |
441 | - **Type**
442 |
443 | ```lua
444 | function Registry:release(id: entity)
445 | ```
446 |
447 | - **Details**
448 |
449 | ::: danger
450 | This method does not remove any of the entity's components. Using this
451 | method on an entity that still has components is *undefined behavior*.
452 | :::
453 |
--------------------------------------------------------------------------------
/test/testkit.luau:
--------------------------------------------------------------------------------
1 | --------------------------------------------------------------------------------
2 | -- testkit.luau
3 | -- v0.7.3
4 | --------------------------------------------------------------------------------
5 |
6 | local color = {
7 | white_underline = function(s: string)
8 | return `\27[1;4m{s}\27[0m`
9 | end,
10 |
11 | white = function(s: string)
12 | return `\27[37;1m{s}\27[0m`
13 | end,
14 |
15 | green = function(s: string)
16 | return `\27[32;1m{s}\27[0m`
17 | end,
18 |
19 | red = function(s: string)
20 | return `\27[31;1m{s}\27[0m`
21 | end,
22 |
23 | yellow = function(s: string)
24 | return `\27[33;1m{s}\27[0m`
25 | end,
26 |
27 | red_highlight = function(s: string)
28 | return `\27[41;1;30m{s}\27[0m`
29 | end,
30 |
31 | green_highlight = function(s: string)
32 | return `\27[42;1;30m{s}\27[0m`
33 | end,
34 |
35 | gray = function(s: string)
36 | return `\27[30;1m{s}\27[0m`
37 | end,
38 | }
39 |
40 | local function convert_units(unit: string, value: number): (number, string)
41 | local prefix_colors = {
42 | [4] = color.red,
43 | [3] = color.red,
44 | [2] = color.yellow,
45 | [1] = color.yellow,
46 | [0] = color.green,
47 | [-1] = color.red,
48 | [-2] = color.yellow,
49 | [-3] = color.green,
50 | [-4] = color.red
51 | }
52 |
53 | local prefixes = {
54 | [4] = "T",
55 | [3] ="G",
56 | [2] ="M",
57 | [1] = "k",
58 | [0] = " ",
59 | [-1] = "m",
60 | [-2] = "u",
61 | [-3] = "n",
62 | [-4] = "p"
63 | }
64 |
65 | local order = 0
66 |
67 | while value >= 1000 do
68 | order += 1
69 | value /= 1000
70 | end
71 |
72 | while value ~= 0 and value < 1 do
73 | order -= 1
74 | value *= 1000
75 | end
76 |
77 | if value >= 100 then
78 | value = math.floor(value)
79 | elseif value >= 10 then
80 | value = math.floor(value * 1e1) / 1e1
81 | elseif value >= 1 then
82 | value = math.floor(value * 1e2) / 1e2
83 | end
84 |
85 | return value, prefix_colors[order](prefixes[order] .. unit)
86 | end
87 |
88 | local WALL = color.gray "│"
89 |
90 | --------------------------------------------------------------------------------
91 | -- Testing
92 | --------------------------------------------------------------------------------
93 |
94 | type Test = {
95 | name: string,
96 | case: Case?,
97 | cases: { Case },
98 | duration: number,
99 | error: {
100 | message: string,
101 | trace: string
102 | }?
103 | }
104 |
105 | type Case = {
106 | name: string,
107 | result: number,
108 | line: number?
109 | }
110 |
111 | local PASS, FAIL, NONE, ERROR = 1, 2, 3, 4
112 |
113 | local skip: string?
114 | local test: Test?
115 | local tests: { Test } = {}
116 |
117 | local function output_test_result(test: Test)
118 | print(color.white(test.name))
119 |
120 | for _, case in test.cases do
121 | local status = ({
122 | [PASS] = color.green "PASS",
123 | [FAIL] = color.red "FAIL",
124 | [NONE] = color.yellow "NONE",
125 | [ERROR] = color.red "FAIL"
126 | })[case.result]
127 |
128 | local line = case.result == FAIL and color.red(`{case.line}:`) or ""
129 |
130 | print(`{status}{WALL} {line}{color.gray(case.name)}`)
131 | end
132 |
133 | if test.error then
134 | print(color.gray "error: " .. color.red(test.error.message))
135 | print(color.gray "trace: " .. color.red(test.error.trace))
136 | else
137 | print()
138 | end
139 | end
140 |
141 | local function CASE(name: string)
142 | assert(test, "no active test")
143 |
144 | local case = {
145 | name = name,
146 | result = NONE
147 | }
148 |
149 | test.case = case
150 | table.insert(test.cases, case)
151 | end
152 |
153 | local function CHECK(value: T, stack: number?): T
154 | assert(test, "no active test")
155 | local case = test.case
156 |
157 | if not case then
158 | CASE ""
159 | case = test.case
160 | end
161 |
162 | assert(case, "no active case")
163 |
164 | if case.result ~= FAIL then
165 | case.result = value and PASS or FAIL
166 | case.line = debug.info(stack and stack + 1 or 2, "l")
167 | end
168 |
169 | return value
170 | end
171 |
172 | local function TEST(name: string, fn: () -> ())
173 | if skip and name ~= skip then return end
174 |
175 | local active = test
176 | assert(not active, "cannot start test while another test is in progress")
177 |
178 | test = {
179 | name = name,
180 | cases = {},
181 | duration = 0
182 | }; assert(test)
183 |
184 | table.insert(tests, test)
185 |
186 | local start = os.clock()
187 | local err
188 | local success = xpcall(fn, function(m: string)
189 | err = { message = m, trace = debug.traceback(nil, 2) }
190 | end)
191 | test.duration = os.clock() - start
192 |
193 | if not test.case then CASE "" end
194 | assert(test.case, "no active case")
195 |
196 | if not success then
197 | test.case.result = ERROR
198 | test.error = err
199 | end
200 |
201 | test = nil
202 | end
203 |
204 | local function FINISH(): boolean
205 | local success = true
206 | local total_cases = 0
207 | local passed_cases = 0
208 | local duration = 0
209 |
210 | for _, test in tests do
211 | duration += test.duration
212 | for _, case in test.cases do
213 | total_cases += 1
214 | if case.result == PASS or case.result == NONE then
215 | passed_cases += 1
216 | else
217 | success = false
218 | end
219 | end
220 |
221 | output_test_result(test)
222 | end
223 |
224 | print(color.gray(string.format(
225 | `{passed_cases}/{total_cases} test cases passed in %.3f ms.`,
226 | duration*1e3
227 | )))
228 |
229 | local fails = total_cases - passed_cases
230 |
231 | print(
232 | (
233 | fails > 0
234 | and color.red
235 | or color.green
236 | )(`{fails} {fails == 1 and "fail" or "fails"}`)
237 | )
238 |
239 | return success, table.clear(tests)
240 | end
241 |
242 | local function SKIP(name: string)
243 | assert(not test, "cannot skip during test")
244 | skip = name
245 | end
246 |
247 | --------------------------------------------------------------------------------
248 | -- Benchmarking
249 | --------------------------------------------------------------------------------
250 |
251 | type Bench = {
252 | time_start: number?,
253 | memory_start: number?,
254 | iterations: number?
255 | }
256 |
257 | local bench: Bench?
258 |
259 | function START(iter: number?): number
260 | local n = iter or 1
261 | assert(n > 0, "iterations must be greater than 0")
262 | assert(bench, "no active benchmark")
263 | assert(not bench.time_start, "clock was already started")
264 |
265 | bench.iterations = n
266 | bench.memory_start = gcinfo()
267 | bench.time_start = os.clock()
268 | return n
269 | end
270 |
271 | local function BENCH(name: string, fn: () -> ())
272 | local active = bench
273 | assert(not active, "a benchmark is already in progress")
274 |
275 | bench = {}; assert(bench)
276 |
277 | ;(collectgarbage :: any)("collect")
278 |
279 | local mem_start = gcinfo()
280 | local time_start = os.clock()
281 | local err_msg: string?
282 |
283 | local success = xpcall(fn, function(m: string)
284 | err_msg = m .. debug.traceback(nil, 2)
285 | end)
286 |
287 | local time_stop = os.clock()
288 | local mem_stop = gcinfo()
289 |
290 | if not success then
291 | print(`{WALL}{color.red("ERROR")}{WALL} {name}`)
292 | print(color.gray(err_msg :: string))
293 | else
294 | time_start = bench.time_start or time_start
295 | mem_start = bench.memory_start or mem_start
296 |
297 | local n = bench.iterations or 1
298 | local d, d_unit = convert_units("s", (time_stop - time_start) / n)
299 | local a, a_unit = convert_units("B", math.round((mem_stop - mem_start) / n * 1e3))
300 |
301 | local function round(x: number): string
302 | return x > 0 and x < 10 and (x - math.floor(x)) > 0
303 | and string.format("%2.1f", x)
304 | or string.format("%3.f", x)
305 | end
306 |
307 | print(string.format(
308 | `%s %s %s %s{WALL} %s`,
309 | color.gray(round(d)),
310 | d_unit,
311 | color.gray(round(a)),
312 | a_unit,
313 | color.gray(name)
314 | ))
315 | end
316 |
317 | bench = nil
318 | end
319 |
320 | --------------------------------------------------------------------------------
321 | -- Printing
322 | --------------------------------------------------------------------------------
323 |
324 | local function print2(v: unknown)
325 | type Buffer = { n: number, [number]: string }
326 | type Cyclic = { n: number, [{}]: number }
327 |
328 | -- overkill concatenationless string buffer
329 | local function tos(value: any, stack: number, str: Buffer, cyclic: Cyclic)
330 | local TAB = " "
331 | local indent = table.concat(table.create(stack, TAB))
332 |
333 | if type(value) == "string" then
334 | local n = str.n
335 | str[n + 1] = "\""
336 | str[n + 2] = value
337 | str[n + 3] = "\""
338 | str.n = n + 3
339 | elseif type(value) ~= "table" then
340 | local n = str.n
341 | str[n + 1] = value == nil and "nil" or tostring(value)
342 | str.n = n + 1
343 | elseif next(value) == nil then
344 | local n = str.n
345 | str[n + 1] = "{}"
346 | str.n = n + 1
347 | else -- is table
348 | local tabbed_indent = indent .. TAB
349 |
350 | if cyclic[value] then
351 | str.n += 1
352 | str[str.n] = color.gray(`CYCLIC REF {cyclic[value]}`)
353 | return
354 | else
355 | cyclic.n += 1
356 | cyclic[value] = cyclic.n
357 | end
358 |
359 | str.n += 3
360 | str[str.n - 2] = "{ "
361 | str[str.n - 1] = color.gray(tostring(cyclic[value]))
362 | str[str.n - 0] = "\n"
363 |
364 | local i, v = next(value, nil)
365 | while v ~= nil do
366 | local n = str.n
367 | str[n + 1] = tabbed_indent
368 |
369 | if type(i) ~= "string" then
370 | str[n + 2] = "["
371 | str[n + 3] = tostring(i)
372 | str[n + 4] = "]"
373 | n += 4
374 | else
375 | str[n + 2] = tostring(i)
376 | n += 2
377 | end
378 |
379 | str[n + 1] = " = "
380 | str.n = n + 1
381 |
382 | tos(v, stack + 1, str, cyclic)
383 |
384 | i, v = next(value, i)
385 |
386 | n = str.n
387 | str[n + 1] = v ~= nil and ",\n" or "\n"
388 | str.n = n + 1
389 | end
390 |
391 | local n = str.n
392 | str[n + 1] = indent
393 | str[n + 2] = "}"
394 | str.n = n + 2
395 | end
396 | end
397 |
398 | local str = { n = 0 }
399 | local cyclic = { n = 0 }
400 | tos(v, 0, str, cyclic)
401 | print(table.concat(str))
402 | end
403 |
404 | --------------------------------------------------------------------------------
405 | -- Equality
406 | --------------------------------------------------------------------------------
407 |
408 | local function shallow_eq(a: {}, b: {}): boolean
409 | if #a ~= #b then return false end
410 |
411 | for i, v in next, a do
412 | if b[i] ~= v then
413 | return false
414 | end
415 | end
416 |
417 | for i, v in next, b do
418 | if a[i] ~= v then
419 | return false
420 | end
421 | end
422 |
423 | return true
424 | end
425 |
426 | local function deep_eq(a: {}, b: {}): boolean
427 | if #a ~= #b then return false end
428 |
429 | for i, v in next, a do
430 | if type(b[i]) == "table" and type(v) == "table" then
431 | if deep_eq(b[i], v) == false then return false end
432 | elseif b[i] ~= v then
433 | return false
434 | end
435 | end
436 |
437 | for i, v in next, b do
438 | if type(a[i]) == "table" and type(v) == "table" then
439 | if deep_eq(a[i], v) == false then return false end
440 | elseif a[i] ~= v then
441 | return false
442 | end
443 | end
444 |
445 | return true
446 | end
447 |
448 | --------------------------------------------------------------------------------
449 | -- Return
450 | --------------------------------------------------------------------------------
451 |
452 | return {
453 | test = function()
454 | return TEST, CASE, CHECK, FINISH, SKIP
455 | end,
456 |
457 | benchmark = function()
458 | return BENCH, START
459 | end,
460 |
461 | print = print2,
462 |
463 | seq = shallow_eq,
464 | deq = deep_eq,
465 |
466 | color = color
467 | }
468 |
--------------------------------------------------------------------------------
/test/benchmarks.luau:
--------------------------------------------------------------------------------
1 | local testkit = require("./testkit")
2 | local BENCH, START = testkit.benchmark()
3 |
4 | local function TITLE(name: string)
5 | print()
6 | print(testkit.color.white(name))
7 | end
8 |
9 | local ecr = require "../src/ecr"
10 |
11 | local function rtrue() return true :: any end
12 | local A, B, C, D = ecr.component(rtrue), ecr.component(rtrue), ecr.component(rtrue), ecr.component(rtrue)
13 | local E, F, G, H = ecr.component(), ecr.component(), ecr.component(), ecr.component()
14 |
15 | local TA, TB, TC, TD = ecr.tag(), ecr.tag(), ecr.tag(), ecr.tag()
16 |
17 | local function BULK_CREATE_IDS(reg: ecr.Registry, n: number): { ecr.entity }
18 | local ids = table.create(n)
19 | for i = 1, n do
20 | ids[i] = reg:create()
21 | end
22 | return ids
23 | end
24 |
25 | local N = 2^16 - 2 -- 65,534
26 |
27 | local function REG_INIT(size: number)
28 | local reg = ecr.registry()
29 |
30 | for _, ctype in { ecr.entity, A, B, C, D, E, F, G, H, TA, TB, TC, TD } do
31 | reg:storage(ctype):reserve(size)
32 | end
33 |
34 | return reg
35 | end
36 |
37 | do TITLE "entities"
38 | BENCH("create (init)", function()
39 | local reg = REG_INIT(N/2)
40 |
41 | for i = 1, START(N) do
42 | reg:create()
43 | end
44 | end)
45 |
46 | BENCH("create", function()
47 | local reg = REG_INIT(N)
48 |
49 | for i = 1, START(N) do
50 | reg:create()
51 | end
52 | end)
53 |
54 | BENCH("release", function()
55 | local reg = REG_INIT(N)
56 |
57 | local id = table.create(N)
58 |
59 | for i = 1, N do
60 | id[i] = reg:create()
61 | end
62 |
63 | for i = 1, START(N) do
64 | reg:release(id[i])
65 | end
66 | end)
67 |
68 | BENCH("create with id (init)", function()
69 | local sreg = REG_INIT(N)
70 |
71 | local id = table.create(N)
72 |
73 | for i = 1, N do
74 | id[i] = sreg:create()
75 | end
76 |
77 | local reg = REG_INIT(N)
78 |
79 | for i = 1, START(N) do
80 | reg:create(id[i])
81 | end
82 | end)
83 |
84 | BENCH("create with id", function()
85 | local sreg = REG_INIT(N)
86 |
87 | local id = table.create(N)
88 |
89 | for i = 1, N do
90 | id[i] = sreg:create()
91 | end
92 |
93 | local reg = REG_INIT(N)
94 |
95 | START(N)
96 | reg:create(id[N])
97 | for i = 1, N - 1 do
98 | reg:create(id[i])
99 | end
100 | end)
101 | end
102 |
103 | do TITLE "add"
104 | local function setup(n: number)
105 | -- todo: why is (N + 2) / 2 + 1 so slow - something to do with array idx set to nil
106 | local reg = REG_INIT(n)
107 |
108 | local ids = table.create(N)
109 | for i = 1, N do
110 | ids[i] = reg:create()
111 | end
112 | return reg, ids
113 | end
114 |
115 | do
116 | local reg, ids = setup(N/4) -- todo: inaccurate reading
117 |
118 | BENCH("1 component (init)", function()
119 | for i = 1, START(N) do
120 | reg:add(ids[i], A)
121 | end
122 | end)
123 | end
124 |
125 | do
126 | local reg, ids = setup(N)
127 |
128 | BENCH("1 component", function()
129 | for i = 1, START(N) do
130 | reg:add(ids[i], A)
131 | end
132 | end)
133 | end
134 |
135 | do
136 | local reg, ids = setup(N)
137 |
138 | BENCH("2 components", function()
139 | for i = 1, START(N) do
140 | reg:add(ids[i], A, B)
141 | end
142 | end)
143 | end
144 |
145 | do
146 | local reg, ids = setup(N)
147 |
148 | BENCH("4 components", function()
149 | for i = 1, START(N) do
150 | reg:add(ids[i], A, B, C, D)
151 | end
152 | end)
153 | end
154 |
155 | do
156 | local reg, ids = setup(N/2)
157 |
158 | BENCH("1 tag (init)", function()
159 | for i = 1, START(N) do
160 | reg:add(ids[i], TA)
161 | end
162 | end)
163 | end
164 |
165 | do
166 | local reg, ids = setup(N)
167 |
168 | -- todo: despite causing a reallocation why is this not recorded by gcinfo()?
169 | BENCH("1 tag", function()
170 | for i = 1, START(N) do
171 | reg:add(ids[i], TA)
172 | end
173 | end)
174 | end
175 |
176 | do
177 | local reg, ids = setup(N)
178 |
179 | BENCH("4 tags", function()
180 | for i = 1, START(N) do
181 | reg:add(ids[i], TA, TB, TC, TD)
182 | end
183 | end)
184 | end
185 | end
186 |
187 | do TITLE "set"
188 | local function setup()
189 | local reg = REG_INIT(N)
190 | local ids = table.create(N)
191 | for i = 1, N do
192 | ids[i] = reg:create()
193 | end
194 | return reg, ids
195 | end
196 |
197 | do
198 | local reg, ids = setup()
199 |
200 | BENCH("add 1", function()
201 | for i = 1, START(N) do
202 | reg:set(ids[i], A, true)
203 | end
204 | end)
205 |
206 | BENCH("change 1", function()
207 | for i = 1, START(N) do
208 | reg:set(ids[i], A, true)
209 | end
210 | end)
211 | end
212 |
213 | do
214 | local reg, ids = setup()
215 |
216 | BENCH("add 2", function()
217 | for i = 1, START(N) do
218 | local e = ids[i]
219 | reg:set(e, A, true)
220 | reg:set(e, B, true)
221 | end
222 | end)
223 |
224 | BENCH("change 2", function()
225 | for i = 1, START(N) do
226 | local e = ids[i]
227 | reg:set(e, A, true)
228 | reg:set(e, B, true)
229 | end
230 | end)
231 | end
232 |
233 | do
234 | local reg, ids = setup()
235 | BENCH("add 4", function()
236 | for i = 1, START(N) do
237 | local e = ids[i]
238 | reg:set(e, A, true)
239 | reg:set(e, B, true)
240 | reg:set(e, C, true)
241 | reg:set(e, D, true)
242 | end
243 | end)
244 |
245 | BENCH("change 4", function()
246 | for i = 1, START(N) do
247 | local e = ids[i]
248 | reg:set(e, A, true)
249 | reg:set(e, B, true)
250 | reg:set(e, C, true)
251 | reg:set(e, D, true)
252 | end
253 | end)
254 | end
255 |
256 | do
257 | local reg, ids = setup()
258 | BENCH("add tag", function()
259 | for i = 1, START(N) do
260 | reg:set(ids[i], TA)
261 | end
262 | end)
263 | end
264 | end
265 |
266 | do TITLE "insert"
267 |
268 | BENCH("insert new array", function()
269 | local reg = REG_INIT(N)
270 | local ids = table.create(N)
271 |
272 | for i = 1, N do
273 | local e = reg:create()
274 | ids[i] = e
275 | end
276 |
277 | for i = 1, START(N) do
278 | reg:insert(ids[i], A, true)
279 | end
280 | end)
281 |
282 | BENCH("insert new value", function()
283 | local reg = REG_INIT(N)
284 | local ids = table.create(N)
285 |
286 | for i = 1, N do
287 | local e = reg:create()
288 | reg:insert(e, A, true)
289 | ids[i] = e
290 | end
291 |
292 | for i = 1, START(N) do
293 | reg:insert(ids[i], A, true)
294 | end
295 | end)
296 |
297 | BENCH("insert new value to same entity", function()
298 | local reg = REG_INIT(N)
299 | local id = reg:create()
300 |
301 | for i = 1, START(N) do
302 | reg:insert(id, A, true)
303 | end
304 | end)
305 |
306 | end
307 |
308 | do TITLE "patch"
309 | local function setup()
310 | local reg = REG_INIT(N)
311 | local ids = table.create(N)
312 | for i = 1, N do
313 | local e = reg:create()
314 | ids[i] = e
315 | reg:set(e, A, 0)
316 | reg:set(e, B, 0)
317 | reg:set(e, C, 0)
318 | reg:set(e, D, 0)
319 | end
320 | return reg, ids
321 | end
322 |
323 | local function patcher(v: number)
324 | return v + 1
325 | end
326 |
327 | do
328 | local reg, ids = setup()
329 |
330 | BENCH("patch 1", function()
331 | for i = 1, START(N) do
332 | local e = ids[i]
333 | reg:patch(e, A, patcher)
334 | end
335 | end)
336 | end
337 |
338 | do
339 | local reg, ids = setup()
340 |
341 | BENCH("patch 2", function()
342 | for i = 1, START(N) do
343 | local e = ids[i]
344 | reg:patch(e, A, patcher)
345 | reg:patch(e, B, patcher)
346 | end
347 | end)
348 | end
349 |
350 | do
351 | local reg, ids = setup()
352 |
353 | BENCH("patch 4", function()
354 | for i = 1, START(N) do
355 | local e = ids[i]
356 | reg:patch(e, A, patcher)
357 | reg:patch(e, B, patcher)
358 | reg:patch(e, C, patcher)
359 | reg:patch(e, D, patcher)
360 | end
361 | end)
362 | end
363 | end
364 |
365 | do TITLE "has"
366 | local reg = REG_INIT(N)
367 | local ids = table.create(N)
368 |
369 | for i = 1, N do
370 | ids[i] = reg:create()
371 | reg:set(ids[i], A, true)
372 | reg:set(ids[i], B, true)
373 | reg:set(ids[i], C, true)
374 | reg:set(ids[i], D, true)
375 | end
376 |
377 | BENCH("1 component", function()
378 | for i = 1, START(N) do
379 | reg:has(ids[i], A)
380 | end
381 | end)
382 |
383 | BENCH("2 components", function()
384 | for i = 1, START(N) do
385 | local e = ids[i]
386 | reg:has(e, A, B)
387 | end
388 | end)
389 |
390 | BENCH("4 components", function()
391 | for i = 1, START(N) do
392 | local e = ids[i]
393 | reg:has(e, A, B, C, D)
394 | end
395 | end)
396 | end
397 |
398 | do TITLE("get")
399 | local reg = REG_INIT(N)
400 | local ids = table.create(N)
401 |
402 | for i = 1, N do
403 | local e = reg:create()
404 | ids[i] = e
405 | reg:set(e, A, true)
406 | reg:set(e, B, true)
407 | reg:set(e, C, true)
408 | reg:set(e, D, true)
409 |
410 | reg:set(e, E, true)
411 | reg:set(e, F, true)
412 | reg:set(e, G, true)
413 | reg:set(e, H, true)
414 | end
415 |
416 | BENCH("1 component", function()
417 | for i = 1, START(N) do
418 | reg:get(ids[i], A)
419 | end
420 | end)
421 |
422 | BENCH("2 components", function()
423 | for i = 1, START(N) do
424 | local e = ids[i]
425 | reg:get(e, A, B)
426 | end
427 | end)
428 |
429 | BENCH("4 components", function()
430 | for i = 1, START(N) do
431 | local e = ids[i]
432 | reg:get(e, A, B, C, D)
433 | end
434 | end)
435 |
436 | BENCH("try 1 component", function()
437 | for i = 1, START(N) do
438 | reg:try_get(ids[i], A)
439 | end
440 | end)
441 | end
442 |
443 | do TITLE("remove")
444 | local function setup()
445 | local reg = REG_INIT(N)
446 | local ids = table.create(N)
447 | for i = 1, N do
448 | local e = reg:create()
449 | ids[i] = e
450 | reg:set(e, A, true)
451 | reg:set(e, B, true)
452 | reg:set(e, C, true)
453 | reg:set(e, D, true)
454 | reg:set(e, E, true)
455 | reg:set(e, F, true)
456 | reg:set(e, G, true)
457 | reg:set(e, H, true)
458 | end
459 | return reg, ids
460 | end
461 |
462 | BENCH("1 unowned", function()
463 | local reg, ids = setup()
464 |
465 | for i = 1, N do
466 | reg:remove(ids[i], A)
467 | end
468 |
469 | for i = 1, START(N) do
470 | reg:remove(ids[i], A)
471 | end
472 | end)
473 |
474 | BENCH("1 component", function()
475 | local reg, ids = setup()
476 |
477 | for i = 1, START(N) do
478 | reg:remove(ids[i], A)
479 | end
480 | end)
481 |
482 | BENCH("2 components", function()
483 | local reg, ids = setup()
484 |
485 | for i = 1, START(N) do
486 | local e = ids[i]
487 | reg:remove(e, A, B)
488 | end
489 | end)
490 |
491 | BENCH("4 components", function()
492 | local reg, ids = setup()
493 |
494 | for i = 1, START(N) do
495 | local e = ids[i]
496 | reg:remove(e, A, B, C, D)
497 | end
498 | end)
499 |
500 | BENCH("8 components", function()
501 | local reg, ids = setup()
502 |
503 | for i = 1, START(N) do
504 | local e = ids[i]
505 | reg:remove(e, A, B, C, D, E, F, G, H)
506 | end
507 | end)
508 | end
509 |
510 | do TITLE "find"
511 | local reg = REG_INIT(N)
512 | for i = 1, N do
513 | local id = reg:create()
514 | reg:set(id, A, i)
515 | end
516 |
517 | BENCH("search through 1", function()
518 | for i = 1, START(N) do
519 | reg:find(A, 1)
520 | end
521 | end)
522 |
523 | BENCH("search through 10", function()
524 | for i = 1, START(N) do
525 | reg:find(A, 10)
526 | end
527 | end)
528 |
529 | assert(N > 1000)
530 |
531 | BENCH("search through 1000", function()
532 | for i = 1, START(100) do
533 | reg:find(A, 1000)
534 | end
535 | end)
536 | end
537 |
538 | do TITLE "clear"
539 | local function setup()
540 | local reg = REG_INIT(N)
541 | local ids = table.create(N)
542 | for i = 1, N do
543 | local e = reg:create()
544 | ids[i] = e
545 | reg:set(e, A, true)
546 | reg:set(e, B, true)
547 | reg:set(e, C, true)
548 | reg:set(e, D, true)
549 | reg:set(e, E, true)
550 | reg:set(e, F, true)
551 | reg:set(e, G, true)
552 | reg:set(e, H, true)
553 | end
554 | return reg, ids
555 | end
556 |
557 | BENCH("1 component", function()
558 | local reg = setup()
559 |
560 | START(N)
561 | reg:clear(A)
562 | end)
563 |
564 | BENCH("2 components", function()
565 | local reg = setup()
566 |
567 | START(N)
568 | reg:clear(A, B)
569 | end)
570 |
571 | BENCH("4 components", function()
572 | local reg = setup()
573 |
574 | START(N)
575 | reg:clear(A, B, C, D)
576 | end)
577 |
578 | BENCH("8 components", function()
579 | local reg = setup()
580 |
581 | START(N)
582 | reg:clear(A, B, C, D, E, F, G, H)
583 | end)
584 |
585 | BENCH("all entities (no components)", function()
586 | local reg = REG_INIT(N)
587 | for i = 1, N do reg:create() end
588 |
589 | START(N)
590 | reg:clear()
591 | end)
592 |
593 | BENCH("1 component (grouped)", function()
594 | local reg = setup()
595 | reg:group(A, B)
596 |
597 | START(N)
598 | reg:clear(A)
599 | end)
600 | end
601 |
602 | do TITLE("create view")
603 | local reg = REG_INIT(N)
604 |
605 | reg:view(A, B, C, D, E, F, G, H) -- register components
606 |
607 | BENCH("1 component", function()
608 | for i = 1, START(N) do
609 | reg:view(A)
610 | end
611 | end)
612 |
613 | BENCH("4 component", function()
614 | for i = 1, START(N) do
615 | reg:view(A, B, C, D)
616 | end
617 | end)
618 |
619 | BENCH("1 component and iterate", function()
620 | for i = 1, START(N) do
621 | for _ in reg:view(A) do end
622 | end
623 | end)
624 |
625 | BENCH("4 component and iterate", function()
626 | for i = 1, START(N) do
627 | for _ in reg:view(A, B, C, D) do end
628 | end
629 | end)
630 | end
631 |
632 | do
633 | local function view_bench(reg: ecr.Registry)
634 | local function exact_size(view: ecr.View<...any>): number
635 | local i = 0
636 | for _ in view do
637 | i += 1
638 | end
639 | return i
640 | end
641 |
642 | BENCH("1 component", function()
643 | local view = reg:view(A)
644 |
645 | START(exact_size(view))
646 | for entity, a in view do end
647 | end)
648 |
649 | BENCH("2 components", function()
650 | local view = reg:view(A, B)
651 |
652 | START(exact_size(view))
653 | for entity, a, b in view do end
654 | end)
655 |
656 | BENCH("3 components", function()
657 | local view = reg:view(A, B, C)
658 |
659 | START(exact_size(view))
660 | for entity, a, b, c in view do end
661 | end)
662 |
663 | BENCH("4 components", function()
664 | local view = reg:view(A, B, C, D)
665 |
666 | START(exact_size(view))
667 | for entity, a, b, c, d in view do end
668 | end)
669 |
670 | BENCH("8 components", function()
671 | local view = reg:view(A, B, C, D, E, F, G, H)
672 |
673 | START(exact_size(view))
674 | for entity, a, b, c, d, e, f, g, h in view do end
675 | end)
676 | end
677 |
678 | do TITLE("iter view (ordered)")
679 | local reg = REG_INIT(N)
680 |
681 | for i = 1, N do
682 | local entity = reg:create()
683 | reg:set(entity, A, true)
684 | reg:set(entity, B, true)
685 | reg:set(entity, C, true)
686 | reg:set(entity, D, true)
687 | reg:set(entity, E, true)
688 | reg:set(entity, F, true)
689 | reg:set(entity, G, true)
690 | reg:set(entity, H, true)
691 | end
692 |
693 | view_bench(reg)
694 | end
695 |
696 | do TITLE("iter view (random)")
697 | local reg = REG_INIT(N)
698 |
699 | local function flip() return math.random() > 0.5 end
700 |
701 | for i = 1, N do
702 | local entity = reg:create()
703 | if flip() then reg:set(entity, A, true) end
704 | if flip() then reg:set(entity, B, true) end
705 | if flip() then reg:set(entity, C, true) end
706 | if flip() then reg:set(entity, D, true) end
707 | if flip() then reg:set(entity, E, true) end
708 | if flip() then reg:set(entity, F, true) end
709 | if flip() then reg:set(entity, G, true) end
710 | if flip() then reg:set(entity, H, true) end
711 | end
712 |
713 | view_bench(reg)
714 | end
715 |
716 | do TITLE("iter view (random + common)")
717 | -- 7 components are randomly assigned to entities,
718 | -- any entities that receive all 7 will be tagged with an 8th component.
719 | -- This guarantees that when iterating over this 8th component, all
720 | -- entities iterated will also contain the other 7.
721 |
722 | local reg = REG_INIT(N)
723 |
724 | local function flip() return math.random() > 0.5 end
725 |
726 | for i = 1, N do
727 | local entity = reg:create()
728 | local b, c, d, e, f, g, h
729 | if flip() then b=true; reg:set(entity, B, true) end
730 | if flip() then c=true; reg:set(entity, C, true) end
731 | if flip() then d=true; reg:set(entity, D, true) end
732 | if flip() then e=true; reg:set(entity, E, true) end
733 | if flip() then f=true; reg:set(entity, F, true) end
734 | if flip() then g=true; reg:set(entity, G, true) end
735 | if flip() then h=true; reg:set(entity, H, true) end
736 | if b and c and d and e and f and g and h then reg:set(entity, A, true) end
737 | end
738 |
739 | view_bench(reg)
740 | end
741 |
742 | do TITLE("iter view (tags)")
743 | local reg = REG_INIT(N)
744 |
745 | for i = 1, N do
746 | local e = reg:handle()
747 | e:add(TA, TB, TC, TD)
748 | end
749 |
750 | BENCH("1 tag", function()
751 | START(N)
752 | for id, a in reg:view(TA) do end
753 | end)
754 |
755 | BENCH("4 tags", function()
756 | START(N)
757 | for id, a, b, c, d in reg:view(TA, TB, TC, TD) do end
758 | end)
759 | end
760 |
761 | do TITLE("patch view (ordered)")
762 | local reg = REG_INIT(N)
763 |
764 | for i = 1, N do
765 | local entity = reg:create()
766 | reg:set(entity, A, true)
767 | reg:set(entity, B, true)
768 | reg:set(entity, C, true)
769 | reg:set(entity, D, true)
770 | end
771 |
772 | BENCH("1 component", function()
773 | START(N)
774 | reg:view(A):patch(function(a) return a end)
775 | end)
776 |
777 | BENCH("2 components", function()
778 | START(N)
779 | reg:view(A, B):patch(function(a, b) return a, b end)
780 | end)
781 |
782 | BENCH("4 components", function()
783 | START(N)
784 | reg:view(A, B, C, D):patch(function(a, b, c, d) return a, b, c, d end)
785 | end)
786 | end
787 | end
788 |
789 | do TITLE("handle")
790 | local N2 = N/32
791 |
792 | local reg = REG_INIT(N)
793 | local ids = table.create(N2)
794 |
795 | for i = 1, N2 do
796 | ids[i] = reg:create()
797 | end
798 |
799 | BENCH("create new", function()
800 | for i = 1, START(N2) do
801 | reg:handle(ids[i])
802 | end
803 | end)
804 |
805 | -- cache to prevent gc when starting next benchmark
806 | local cache = table.create(N2)
807 | for i = 1, N2 do
808 | cache[i] = reg:handle(ids[i])
809 | end
810 |
811 | BENCH("create cached", function()
812 | for i = 1, START(N2) do
813 | reg:handle(ids[i])
814 | end
815 | end)
816 | end
817 |
818 | do TITLE "group"
819 | do
820 | local reg = REG_INIT(N)
821 |
822 | local group = reg:group(A, B)
823 |
824 | local ids = {}
825 |
826 | for i = 1, N do
827 | ids[i] = reg:create()
828 | end
829 |
830 | BENCH("add 2 components", function()
831 | for i = 1, START(N) do
832 | local e = ids[i]
833 | reg:set(e, A, 1)
834 | reg:set(e, B, 2)
835 | end
836 | end)
837 |
838 | BENCH("iter 2 components", function()
839 | START(N)
840 | for entity, a, b in group do end
841 | end)
842 |
843 | BENCH("remove 2 components", function()
844 | for i = 1, START(N) do
845 | local e = ids[i]
846 | reg:remove(e, A, B)
847 | end
848 | end)
849 |
850 | BENCH("add 1 component", function()
851 | for i = 1, START(N) do
852 | local e = ids[i]
853 | reg:set(e, A, 1)
854 | end
855 | end)
856 | end
857 |
858 | do
859 | local reg = REG_INIT(N)
860 |
861 | local group2 = reg:group(C, D, E, F)
862 |
863 | local ids = {}
864 |
865 | for i = 1, N do
866 | ids[i] = reg:create()
867 | end
868 |
869 | BENCH("add 4 components", function()
870 | for i = 1, START(N) do
871 | local e = ids[i]
872 | reg:set(e, C, 1)
873 | reg:set(e, D, 2)
874 | reg:set(e, E, 3)
875 | reg:set(e, F, 4)
876 | end
877 | end)
878 |
879 | BENCH("iter 4 components", function()
880 | START(N)
881 | for entity, a, b, c, d in group2 do end
882 | end)
883 |
884 | BENCH("remove 4 components", function()
885 | for i = 1, START(N) do
886 | local e = ids[i]
887 | reg:remove(e, C, D, E, F)
888 | end
889 | end)
890 |
891 | BENCH("add 1 component", function()
892 | for i = 1, START(N) do
893 | local e = ids[i]
894 | reg:set(e, C, 1)
895 | end
896 | end)
897 | end
898 | end
899 |
900 | do TITLE("destroy")
901 |
902 | BENCH("0 components", function()
903 | local reg = ecr.registry()
904 |
905 | for p in reg:storage() do
906 | print"p"
907 | end
908 |
909 | local ids = table.create(N)
910 |
911 | for i = 1, N do
912 | ids[i] = reg:create()
913 | end
914 |
915 | for i = 1, START(N) do
916 | reg:destroy(ids[i])
917 | end
918 | end)
919 |
920 | BENCH("1 component", function()
921 | local reg = ecr.registry()
922 | local ids = table.create(N)
923 |
924 | for i = 1, N do
925 | local e = reg:create()
926 | reg:set(e, A, true)
927 | ids[i] = e
928 | end
929 |
930 | for i = 1, START(N) do
931 | reg:destroy(ids[i])
932 | end
933 | end)
934 |
935 | BENCH("2 components", function()
936 | local reg = ecr.registry()
937 | local ids = table.create(N)
938 |
939 | for i = 1, N do
940 | local e = reg:create()
941 | reg:set(e, A, true)
942 | reg:set(e, B, true)
943 | ids[i] = e
944 | end
945 |
946 | for i = 1, START(N) do
947 | reg:destroy(ids[i])
948 | end
949 | end)
950 |
951 | BENCH("4 components", function()
952 | local reg = ecr.registry()
953 | local ids = table.create(N)
954 |
955 | for i = 1, N do
956 | local e = reg:create()
957 | reg:set(e, A, true)
958 | reg:set(e, B, true)
959 | reg:set(e, C, true)
960 | reg:set(e, D, true)
961 | ids[i] = e
962 | end
963 |
964 | for i = 1, START(N) do
965 | reg:destroy(ids[i])
966 | end
967 | end)
968 |
969 | BENCH("8 components", function()
970 | local reg = ecr.registry()
971 | local ids = table.create(N)
972 |
973 | for i = 1, N do
974 | local e = reg:create()
975 | reg:set(e, A, true)
976 | reg:set(e, B, true)
977 | reg:set(e, C, true)
978 | reg:set(e, D, true)
979 | reg:set(e, E, true)
980 | reg:set(e, F, true)
981 | reg:set(e, G, true)
982 | reg:set(e, H, true)
983 | ids[i] = e
984 | end
985 |
986 | for i = 1, START(N) do
987 | reg:destroy(ids[i])
988 | end
989 | end)
990 |
991 | BENCH("0 components (8 registered)", function()
992 | local reg = ecr.registry()
993 | local ids = table.create(N)
994 |
995 | for i = 1, N - 1 do
996 | local e = reg:create()
997 | ids[i] = e
998 | end
999 |
1000 | do -- register all components
1001 | local e = reg:create()
1002 | reg:set(e, A, true)
1003 | reg:set(e, B, true)
1004 | reg:set(e, C, true)
1005 | reg:set(e, D, true)
1006 | reg:set(e, E, true)
1007 | reg:set(e, F, true)
1008 | reg:set(e, G, true)
1009 | reg:set(e, H, true)
1010 | end
1011 |
1012 | for i = 1, START(N - 1) do
1013 | reg:destroy(ids[i])
1014 | end
1015 | end)
1016 |
1017 | BENCH("0 components (128 registered)", function()
1018 | local ctypes = {}
1019 | for i = 1, 128 do
1020 | ctypes[i] = ecr.component()
1021 | end
1022 |
1023 | local reg = ecr.registry()
1024 |
1025 | do
1026 | local id = reg:create()
1027 | for i = 1, 128 do
1028 | reg:set(id, ctypes[i], true)
1029 | end
1030 | end
1031 |
1032 | local ids = table.create(N - 1)
1033 |
1034 | for i = 1, N - 1 do
1035 | ids[i] = reg:create()
1036 | end
1037 |
1038 | for i = 1, START(N - 1) do
1039 | reg:destroy(ids[i])
1040 | end
1041 | end)
1042 |
1043 | BENCH("0 components (1024 registered)", function()
1044 | local ctypes = {}
1045 | for i = 1, 1024 do
1046 | ctypes[i] = ecr.component()
1047 | end
1048 |
1049 | local reg = ecr.registry()
1050 |
1051 | do
1052 | local id = reg:create()
1053 | for i = 1, 1024 do
1054 | reg:set(id, ctypes[i], true)
1055 | end
1056 | end
1057 |
1058 | local ids = table.create(N)
1059 |
1060 | for i = 1, N/10 do
1061 | ids[i] = reg:create()
1062 | end
1063 |
1064 | for i = 1, START(N/10) do
1065 | reg:destroy(ids[i])
1066 | end
1067 | end)
1068 |
1069 | BENCH("32 components (1024 registered)", function()
1070 | local ctypes = {}
1071 | for i = 1, 1024 do
1072 | ctypes[i] = ecr.component()
1073 | end
1074 |
1075 | local reg = ecr.registry()
1076 |
1077 | do
1078 | local id = reg:create()
1079 | for i = 1, 1024 do
1080 | reg:set(id, ctypes[i], true)
1081 | end
1082 | end
1083 |
1084 | local ids = table.create(N)
1085 |
1086 | for i = 1, N/10 do
1087 | ids[i] = reg:create()
1088 | for j = 1, 32 do
1089 | reg:set(ids[i], ctypes[j], true)
1090 | end
1091 | end
1092 |
1093 | for i = 1, START(N/10) do
1094 | reg:destroy(ids[i])
1095 | end
1096 | end)
1097 | end
1098 |
1099 | do TITLE("contains")
1100 | BENCH("", function()
1101 | local reg = REG_INIT(N)
1102 | local ids = table.create(N)
1103 | for i = 1, N do
1104 | ids[i] = reg:create()
1105 | end
1106 |
1107 | START(N)
1108 |
1109 | for i = 1, N do
1110 | reg:contains(ids[i])
1111 | end
1112 | end)
1113 | end
1114 |
1115 | do TITLE("copy")
1116 | local reg = REG_INIT(N)
1117 | local ids = table.create(N)
1118 |
1119 | for i = 1, N do
1120 | ids[i] = reg:create()
1121 | end
1122 |
1123 | BENCH("values no pre-alloc", function()
1124 | for i = 1, N do
1125 | reg:set(ids[i], A, "1")
1126 | end
1127 |
1128 | START(N)
1129 |
1130 | reg:copy(A, B)
1131 | end)
1132 |
1133 | BENCH("values pre-alloc", function()
1134 | for i = 1, N do
1135 | reg:set(ids[i], A, "2")
1136 | end
1137 |
1138 | START(N)
1139 |
1140 | reg:copy(A, B)
1141 | end)
1142 |
1143 | BENCH("tag no pre-alloc", function()
1144 | for i = 1, N do
1145 | reg:set(ids[i], TA)
1146 | end
1147 |
1148 | START(N)
1149 |
1150 | reg:copy(TA, TB)
1151 | end)
1152 |
1153 | BENCH("tag pre-alloc", function()
1154 | START(N)
1155 |
1156 | reg:copy(TA, TB)
1157 | end)
1158 | end
1159 |
1160 | do TITLE "signal"
1161 | local reg = REG_INIT(N)
1162 | local ids = table.create(N)
1163 |
1164 | for i = 1, N do
1165 | ids[i] = reg:create()
1166 | end
1167 |
1168 | BENCH("added", function()
1169 | reg:on_add(A):connect(function() end)
1170 |
1171 | for i = 1, START(N) do
1172 | reg:set(ids[i], A, true)
1173 | end
1174 | end)
1175 |
1176 | BENCH("changed", function()
1177 | reg:on_change(A):connect(function() end)
1178 |
1179 | for i = 1, START(N) do
1180 | reg:set(ids[i], A, false)
1181 | end
1182 | end)
1183 |
1184 | BENCH("removed", function()
1185 | reg:on_remove(A):connect(function() end)
1186 |
1187 | for i = 1, START(N) do
1188 | reg:remove(ids[i], A)
1189 | end
1190 | end)
1191 | end
1192 |
1193 | do TITLE "observer"
1194 | local reg = REG_INIT(N)
1195 | local observer = reg:track(A)
1196 |
1197 | local ids = table.create(N)
1198 | for i = 1, N do
1199 | ids[i] = reg:create()
1200 | end
1201 |
1202 | (observer :: any).pool:reserve(N/2)
1203 |
1204 | BENCH("add (init)", function()
1205 | for i = 1, START(N) do
1206 | reg:set(ids[i], A, true)
1207 | end
1208 | end)
1209 |
1210 | reg:clear(A)
1211 | assert(#observer == 0)
1212 |
1213 | BENCH("add", function()
1214 | for i = 1, START(N) do
1215 | reg:set(ids[i], A, true)
1216 | end
1217 | end)
1218 |
1219 | BENCH("change", function()
1220 | for i = 1, START(N) do
1221 | reg:set(ids[i], A, false)
1222 | end
1223 | end)
1224 |
1225 | BENCH("iterate", function()
1226 | START(N)
1227 | for id, v in observer do end
1228 | end)
1229 |
1230 | BENCH("remove", function()
1231 | for i = 1, START(N) do
1232 | reg:remove(ids[i], A)
1233 | end
1234 | end)
1235 |
1236 | BENCH("clear", function()
1237 | START(N)
1238 | observer:clear()
1239 | end)
1240 | end
1241 |
1242 | do TITLE "queue"
1243 | do
1244 | local q = ecr.queue()
1245 |
1246 | BENCH("add 1", function()
1247 | for i = 1, START(N) do
1248 | q:add(i)
1249 | end
1250 | end)
1251 |
1252 | BENCH("iter 1", function()
1253 | START(N)
1254 | for i in q do end
1255 | end)
1256 | end
1257 |
1258 | do
1259 | local q = ecr.queue()
1260 |
1261 | BENCH("add 4", function()
1262 | for i = 1, START(N) do
1263 | q:add(i, i, i, i)
1264 | end
1265 | end)
1266 |
1267 | BENCH("iter 4", function()
1268 | START(N)
1269 | for a, b, c, d in q do end
1270 | end)
1271 | end
1272 | end
1273 |
1274 | do TITLE "buffer"
1275 | local reg = ecr.registry()
1276 |
1277 | local ids = BULK_CREATE_IDS(reg, N)
1278 | local buf = buffer.create(N * ecr.id_size)
1279 |
1280 | BENCH("array to buffer", function()
1281 | ecr.array_to_buffer(ids, START(N), buf)
1282 | end)
1283 |
1284 | local arr = table.create(N)
1285 |
1286 | BENCH("buffer to array", function()
1287 | ecr.buffer_to_array(buf, START(N), arr)
1288 | end)
1289 |
1290 | local pool = reg:storage(ecr.entity)
1291 |
1292 | BENCH("buffer to buffer", function()
1293 | ecr.buffer_to_buffer(pool.entities, START(N), buf)
1294 | end)
1295 | end
1296 |
1297 | local PN = 2^11 -- 2048
1298 |
1299 | do TITLE(`practical test ECS ({PN} entities)`)
1300 | local Position = ecr.component() :: number
1301 | local Velocity = ecr.component() :: number
1302 | local Health = ecr.component() :: number
1303 | local Dead = ecr.tag()
1304 |
1305 | local world = ecr.registry()
1306 |
1307 | world:group(Position, Velocity)
1308 |
1309 | local function init()
1310 | for i = 1, PN do
1311 | local id = world:create()
1312 | world:set(id, Position, i)
1313 | world:set(id, Velocity, i)
1314 | world:set(id, Health, 0)
1315 |
1316 | end
1317 | end
1318 |
1319 | do -- pre-alloc
1320 | init()
1321 |
1322 | for id in world:view(ecr.entity) do
1323 | world:add(id, Dead)
1324 | end
1325 |
1326 | world:clear()
1327 | end
1328 |
1329 | BENCH("create entities with 3 components", function()
1330 | init()
1331 | end)
1332 |
1333 | BENCH("update positions", function()
1334 | for id, pos, vel in world:group(Position, Velocity) do
1335 | world:set(id, Position, pos + vel*1/60)
1336 | end
1337 | end)
1338 |
1339 | BENCH("update positions (direct)", function()
1340 | local n = #world:group(Position, Velocity)
1341 | local positions = world:storage(Position).values
1342 | local velocities = world:storage(Velocity).values
1343 |
1344 | for i = 1, n do
1345 | positions[i] += velocities[i] * 1/60
1346 | end
1347 |
1348 | end)
1349 |
1350 | BENCH("add tags", function()
1351 | for id, health in world:view(Health) do
1352 | if health <= 0 then
1353 | world:set(id, Dead)
1354 | end
1355 | end
1356 | end)
1357 |
1358 | BENCH("destroy", function()
1359 | for id in world:view(Dead) do
1360 | world:destroy(id)
1361 | end
1362 | end)
1363 | end
1364 |
1365 | -- attempt to repeat the same test above but OOP equivalent
1366 | do TITLE(`practical test OOP ({PN} entities)`)
1367 | local function gc()
1368 | (collectgarbage :: any)("collect")
1369 | end
1370 |
1371 |
1372 | type Obj = {
1373 | Position: number,
1374 | Velocity: number,
1375 | Health: number,
1376 | Dead: boolean?
1377 | }
1378 |
1379 | local world = {} :: { Obj }
1380 |
1381 | --[[
1382 |
1383 | One advantage of ECS is the dependence on interfaces and not complete object
1384 | structure.
1385 |
1386 | Luau has many optimizations for accessing a table index of known shape, so
1387 | we use multiple table shapes with a common interface to try simulate what
1388 | we would do in ECS. This should degrade OOP performance as the table index
1389 | predicter will not be correct every time as table shapes differ.
1390 |
1391 | Note: In OOP, composition is usually done instead of below, but that needs
1392 | manual separation of data and behavior by the programmer, which needs more
1393 | work than what ECS can do.
1394 |
1395 | ]]
1396 |
1397 | local ctors = {
1398 | function(i: number): Obj
1399 | return {
1400 | Position = i, Velocity = i, Health = 0,
1401 | }
1402 | end,
1403 | function(i: number): Obj
1404 | return {
1405 | Position = i, Velocity = i, Health = 0,
1406 | A = 0, B = 0, C = 0, D = 0 -- dummy fields
1407 | }
1408 | end,
1409 | function(i: number): Obj
1410 | return {
1411 | Position = i, Velocity = i, Health = 0,
1412 | A = 0, B = 0 -- dummy fields
1413 | }
1414 | end,
1415 | function(i: number): Obj
1416 | return {
1417 | Position = i, Velocity = i, Health = 0,
1418 | A = 0, B = 0, C = 0, D = 0, E = 0, F = 0, G = 0 -- dummy fields
1419 | }
1420 | end,
1421 | }
1422 |
1423 | local ctor_n = #ctors
1424 |
1425 | local function create(i): Obj
1426 | return ctors[math.random(1, ctor_n)](i)
1427 | end
1428 |
1429 | --[[
1430 |
1431 | We garbage collect random tables to make the heap more fragmented, making it
1432 | more difficult to allocate new table objects, as would happen in a practical
1433 | scenario when as a game runs for a long period of time.
1434 |
1435 | ]]
1436 |
1437 | local junk = {}
1438 |
1439 | do
1440 |
1441 | for i = 1, PN*4 do
1442 | junk[i] = create(i)
1443 | end
1444 |
1445 | for i = 1, PN*4 do
1446 | if math.random() > 0.6 then
1447 | junk[i] = nil
1448 | end
1449 | end
1450 |
1451 | gc()
1452 | end
1453 |
1454 | --[[
1455 |
1456 | Luau allocates same-shape tables in linear pages which will be more cache
1457 | friendly. However, in a practical game program, tables will be constantly
1458 | garbage collected and recreated, which will cause a mismatch and randomness
1459 | between an array of tables vs how those tables are allocated on the heap.
1460 | We try artifically cause this randomness by allocating in a random pattern.
1461 |
1462 | ]]
1463 |
1464 | local permutation = {}
1465 |
1466 | do
1467 | for i = 1, PN do
1468 | permutation[i] = i
1469 | end
1470 |
1471 | for _ = 1, PN do
1472 | local a, b = math.random(1, PN), math.random(1, PN)
1473 | permutation[a], permutation[b] = permutation[b], permutation[a]
1474 | end
1475 | end
1476 |
1477 | BENCH("create entities with 3 components", function()
1478 | for i = 1, PN do
1479 | local idx = permutation[i]
1480 | world[idx] = create(i)
1481 | end
1482 | end)
1483 |
1484 | BENCH("update positions", function()
1485 | for _, obj in world do
1486 | obj.Position += obj.Velocity * 1/60
1487 | end
1488 | end)
1489 |
1490 | BENCH("add tags", function()
1491 | for _, obj in world do
1492 | if obj.Health <= 0 then
1493 | obj.Dead = true
1494 | end
1495 | end
1496 | end)
1497 |
1498 | BENCH("destroy", function()
1499 | --table.clear(world) -- this hangs the program for some reason
1500 | -- forcing a full gc cycle isn't fair \o/
1501 | gc()
1502 | end)
1503 | end
1504 |
--------------------------------------------------------------------------------
/test/tests.luau:
--------------------------------------------------------------------------------
1 | local testkit = require("./testkit")
2 |
3 | local TEST, CASE, CHECK, FINISH = testkit.test()
4 |
5 | local function CHECK_ERR(s: string, fn: (T...) -> (), ...: T...)
6 | local ok, err: string? = pcall(fn, ...)
7 | if CHECK(not ok, 2) then
8 | local i = string.find(err :: string, " ")
9 | assert(i)
10 | local msg = string.sub(err :: string, i+1)
11 | CHECK(msg == s, 2)
12 | end
13 | end
14 |
15 | type Map = { [T]: U }
16 | type Array = { T }
17 |
18 | local ecr = require "../src/ecr"
19 |
20 | local A, B, C, D = ecr.component(), ecr.component(), ecr.component(), ecr.component()
21 | local E, F, G, H = ecr.component(), ecr.component(), ecr.component(), ecr.component(function() return true end)
22 |
23 | local TA, TB, TC, TD = ecr.tag(), ecr.tag(), ecr.tag(), ecr.tag()
24 |
25 | local MAX_VER = ecr._test.max_ver
26 |
27 | local function KEY(id: ecr.entity): number
28 | local key = ecr.inspect(id)
29 | return key
30 | end
31 |
32 | local function VER(id: ecr.entity): number
33 | local _, ver = ecr.inspect(id)
34 | return ver
35 | end
36 |
37 | local function GET_KEY_VERSION(reg: ecr.Registry, key: number): number
38 | return ecr._test.get_key_version(reg, key)
39 | end
40 |
41 | local function CREATE_ID(key: number, ver: number)
42 | return ecr._test.create_id(key, ver)
43 | end
44 |
45 | local function SET_KEY_VERSION_AND_ENSURE_EXISTS(reg: ecr.Registry, key: number, ver: number)
46 | reg:release(reg:create(CREATE_ID(key, 1)))
47 | ecr._test.set_key_version(reg, key, ver)
48 | end
49 |
50 | local function SET_KEY_VERSION(reg: ecr.Registry, key: number, ver: number)
51 | ecr._test.set_key_version(reg, key, ver)
52 | end
53 |
54 | local function BULK_CREATE_IDS(reg: ecr.Registry, n: number): { ecr.entity }
55 | local ids = table.create(n)
56 | for i = 1, n do
57 | ids[i] = reg:create()
58 | end
59 | return ids
60 | end
61 |
62 | local function DEBUG_ID(key_ver: ecr.entity): string
63 | local key = bit32.band(key_ver, 2^16 - 1)
64 | local ver = bit32.rshift(bit32.band(key_ver, bit32.lshift(2^16 - 1, 16)), 16)
65 | return `{key}:{ver}`
66 | end
67 |
68 | local function DEBUG_ENTITY_LIST(reg: ecr.Registry)
69 | local list = reg:storage("list") :: any
70 | local s = {}
71 | table.insert(s, "------------------")
72 | table.insert(s, `free: {list.free}`)
73 | for i = 0, list.capacity - 1 do
74 | local key_ver = buffer.readu32(list.data, i*4)
75 | table.insert(s, `{i}: {DEBUG_ID(key_ver)} {key_ver}`)
76 | end
77 | table.insert(s, "------------------")
78 | print(table.concat(s, "\n"))
79 | end
80 |
81 | local N = 1e3
82 |
83 | TEST("registry:create()", function()
84 | do CASE "new ids unique"
85 | local reg = ecr.registry()
86 | local cache = {}
87 |
88 | for i = 1, N do
89 | local id = reg:create()
90 | CHECK(not cache[id])
91 | cache[id] = true
92 | end
93 | end
94 |
95 | do CASE "reusing keys produce unique ids"
96 | local reg = ecr.registry()
97 |
98 | local cache = {}
99 |
100 | for i = 1, N do
101 | local id = reg:create()
102 | cache[id] = true
103 | end
104 |
105 | for id in cache do
106 | reg:release(id)
107 | end
108 |
109 | for i = 1, N do
110 | local id = reg:create()
111 | CHECK(not cache[id])
112 | cache[id] = true
113 | end
114 | end
115 |
116 | do CASE "use specific id"
117 | do -- copy ids from a server registry to a new registry in random order
118 | local sreg = ecr.registry()
119 |
120 | local ids = BULK_CREATE_IDS(sreg, N)
121 |
122 | -- randomize id array order
123 | local mixed = table.clone(ids)
124 | for i = 1, N do
125 | local a = math.random(1, N)
126 | local b = math.random(1, N)
127 | mixed[a], mixed[b] = mixed[b], mixed[a]
128 | end
129 |
130 | local reg = ecr.registry()
131 |
132 | for i = 1, N do
133 | reg:create(mixed[i])
134 | end
135 |
136 | for i = 1, N do
137 | CHECK(reg:contains(ids[i]))
138 | end
139 | end
140 |
141 | do -- test internal linked list order
142 | local reg = ecr.registry()
143 | reg:create(CREATE_ID(3, 2))
144 | CHECK(reg:create() == CREATE_ID(1, 1))
145 | CHECK(reg:create() == CREATE_ID(2, 1))
146 | CHECK(reg:create() == CREATE_ID(4, 1))
147 | end
148 | end
149 |
150 | do CASE "reuse previously used id"
151 | do
152 | local reg = ecr.registry()
153 | local id = reg:create()
154 | reg:release(id)
155 | reg:create(id)
156 | CHECK(reg:contains(id))
157 | end
158 |
159 | do -- test internal linked list order and version tracking
160 | local reg = ecr.registry()
161 | local id1, id2, id3 = reg:create(), reg:create(), reg:create()
162 | local id4 = reg:create()
163 |
164 | reg:release(id4)
165 | reg:release(id3)
166 | reg:release(id2)
167 | reg:release(id1)
168 |
169 | -- 1 -> 2 -> 3 -> 4
170 | reg:create(id4)
171 | -- 1 -> 2 -> 3
172 | reg:create(id2)
173 | -- 1 -> 3
174 | reg:create(id1)
175 | -- 3
176 | local id5 = reg:create(CREATE_ID(5, 1))
177 | reg:release(id5)
178 | -- 3 -> 5
179 | reg:create(id5)
180 | -- 3
181 | reg:create(id3)
182 | -- [empty]
183 |
184 | CHECK(reg:contains(id1))
185 | CHECK(reg:contains(id2))
186 | CHECK(reg:contains(id3))
187 | CHECK(reg:contains(id4))
188 | CHECK(reg:contains(id5))
189 |
190 | CHECK(KEY(id1) == 1)
191 | CHECK(KEY(id2) == 2)
192 | CHECK(KEY(id3) == 3)
193 | CHECK(KEY(id4) == 4)
194 | CHECK(KEY(id5) == 5)
195 | end
196 | end
197 |
198 | do CASE "error if key is already in use"
199 | do
200 | local sreg = ecr.registry()
201 | local id = sreg:create()
202 |
203 | local reg = ecr.registry()
204 |
205 | CHECK(id == reg:create())
206 |
207 | CHECK_ERR("unable to create entity; key is already in use", function()
208 | reg:create(id)
209 | end)
210 | end
211 |
212 | do -- try force create an id with same key as one in use
213 | local ok, err = pcall(function()
214 | local reg = ecr.registry()
215 | local id1 = reg:create() -- ID(1,1)
216 | reg:destroy(id1)
217 | reg:create() -- ID(1,2)
218 | reg:create(id1)
219 | end)
220 | CHECK(not ok and string.find(err, "already in use"))
221 | end
222 | end
223 |
224 | do CASE "error if null is used"
225 | local reg = ecr.registry()
226 | CHECK_ERR("malformed id", function()
227 | reg:create(ecr.null)
228 | end)
229 | end
230 |
231 | do CASE "entity limit"
232 | -- 2 ids reserved: ctx and null
233 | local LIMIT = 2^16 - 2
234 |
235 | local reg = ecr.registry()
236 |
237 | for i = 1, LIMIT do
238 | reg:create()
239 | end
240 |
241 | CHECK(not reg:contains(ecr.null))
242 |
243 | CHECK_ERR("cannot create entity; registry is at max entities", function()
244 | reg:create()
245 | end)
246 | end
247 |
248 | do CASE "partitions respected"
249 | local sreg = ecr.registry(1, 100)
250 | local reg = ecr.registry(101, 200)
251 |
252 | local ids = {}
253 |
254 | for i = 1, 100 do
255 | ids[i] = sreg:create()
256 | end
257 | CHECK_ERR("cannot create entity; registry is at max entities", function()
258 | sreg:create()
259 | end)
260 |
261 | for i = 1, 100 do
262 | reg:create()
263 | end
264 | CHECK_ERR("cannot create entity; registry is at max entities", function()
265 | reg:create()
266 | end)
267 |
268 | for _, id in ids do
269 | reg:create(id)
270 | end
271 |
272 | for _, id in ids do
273 | CHECK(reg:contains(id))
274 | end
275 |
276 | -- check if entity outside partition is not readded to list
277 | reg:destroy(ids[1])
278 | CHECK_ERR("cannot create entity; registry is at max entities", function()
279 | reg:create()
280 | end)
281 |
282 | end
283 |
284 | do CASE "partition edge cases"
285 | CHECK(not pcall(function() ecr.registry(0, 1) end))
286 | CHECK(not pcall(function() ecr.registry(2, 1) end))
287 | CHECK(not pcall(function() ecr.registry(1, 2^16-1) end))
288 | CHECK(pcall(function() ecr.registry(1, 2^16-2) end))
289 |
290 | do
291 | local reg = ecr.registry(1000, 1000)
292 | reg:create()
293 | CHECK_ERR("cannot create entity; registry is at max entities", function()
294 | reg:create()
295 | end)
296 | end
297 |
298 | do -- test linked list traversal branch
299 | local reg = ecr.registry(901, 1000)
300 | reg:create(CREATE_ID(1000, 1)) -- create last entity to initialize linked list of entire partition
301 | SET_KEY_VERSION(reg, 950, 10)
302 | reg:destroy(reg:create(CREATE_ID(950, 2))) -- create entity in middle of list then destroy
303 | CHECK(reg:create() == CREATE_ID(950, 3)) -- pop head which was previously destroyed entity
304 | for i = 1, 49 do -- check linked list is maintained
305 | CHECK(reg:create() == CREATE_ID(900 + i, 1))
306 | end
307 | end
308 | end
309 | end)
310 |
311 | TEST("registry:release()", function()
312 | local reg = ecr.registry()
313 |
314 | do CASE "released entities invalid"
315 | local id = reg:create()
316 | reg:release(id)
317 | CHECK(not reg:contains(id))
318 | end
319 |
320 | do CASE "release invalid id"
321 | local id = reg:create()
322 | reg:release(id)
323 | CHECK_ERR("invalid entity", function()
324 | reg:release(id)
325 | end)
326 | end
327 |
328 | do CASE "release null id"
329 | CHECK_ERR("invalid entity", function()
330 | reg:release(ecr.null)
331 | end)
332 | end
333 | end)
334 |
335 | TEST("registry:contains()", function()
336 | local reg = ecr.registry()
337 |
338 | do
339 | local id = reg:create()
340 |
341 | do CASE "new entity valid"
342 | CHECK(reg:contains(id))
343 | end
344 |
345 | reg:release(id)
346 |
347 | do CASE "released entity invalid"
348 | CHECK(not reg:contains(id))
349 | end
350 |
351 | local newid = reg:create()
352 |
353 | do CASE "id with reused key valid"
354 | CHECK(reg:contains(newid))
355 | end
356 |
357 | do CASE "released entity still invalid despite key reuse"
358 | CHECK(not reg:contains(id))
359 | end
360 |
361 | do CASE "id that has not been created yet"
362 | local sreg = ecr.registry()
363 | for i = 1, 10 do sreg:create() end
364 | local id2 = sreg:create()
365 | CHECK(not reg:contains(id2))
366 | end
367 | end
368 |
369 | do CASE "integer invalid"
370 | local id = reg:create()
371 | assert(KEY(id) == 2)
372 | CHECK(not reg:contains(2))
373 | end
374 |
375 | do CASE "null entity invalid"
376 | CHECK(not reg:contains(ecr.null))
377 | end
378 | end)
379 |
380 | TEST("registry:version()", function()
381 | local reg = ecr.registry()
382 |
383 | do CASE "get version"
384 | local id = reg:create()
385 | CHECK(VER(id) == 1)
386 | end
387 |
388 | do CASE "version increments on reuse"
389 | local id_old = reg:create()
390 | CHECK(VER(id_old) == 1)
391 | reg:destroy(id_old)
392 | local id_new = reg:create()
393 | CHECK(VER(id_old) == 1)
394 | CHECK(VER(id_new) == 2)
395 | end
396 | end)
397 |
398 | TEST("registry:current()", function()
399 | local reg = ecr.registry()
400 | local id_old = reg:create()
401 | reg:release(id_old)
402 | local id_new = reg:create()
403 |
404 | do CASE "get current version"
405 | CHECK(VER(id_old) == 1)
406 | CHECK(GET_KEY_VERSION(reg, KEY(id_old)) == 2)
407 | end
408 |
409 | do CASE "version increments on release"
410 | reg:release(id_new)
411 | CHECK(GET_KEY_VERSION(reg, KEY(id_new)) == 3)
412 | end
413 | end)
414 |
415 | TEST("registry:add()", function()
416 | local ADD_A = ecr.component(function() return true end)
417 | local ADD_B = ecr.component(function() return false end)
418 | local ADD_C = ecr.component(function() return nil end)
419 |
420 | local reg = ecr.registry()
421 |
422 | do CASE "add components"
423 | local id = reg:create()
424 | reg:add(id, ADD_A, ADD_B)
425 | CHECK(reg:has(id, ADD_A))
426 | CHECK(reg:has(id, ADD_B))
427 | end
428 |
429 | do CASE "add an already added component does nothing"
430 | local id = reg:create()
431 | reg:set(id, ADD_A, false)
432 | reg:add(id, ADD_A)
433 | CHECK(reg:get(id, ADD_A) == false)
434 | end
435 |
436 | do CASE "add a component with no constructor errors"
437 | CHECK_ERR("no constructor defined for component (arg #1)", function()
438 | local id = reg:create()
439 | reg:add(id, B)
440 | end)
441 | end
442 |
443 | do CASE "constructor returning nil errors"
444 | CHECK_ERR("component (arg #1) constructor did not return a value", function()
445 | local id = reg:create()
446 | reg:add(id, ADD_C)
447 | end)
448 | end
449 |
450 | do CASE "add component to invalid id with unused key"
451 | CHECK_ERR("invalid entity", function()
452 | local id = reg:create()
453 | reg:release(id)
454 | reg:add(id, ADD_A)
455 | end)
456 | end
457 |
458 | do CASE "add component to invalid id with used key"
459 | CHECK_ERR("invalid entity", function()
460 | local id = reg:create()
461 | reg:release(id)
462 | reg:create() -- reuse key
463 | reg:add(id, ADD_A)
464 | end)
465 | end
466 |
467 | do CASE "add component to invalid id with used key with component"
468 | CHECK_ERR("invalid entity", function()
469 | local id = reg:create()
470 | reg:release(id)
471 | local newid = reg:create() -- reuse key
472 | reg:add(newid, ADD_A)
473 | reg:add(id, A)
474 | end)
475 | end
476 |
477 | do CASE "add component to null entity"
478 | CHECK_ERR("invalid entity", function()
479 | reg:add(ecr.null, ADD_A)
480 | end)
481 | end
482 | end)
483 |
484 | TEST("registry:set()", function()
485 | local reg = ecr.registry()
486 |
487 | do
488 | local id = reg:create()
489 |
490 | do CASE "add component"
491 | reg:set(id, A, 1)
492 | CHECK(reg:get(id, A) == 1)
493 | end
494 |
495 | do CASE "change component"
496 | reg:set(id, A, 2)
497 | CHECK(reg:get(id, A) == 2)
498 | end
499 |
500 | do CASE "remove component"
501 | reg:remove(id, A)
502 | CHECK(reg:try_get(id, A) == nil)
503 | CHECK(not reg:has(id, A))
504 | end
505 | end
506 |
507 | do CASE "add component to invalid id with unused key"
508 | CHECK_ERR("invalid entity", function()
509 | local id = reg:create()
510 | reg:release(id)
511 | reg:set(id, A, 1)
512 | end)
513 | end
514 |
515 | do CASE "add component to invalid id with used key"
516 | CHECK_ERR("invalid entity", function()
517 | local id = reg:create()
518 | reg:release(id)
519 | reg:create() -- reuse key
520 | reg:set(id, A, 1)
521 | end)
522 | end
523 |
524 | do CASE "add component to invalid id with used key that has component"
525 | CHECK_ERR("invalid entity", function()
526 | local id = reg:create()
527 | reg:release(id)
528 |
529 | local newid = reg:create() -- reuse key
530 | reg:set(newid, A, 0)
531 |
532 | reg:set(id, A, 1)
533 | end)
534 | end
535 |
536 | do CASE "add component to null entity"
537 | CHECK_ERR("invalid entity", function()
538 | reg:set(ecr.null, A, true)
539 | end)
540 | end
541 |
542 | do CASE "change component of invalid entity with used key with component"
543 | CHECK_ERR("invalid entity", function()
544 | local id = reg:create()
545 | reg:release(id)
546 |
547 | local newid = reg:create() -- reuse key
548 | reg:set(newid, A, 1)
549 |
550 | reg:set(id, A, 2)
551 | end)
552 | end
553 |
554 | do CASE "attempt to set nil value"
555 | CHECK_ERR("cannot set component value to nil", function()
556 | local id = reg:create()
557 | reg:set(id, A, nil)
558 | end)
559 | end
560 |
561 | do CASE "set tag"
562 | local id = reg:create()
563 | reg:set(id, TA, nil)
564 | CHECK(reg:has(id, TA))
565 | end
566 | end)
567 |
568 | TEST("registry:get()", function()
569 | local reg = ecr.registry()
570 |
571 | do
572 | local id = reg:create()
573 |
574 | do CASE "get component"
575 | reg:set(id, A, 1)
576 | CHECK(reg:get(id, A) == 1)
577 | end
578 |
579 | do CASE "get multiple components"
580 | reg:set(id, B, 2)
581 | reg:set(id, C, 3)
582 | reg:set(id, D, 4)
583 |
584 | local values = { reg:get(id, A, B, C, D) }
585 |
586 | for i, value in values do
587 | CHECK(value == i)
588 | end
589 | end
590 |
591 | do CASE "get nil component"
592 | reg:remove(id, A)
593 | CHECK_ERR("entity does not have component (arg #1)", function()
594 | reg:get(id, A)
595 | end)
596 | end
597 | end
598 |
599 | do CASE "get component of invalid id"
600 | local id = reg:create()
601 | reg:release(id)
602 | CHECK_ERR("entity does not have component (arg #1)", function()
603 | reg:get(id, A)
604 | end)
605 | end
606 |
607 | do CASE "get component of invalid id with used key that has component"
608 | local id = reg:create()
609 | reg:release(id)
610 |
611 | local newid = reg:create()
612 | reg:set(newid, A, 1)
613 |
614 | CHECK_ERR("invalid entity", function()
615 | reg:get(id, A)
616 | end)
617 | end
618 | end)
619 |
620 | TEST("registry:try_get()", function()
621 | local reg = ecr.registry()
622 |
623 | do CASE "get nil component"
624 | local id = reg:create()
625 | reg:remove(id, A)
626 | CHECK(reg:try_get(id, A) == nil)
627 | end
628 |
629 | do CASE "get component of invalid id"
630 | local id = reg:create()
631 | reg:release(id)
632 | CHECK(reg:try_get(id, A) == nil)
633 | end
634 |
635 | do CASE "get component of invalid id with used key that has component"
636 | local id = reg:create()
637 | reg:release(id)
638 |
639 | local newid = reg:create()
640 | reg:set(newid, A, 1)
641 |
642 | CHECK(reg:try_get(id, A) == nil)
643 | end
644 | end)
645 |
646 | TEST("registry:has()", function()
647 | local reg = ecr.registry()
648 |
649 | do
650 | local id = reg:create()
651 |
652 | do CASE "has component"
653 | reg:set(id, A, true)
654 | CHECK(reg:has(id, A) == true)
655 |
656 | reg:set(id, B, true)
657 | CHECK(reg:has(id, A, B))
658 | end
659 |
660 | do CASE "does not have component"
661 | reg:remove(id, A)
662 | CHECK(reg:has(id, A) == false)
663 | CHECK(reg:has(id, B, A) == false)
664 | end
665 | end
666 |
667 | do CASE "invalid entity"
668 | local id = reg:create()
669 | reg:release(id)
670 | CHECK(reg:has(id, A) == false)
671 | end
672 |
673 | do CASE "invalid entity with used key that has component"
674 | local id = reg:create()
675 | reg:release(id)
676 |
677 | local newid = reg:create()
678 | reg:set(newid, A, 1)
679 |
680 | CHECK(reg:has(id, A) == false)
681 | end
682 | end)
683 |
684 | TEST("registry:insert()", function()
685 | local ARRAY = ecr.component() :: {number}
686 | local reg = ecr.registry()
687 | local id = reg:create()
688 |
689 | do CASE "insert adds array if no value"
690 | reg:insert(id, ARRAY, 1)
691 |
692 | CHECK(reg:has(id, ARRAY))
693 | CHECK(type(reg:get(id, ARRAY)) == "table")
694 | CHECK(reg:get(id, ARRAY)[1] == 1)
695 | end
696 |
697 | do CASE "insert adds values to the end"
698 | reg:insert(id, ARRAY, 2)
699 |
700 | CHECK(reg:has(id, ARRAY))
701 | CHECK(type(reg:get(id, ARRAY)) == "table")
702 | CHECK(reg:get(id, ARRAY)[1] == 1)
703 | CHECK(reg:get(id, ARRAY)[2] == 2)
704 | end
705 |
706 | end)
707 |
708 | TEST("registry:patch()", function()
709 | local reg = ecr.registry()
710 |
711 | do CASE "change component"
712 | local id = reg:create()
713 | reg:set(id, A, 0)
714 |
715 | local ran = false
716 | reg:on_change(A):connect(function()
717 | ran = true
718 | end)
719 |
720 | reg:patch(id, A :: number, function(v)
721 | CHECK(v == 0)
722 | return v + 1
723 | end)
724 | CHECK(reg:get(id, A) == 1)
725 | CHECK(ran)
726 | end
727 |
728 | do CASE "patch returns nil"
729 | local id = reg:create()
730 | reg:set(id, A, true)
731 |
732 | CHECK_ERR("function cannot return nil", function()
733 | reg:patch(id, A :: number, function(v)
734 | return nil :: any
735 | end)
736 | end)
737 | end
738 |
739 | do CASE "change non-existent component"
740 | local id = reg:create()
741 |
742 | CHECK_ERR("entity does not have component and no constructor for component (unknown)", function()
743 | reg:patch(id, A :: number, function(v)
744 | CHECK(false)
745 | return v + 1
746 | end)
747 | end)
748 | end
749 |
750 | do CASE "add non-existent component without constructor"
751 | local id = reg:create()
752 |
753 | local ran = false
754 | reg:on_add(H):connect(function()
755 | ran = true
756 | end)
757 |
758 | reg:patch(id, H)
759 |
760 | CHECK(ran)
761 | CHECK(reg:get(id, H) == true)
762 | end
763 |
764 | do CASE "change non-existent component with constructor"
765 | local id = reg:create()
766 |
767 | local ran = false
768 | reg:on_add(H):connect(function()
769 | ran = true
770 | end)
771 |
772 | reg:patch(id, H, function(v)
773 | CHECK(v == true)
774 | return not v
775 | end)
776 |
777 | CHECK(ran)
778 | CHECK(reg:get(id, H) == false)
779 |
780 | CHECK_ERR("function cannot return nil", function()
781 | reg:patch(id, H, function(v)
782 | return nil :: any
783 | end)
784 | end)
785 | end
786 |
787 | do CASE "invalid id"
788 | local id = reg:create()
789 | reg:release(id)
790 |
791 | CHECK_ERR("invalid entity", function()
792 | reg:patch(id, A :: number, function(v)
793 | CHECK(false)
794 | return v + 1
795 | end)
796 | end)
797 | end
798 |
799 | do CASE "invalid id that shares same key with id that has component"
800 | local id = reg:create()
801 | reg:release(id)
802 |
803 | local newid = reg:create()
804 | reg:set(newid, A, 1)
805 |
806 | CHECK_ERR("invalid entity", function()
807 | reg:patch(id, A :: number, function(v)
808 | CHECK(false)
809 | return v + 1
810 | end)
811 | end)
812 | end
813 | end)
814 |
815 | TEST("registry:remove()", function()
816 | local reg = ecr.registry()
817 |
818 | do
819 | local id = reg:create()
820 | reg:set(id, A, 1)
821 | reg:set(id, B, 1)
822 | reg:set(id, C, 1)
823 |
824 | local id2 = reg:create()
825 | reg:set(id2, A, 2)
826 |
827 | do CASE "remove single component"
828 | reg:remove(id, A)
829 | CHECK(reg:has(id, A) == false)
830 | CHECK(reg:get(id2, A) == 2) -- check removal maintains associativity
831 | end
832 |
833 | do CASE "remove multiple components"
834 | reg:remove(id, B, C)
835 | CHECK(reg:has(id, B) == false)
836 | CHECK(reg:has(id, C) == false)
837 | end
838 | end
839 |
840 | do CASE "remove component from invalid entity"
841 | local id = reg:create()
842 | reg:release(id)
843 |
844 | local ok = pcall(function()
845 | reg:remove(id, A)
846 | end)
847 |
848 | CHECK(ok)
849 | end
850 |
851 | do CASE "remove component from invalid id that shares key with entity that has component"
852 | local id = reg:create()
853 | reg:release(id)
854 |
855 | local newid = reg:create()
856 | reg:set(newid, A, 1)
857 |
858 | --CHECK_ERR("invalid entity", function()
859 | --reg:remove(id, A)
860 | --end)
861 |
862 | reg:remove(id, A)
863 |
864 | CHECK(reg:has(newid, A))
865 | end
866 | end)
867 |
868 | TEST("registry:find()", function()
869 | local reg = ecr.registry()
870 |
871 | local ids = BULK_CREATE_IDS(reg, 5)
872 |
873 | for i, id in ids do
874 | reg:set(id, A, i)
875 | reg:set(id, TA)
876 | end
877 |
878 | CHECK(reg:find(A, 1) == ids[1])
879 | CHECK(reg:find(A, 3) == ids[3])
880 | CHECK(reg:find(A, 5) == ids[5])
881 |
882 | CHECK(reg:find(A, 6) == nil)
883 | reg:clear(A)
884 | CHECK(reg:find(A, 1) == nil)
885 |
886 | CHECK(reg:find(TA) == ids[1])
887 | reg:clear(TA)
888 | CHECK(reg:find(TA) == nil)
889 | end)
890 |
891 | TEST("registry:clear()", function()
892 | do
893 | local reg = ecr.registry()
894 | local id1 = reg:create()
895 | local id2 = reg:create()
896 |
897 | reg:set(id1, A, 1)
898 | reg:set(id2, A, 2)
899 |
900 | reg:set(id1, B, 1)
901 | reg:set(id2, B, 2)
902 |
903 | reg:set(id1, C, 1)
904 | reg:set(id2, C, 2)
905 |
906 | do CASE "clear A"
907 | reg:clear(A)
908 | CHECK(reg:has(id1, A) == false)
909 | CHECK(reg:has(id2, A) == false)
910 | end
911 |
912 | do CASE "clear B and C"
913 | reg:clear(B, C)
914 | CHECK(reg:has(id1, B) == false)
915 | CHECK(reg:has(id2, B) == false)
916 | CHECK(reg:has(id1, C) == false)
917 | CHECK(reg:has(id2, C) == false)
918 | end
919 | end
920 |
921 | do CASE "clear entities"
922 | local reg = ecr.registry()
923 |
924 | local ids = table.create(N)
925 |
926 | -- set key = 1 version to max
927 | SET_KEY_VERSION_AND_ENSURE_EXISTS(reg, 1, MAX_VER)
928 |
929 | for i = 1, N do
930 | ids[i] = reg:create()
931 | end
932 |
933 | reg:clear()
934 |
935 | local ids_after = BULK_CREATE_IDS(reg, N)
936 |
937 | for i = 1, N do
938 | if i == 1 then
939 | CHECK(ids_after[i] == CREATE_ID(1, 1))
940 | else
941 | CHECK(VER(ids[i]) + 1 == VER(ids_after[i]))
942 | end
943 | end
944 | end
945 |
946 | do CASE "clear with groups"
947 | local reg = ecr.registry()
948 | reg:group(A, B)
949 |
950 | local listener = { count = 0 } :: { id: number?, count: number }
951 |
952 | reg:on_remove(A):connect(function(id)
953 | listener.id = id
954 | listener.count += 1
955 | end)
956 |
957 | local eB = reg:handle():set(B, 2)
958 | local eAB = reg:handle():set(A, 1):set(B, 1)
959 |
960 | reg:clear(A)
961 |
962 | CHECK(reg:storage(A).size == 0)
963 | CHECK(listener.id == eAB.entity and listener.count == 1)
964 |
965 | eAB:set(A, 1) -- 1
966 | eB:set(A, 2) -- 2
967 |
968 | eAB:remove(A)
969 |
970 | CHECK(reg:storage(A).size == 1)
971 | local pool = reg:storage(A)
972 | local entities = ecr.buffer_to_array(pool.entities, pool.size)
973 | CHECK(entities[1] == eB.entity)
974 | end
975 | end)
976 |
977 | TEST("registry:has_none()", function()
978 | local reg = ecr.registry()
979 | local id = reg:create()
980 |
981 | do CASE "new entity is has none"
982 | CHECK(reg:has_none(id))
983 | end
984 |
985 | reg:set(id, B, 1)
986 |
987 | do CASE "no longer has none after assigning component"
988 | CHECK(not reg:has_none(id))
989 | end
990 |
991 | reg:remove(id, B)
992 |
993 | do CASE "has none after removing last component"
994 | CHECK(reg:has_none(id))
995 | end
996 |
997 | reg:release(id) -- UB
998 |
999 | do CASE "rejects invalid entity"
1000 | CHECK_ERR("invalid entity", function()
1001 | reg:has_none(id)
1002 | end)
1003 | end
1004 | end)
1005 |
1006 | TEST("registry:destroy()", function()
1007 | local reg = ecr.registry()
1008 |
1009 | do CASE "component was removed"
1010 | local id = reg:create()
1011 |
1012 | reg:set(id, A, 1)
1013 | reg:set(id, B, 1)
1014 |
1015 | reg:destroy(id)
1016 |
1017 | -- do not rely on this behavior
1018 | CHECK(not reg:contains(id))
1019 | CHECK(not reg:has(id, A))
1020 | CHECK(not reg:has(id, B))
1021 | end
1022 |
1023 | do CASE "destroy invalid id"
1024 | local id = reg:create()
1025 | reg:release(id)
1026 |
1027 | CHECK_ERR("invalid entity", function()
1028 | reg:destroy(id)
1029 | end)
1030 | end
1031 | end)
1032 |
1033 | TEST("#registry", function()
1034 | local reg = ecr.registry()
1035 |
1036 | do CASE "initial registry size is 0"
1037 | CHECK(#reg:view(ecr.entity) == 0)
1038 | end
1039 |
1040 | local id = reg:create()
1041 |
1042 | do CASE "size after entity creation"
1043 | CHECK(#reg:view(ecr.entity) == 1)
1044 | end
1045 |
1046 | reg:release(id)
1047 |
1048 | do CASE "size after entity release"
1049 | CHECK(#reg:view(ecr.entity) == 0)
1050 | end
1051 |
1052 | for i = 1, 100 do reg:create() end
1053 |
1054 | do CASE "mass creation"
1055 | CHECK(#reg:view(ecr.entity) == 100)
1056 | end
1057 |
1058 | reg:clear()
1059 |
1060 | do CASE "size after clear"
1061 | CHECK(#reg:view(ecr.entity) == 0)
1062 | end
1063 |
1064 | reg:create(id)
1065 |
1066 | do CASE "size after specific entity creation"
1067 | CHECK(#reg:view(ecr.entity) == 1)
1068 | end
1069 |
1070 | do CASE "size after context entity creation"
1071 | reg:context()
1072 | CHECK(#reg:view(ecr.entity) == 2)
1073 | end
1074 | end)
1075 |
1076 | TEST("registry:view(ecr.entity)", function()
1077 | do
1078 | local reg = ecr.registry()
1079 |
1080 | local ids = BULK_CREATE_IDS(reg, N)
1081 |
1082 | -- destroy random entities
1083 | for i = 1, N do
1084 | if math.random() > 0.5 then
1085 | reg:destroy(ids[i])
1086 | ids[i] = nil
1087 | end
1088 | end
1089 |
1090 | do CASE "all entities are valid"
1091 | local cache = {}
1092 |
1093 | for entity in reg:view(ecr.entity) do
1094 | cache[entity] = true
1095 | end
1096 |
1097 | for _, entity in ids do
1098 | CHECK(cache[entity])
1099 | end
1100 | end
1101 | end
1102 | end)
1103 |
1104 | TEST("registry:view()", function()
1105 | local function flip()
1106 | return math.random() > 0.3
1107 | end
1108 |
1109 | local function empty(t)
1110 | return next(t) == nil
1111 | end
1112 |
1113 | local function getids(view: ecr.View<...unknown>): { [ecr.entity]: { unknown } }
1114 | local cache = {}
1115 |
1116 | for id, a, b, c, d in view do
1117 | cache[id] = { a, b, c, d }
1118 | end
1119 |
1120 | return cache
1121 | end
1122 |
1123 | local reg = ecr.registry()
1124 |
1125 | local ids = {}
1126 | local As = {}
1127 | local Bs = {}
1128 | local Cs = {}
1129 | local ABs = {}
1130 | local ABCs = {}
1131 | local ABCDs = {}
1132 | local AnotCs = {}
1133 | local ABnotCs = {}
1134 | local AnotBCs = {}
1135 | local notABs = {}
1136 |
1137 | for i = 1, N do
1138 | local id = reg:create()
1139 | ids[i] = id
1140 |
1141 | local has = {}
1142 | for _, component in { A, B, C, D, E, F, G, H } do
1143 | if flip() then
1144 | reg:set(id, component, id * component)
1145 | has[component] = true
1146 | end
1147 | end
1148 |
1149 | if has[A] then table.insert(As, id) end
1150 | if has[B] then table.insert(Bs, id) end
1151 | if has[C] then table.insert(Cs, id) end
1152 | if has[A] and not has[C] then table.insert(AnotCs, id) end
1153 | if has[A] and has[B] then table.insert(ABs, id) end
1154 | if has[A] and has[B] and has[C] then table.insert(ABCs, id) end
1155 | if has[A] and has[B] and not has[C] then table.insert(ABnotCs, id) end
1156 | if has[A] and not has[B] and not has[C] then table.insert(AnotBCs, id) end
1157 | if has[A] and has[B] and has[C] and has[D] then table.insert(ABCDs, id) end
1158 | if not has[A] and not has[B] then table.insert(notABs, id) end
1159 | end
1160 |
1161 | do CASE "view size"
1162 | CHECK(#reg:view(A) == #As)
1163 | CHECK(#reg:view(B) == #Bs)
1164 | CHECK(#reg:view(A, B, C) >= #ABCs)
1165 | CHECK(#reg:view(A, B):exclude(C) >= #ABnotCs)
1166 | end
1167 |
1168 | do CASE "view all entities with A"
1169 | local viewed = getids(reg:view(A))
1170 | CHECK(not empty(viewed))
1171 | for _, id in As do
1172 | local v = viewed[id]
1173 | if not CHECK(v) then continue end
1174 | CHECK(v[1] == id * A)
1175 | end
1176 | end
1177 |
1178 | do CASE "view all entities with B"
1179 | local viewed = getids(reg:view(B))
1180 | CHECK(not empty(viewed))
1181 | for _, id in Bs do
1182 | local v = viewed[id]
1183 | if not CHECK(v) then continue end
1184 | CHECK(v[1] == id * B)
1185 | end
1186 | end
1187 |
1188 | do CASE "view all entities with AB"
1189 | local viewed = getids(reg:view(A, B))
1190 | CHECK(not empty(viewed))
1191 | for _, id in ABs do
1192 | local v = viewed[id]
1193 | if not CHECK(v) then continue end
1194 | CHECK(v[1] == id * A)
1195 | CHECK(v[2] == id * B)
1196 | end
1197 | end
1198 |
1199 | do CASE "view all entities with ABC"
1200 | local viewed = getids(reg:view(A, B, C))
1201 | CHECK(not empty(viewed))
1202 | for _, id in ABCs do
1203 | local v = viewed[id]
1204 | if not CHECK(v) then continue end
1205 | CHECK(v[1] == id * A)
1206 | CHECK(v[2] == id * B)
1207 | CHECK(v[3] == id * C)
1208 | end
1209 | end
1210 |
1211 | do CASE "view all entities with ABCD"
1212 | local viewed = getids(reg:view(A, B, C, D))
1213 | CHECK(not empty(viewed))
1214 | for _, id in ABCDs do
1215 | local v = viewed[id]
1216 | if not CHECK(v) then continue end
1217 | CHECK(v[1] == id * A)
1218 | CHECK(v[2] == id * B)
1219 | CHECK(v[3] == id * C)
1220 | CHECK(v[4] == id * D)
1221 | end
1222 | end
1223 |
1224 | -- do CASE "view all entities with ABC using C"
1225 | -- local viewed = getids(reg:view(A, B, C):use(C))
1226 | -- CHECK(not empty(viewed))
1227 | -- for _, id in ABCs do
1228 | -- local v = viewed[id]
1229 | -- if not CHECK(v) then continue end
1230 | -- CHECK(v[1] == id)
1231 | -- CHECK(v[2] == id)
1232 | -- CHECK(v[3] == id)
1233 | -- end
1234 | -- end
1235 |
1236 | do CASE "view all entities with A and not C"
1237 | local viewed = getids(reg:view(A):exclude(C))
1238 | CHECK(not empty(viewed))
1239 | for _, id in AnotCs do
1240 | local v = viewed[id]
1241 | if not CHECK(v) then continue end
1242 | CHECK(v[1] == id * A)
1243 | end
1244 | for id in viewed do
1245 | CHECK(not reg:has(id, C))
1246 | end
1247 | end
1248 |
1249 | do CASE "view all entities with AB and not C"
1250 | local viewed = getids(reg:view(A, B):exclude(C))
1251 | CHECK(not empty(viewed))
1252 | for _, id in ABnotCs do
1253 | local v = viewed[id]
1254 | if not CHECK(v) then continue end
1255 | CHECK(v[1] == id * A)
1256 | CHECK(v[2] == id * B)
1257 | end
1258 | for id in viewed do
1259 | CHECK(not reg:has(id, C))
1260 | end
1261 | end
1262 |
1263 | do CASE "view all entities with A and not BC"
1264 |
1265 | local viewed = getids(reg:view(A):exclude(B, C))
1266 | CHECK(not empty(viewed))
1267 | for _, id in AnotBCs do
1268 | local v = viewed[id]
1269 | if not CHECK(v) then continue end
1270 | CHECK(v[1] == id * A)
1271 | end
1272 | for id in viewed do
1273 | CHECK(not reg:has(id, B))
1274 | CHECK(not reg:has(id, C))
1275 | end
1276 | end
1277 |
1278 | do CASE "view all entities without AB"
1279 | local viewed = getids(reg:view(ecr.entity):exclude(A, B))
1280 | CHECK(not empty(viewed))
1281 | for _, id in notABs do
1282 | local v = viewed[id]
1283 | if not CHECK(v) then continue end
1284 | end
1285 | for id in viewed do
1286 | CHECK(not reg:has(id, A))
1287 | CHECK(not reg:has(id, B))
1288 | end
1289 | end
1290 |
1291 | do CASE "addition during view iteration"
1292 | local cache = {}
1293 | for id, c in reg:view(C) do
1294 | -- check that newly added entities+components are not included in iterations
1295 | CHECK(not cache[id])
1296 | if flip() then -- create new entity+component
1297 | local new = reg:create()
1298 | reg:set(new, C, true)
1299 | table.insert(Cs, id)
1300 | cache[new] = true
1301 | end
1302 | end
1303 | end
1304 |
1305 | do CASE "component removal during view iteration"
1306 | local viewed = {}
1307 | for id in reg:view(A) do
1308 | if flip() then
1309 | reg:remove(id, A)
1310 | table.remove(As, table.find(As, id))
1311 | end
1312 | viewed[id] = true
1313 | end
1314 | CHECK(not empty(viewed))
1315 | for _, entity in As do -- check that an entity isn't skipped due to reordering interally
1316 | CHECK(viewed[entity])
1317 | end
1318 | end
1319 |
1320 | -- do CASE "duplicate include errors"
1321 | -- local ok = pcall(function()
1322 | -- reg:view(A, B, A)
1323 | -- end)
1324 | -- CHECK(not ok)
1325 | -- end
1326 |
1327 | do CASE "exclude an include errors"
1328 | local ok = pcall(function()
1329 | reg:view(A, B):exclude(C, A)
1330 | end)
1331 | CHECK(not ok)
1332 | end
1333 |
1334 | do CASE "view patch single"
1335 | local expected_values = {}
1336 |
1337 | for id, v in reg:view(A) do
1338 | expected_values[id] = v + 1
1339 | end
1340 |
1341 | reg:view(A :: number):patch(function(v) return v + 1 end)
1342 |
1343 | for id, v in reg:view(A) do
1344 | CHECK(v == expected_values[id])
1345 | end
1346 | end
1347 |
1348 | do CASE "view patch double"
1349 | local expected_values = {}
1350 |
1351 | for id, a, b in reg:view(A, B) do
1352 | expected_values[id] = { a + 1, b + 2 }
1353 | end
1354 |
1355 | reg:view(A :: number, B :: number):patch(function(a, b) return a + 1, b + 2 end)
1356 |
1357 | for id, a, b in reg:view(A, B) do
1358 | CHECK(a == expected_values[id][1])
1359 | CHECK(b == expected_values[id][2])
1360 | end
1361 | end
1362 |
1363 | do CASE "view patch double (reverse query order)"
1364 | local expected_values = {}
1365 |
1366 | for id, a, b in reg:view(B, A) do
1367 | expected_values[id] = { a + 1, b + 2 }
1368 | end
1369 |
1370 | reg:view(B :: number, A :: number):patch(function(a, b) return a + 1, b + 2 end)
1371 |
1372 | for id, a, b in reg:view(B, A) do
1373 | CHECK(a == expected_values[id][1])
1374 | CHECK(b == expected_values[id][2])
1375 | end
1376 | end
1377 |
1378 | do CASE "view patch multi"
1379 | local expected_values = {}
1380 |
1381 | for id, a, b, c in reg:view(A, B, C) do
1382 | expected_values[id] = { a + 1, b + 2, c + 3 }
1383 | end
1384 |
1385 | reg:view(A :: number, B :: number, C :: number):patch(function(a, b, c)
1386 | return a + 1, b + 2, c + 3
1387 | end)
1388 |
1389 | for id, a, b, c in reg:view(A, B, C) do
1390 | CHECK(a == expected_values[id][1])
1391 | CHECK(b == expected_values[id][2])
1392 | CHECK(c == expected_values[id][3])
1393 | end
1394 | end
1395 | end)
1396 |
1397 | TEST("registry:on_add()", function()
1398 | local reg = ecr.registry()
1399 |
1400 | -- runcount, reg, entity, value
1401 | local cc, ce, cv = 0, nil, nil
1402 |
1403 | local function fn(...)
1404 | cc += 1
1405 | ce, cv = ...
1406 | end
1407 |
1408 | do
1409 | local con = reg:on_add(H):connect(fn)
1410 | local id = reg:create()
1411 |
1412 | CASE "adding triggers"
1413 | reg:add(id, H)
1414 | CHECK(cc == 1)
1415 | CHECK(ce == id)
1416 | CHECK(cv == true)
1417 |
1418 | CASE "changing does not trigger"
1419 | reg:set(id, H, false)
1420 | CHECK(cc == 1)
1421 |
1422 | CASE "removing then adding triggers"
1423 | reg:remove(id, H)
1424 | reg:set(id, H, true)
1425 | CHECK(cc == 2)
1426 |
1427 | con:disconnect()
1428 |
1429 | CASE "adding after disconnect does not trigger"
1430 | reg:remove(id, H)
1431 | reg:set(id, H, true)
1432 | CHECK(cc == 2)
1433 | end
1434 |
1435 | do CASE "entity creation"
1436 | reg:on_add(ecr.entity):connect(fn)
1437 |
1438 | local id = reg:create()
1439 |
1440 | CHECK(cc == 3)
1441 | CHECK(ce == id)
1442 |
1443 | reg:release(id)
1444 | reg:create(id)
1445 |
1446 | CHECK(cc == 4)
1447 | CHECK(ce == id)
1448 | end
1449 | end)
1450 |
1451 | TEST("registry:on_change()", function()
1452 | local reg = ecr.registry()
1453 | local id = reg:create()
1454 |
1455 | local cc, ce, cv, ov = 0, nil, nil, nil
1456 |
1457 | local function fn(...)
1458 | cc += 1
1459 | ce, cv = ...
1460 | ov = reg:get(..., A)
1461 | end
1462 |
1463 | local con = reg:on_change(A):connect(fn)
1464 |
1465 | CASE "adding does not trigger"
1466 | reg:set(id, A, true)
1467 | CHECK(cc == 0)
1468 |
1469 | CASE "changing triggers"
1470 | reg:set(id, A, false)
1471 | CHECK(cc == 1)
1472 | CHECK(ce == id)
1473 | CHECK(cv == false)
1474 | CHECK(ov == true)
1475 |
1476 | reg:patch(id, A, function(v)
1477 | return not v
1478 | end)
1479 | CHECK(cc == 2)
1480 | CHECK(ce == id)
1481 | CHECK(cv == true)
1482 |
1483 | CASE "removing does not trigger"
1484 | reg:remove(id, A)
1485 | CHECK(cc == 2)
1486 |
1487 | con:disconnect()
1488 |
1489 | CASE "changing after disconnect does not trigger"
1490 | reg:set(id, A, true) -- add
1491 | reg:set(id, A, false) -- change
1492 | CHECK(cc == 2)
1493 |
1494 | con:reconnect()
1495 |
1496 | CASE "changing after reconnect fires again"
1497 | reg:set(id, A, true)
1498 | CHECK(cc == 3)
1499 | end)
1500 |
1501 | TEST("registry:on_remove()", function()
1502 | do
1503 | local reg = ecr.registry()
1504 | local id = reg:create()
1505 |
1506 | local cc, ce, cv, lv = 0, nil, nil, nil
1507 |
1508 | local function fn(...)
1509 | cc += 1
1510 | lv = reg:get(id, A) -- get current value
1511 | ce, cv = ...
1512 | end
1513 |
1514 | local con = reg:on_remove(A):connect(fn)
1515 |
1516 | do CASE "removing triggers"
1517 | reg:set(id, A, true)
1518 | reg:remove(id, A)
1519 | CHECK(cc == 1)
1520 | CHECK(ce == id)
1521 | CHECK(cv == nil)
1522 | CHECK(lv == true)
1523 |
1524 | reg:set(id, A, true)
1525 | reg:remove(id, A)
1526 | CHECK(cc == 2)
1527 | CHECK(ce == id)
1528 | CHECK(cv == nil)
1529 | CHECK(lv == true)
1530 | end
1531 |
1532 | do CASE "removing via destroy"
1533 | reg:set(id, A, false)
1534 | reg:destroy(id)
1535 |
1536 | CHECK(cc == 3)
1537 | CHECK(ce == id)
1538 | CHECK(cv == nil)
1539 | CHECK(lv == false)
1540 | end
1541 |
1542 | con:disconnect()
1543 |
1544 | do CASE "removing after disconnect does not trigger"
1545 | reg:create(id)
1546 | reg:set(id, A, true)
1547 | reg:remove(id, A)
1548 | CHECK(cc == 3)
1549 | end
1550 | end
1551 |
1552 | do CASE "clearing components fires removing event"
1553 | local reg = ecr.registry()
1554 |
1555 | local entities = {}
1556 |
1557 | for i = 1, 1e3 do
1558 | entities[i] = reg:create()
1559 | reg:set(entities[i], A, true)
1560 | end
1561 |
1562 | local cache = {}
1563 |
1564 | reg:on_remove(A):connect(function(entity)
1565 | cache[entity] = true
1566 | end)
1567 |
1568 | reg:clear(A)
1569 |
1570 | for _, entity in entities do
1571 | CHECK(cache[entity])
1572 | end
1573 | end
1574 |
1575 | do CASE "entity destruction"
1576 | local reg = ecr.registry()
1577 |
1578 | local cid
1579 | reg:on_remove(ecr.entity):connect(function(id_)
1580 | cid = id_
1581 | end)
1582 |
1583 | local id1 = reg:create()
1584 | local id2 = reg:create()
1585 | reg:release(id1)
1586 |
1587 | CHECK(cid ~= nil)
1588 | CHECK(cid == id1)
1589 |
1590 | reg:clear()
1591 |
1592 | CHECK(cid == id2)
1593 | end
1594 | end)
1595 |
1596 | TEST("signals", function()
1597 | local reg = ecr.registry()
1598 |
1599 | do CASE "firing after disconnecting"
1600 | local run = false
1601 | local dont_run = true
1602 | local a = reg:on_add(A):connect(function() run = true end) -- idx 1
1603 | local b = reg:on_add(A):connect(function() dont_run = false end) -- idx 2
1604 | -- {A, B}
1605 | a:disconnect()
1606 | -- {B}
1607 | a:reconnect()
1608 | -- {B, A}
1609 | local _c = reg:on_add(A):connect(function() end) -- idx 3
1610 | -- {B, A, C}
1611 | b:disconnect()
1612 | -- should be {A, C}, not {B, C}
1613 | reg:handle():set(A, true)
1614 | CHECK(run)
1615 | CHECK(dont_run)
1616 | end
1617 |
1618 | end)
1619 |
1620 | TEST("registry:track()", function()
1621 | local function flip()
1622 | return math.random() > 0.3
1623 | end
1624 |
1625 | local function empty(t)
1626 | return next(t) == nil
1627 | end
1628 |
1629 | local function cache_ids(observer: ecr.Observer): { [ecr.entity]: { unknown } }
1630 | local cache = {}
1631 | for id, a, b, c in observer do
1632 | cache[id] = { a, b, c }
1633 | end
1634 | return cache
1635 | end
1636 |
1637 | local reg = ecr.registry()
1638 | local observerA = reg:track(A)
1639 | local observerB = reg:track(B)
1640 | local observerABC = reg:track(A, B, C)
1641 | local observerAnotC = reg:track(A):exclude(C)
1642 |
1643 | local ids = {}
1644 | local As = {}
1645 | local Bs = {}
1646 | local ABCs = {}
1647 | local AnotCs = {}
1648 |
1649 | for i = 1, N do
1650 | local id = reg:create()
1651 | ids[i] = id
1652 |
1653 | local has = {}
1654 | for _, component in { A, B, C, D, E, F, G, H } do
1655 | if flip() then
1656 | reg:set(id, component, id)
1657 | has[component] = true
1658 | end
1659 | end
1660 |
1661 | if has[A] then table.insert(As, id) end
1662 | if has[B] then table.insert(Bs, id) end
1663 | if has[A] and has[B] and has[C] then table.insert(ABCs, id) end
1664 | if has[A] and not has[C] then table.insert(AnotCs, id) end
1665 | end
1666 |
1667 | do CASE "observer size"
1668 | CHECK(#observerA == #As)
1669 | CHECK(#observerB == #Bs)
1670 | CHECK(#observerABC >= #ABCs)
1671 | CHECK(#observerAnotC >= #AnotCs)
1672 | end
1673 |
1674 | -- do CASE "initial components treated as changed"
1675 | -- do
1676 | -- local observer = reg:track(A)
1677 |
1678 | -- local cache = cache_ids(observer)
1679 | -- CHECK(not empty(cache))
1680 |
1681 | -- for _, id in As do
1682 | -- local v = cache[id]
1683 | -- if not CHECK(v) then continue end
1684 | -- CHECK(v[1] == id)
1685 | -- end
1686 | -- end
1687 | -- do
1688 | -- local observer = reg:track(A, B, C)
1689 | -- local cache = cache_ids(observer)
1690 | -- CHECK(not empty(cache))
1691 |
1692 | -- for _, id in ABCs do
1693 | -- local v = cache[id]
1694 | -- if not CHECK(v) then continue end
1695 | -- CHECK(v[1] == id)
1696 | -- CHECK(v[2] == id)
1697 | -- CHECK(v[3] == id)
1698 | -- end
1699 | -- end
1700 | -- end
1701 |
1702 | do CASE "A tracked"
1703 | local cache = cache_ids(observerA)
1704 | CHECK(not empty(cache))
1705 | for _, id in As do
1706 | local v = cache[id]
1707 | if not CHECK(v) then continue end
1708 | CHECK(v[1] == id)
1709 | end
1710 | CHECK(#observerA == 0)
1711 | end
1712 |
1713 | do CASE "AnotC tracked"
1714 | local cache = cache_ids(observerAnotC)
1715 | CHECK(not empty(cache))
1716 | for _, id in AnotCs do
1717 | local v = cache[id]
1718 | if not CHECK(v) then continue end
1719 | CHECK(v[1] == id)
1720 | end
1721 | CHECK(#observerAnotC == 0)
1722 | end
1723 |
1724 | do CASE "ABC tracked"
1725 | local cache = cache_ids(observerABC)
1726 | CHECK(not empty(cache))
1727 | for _, id in ABCs do
1728 | local v = cache[id]
1729 | if not CHECK(v) then continue end
1730 | CHECK(v[1] == id)
1731 | CHECK(v[2] == id)
1732 | CHECK(v[3] == id)
1733 | end
1734 | CHECK(#observerABC == 0)
1735 | end
1736 |
1737 | observerA:clear()
1738 |
1739 | do CASE "changed components tracked after clear"
1740 | CHECK(#observerA == 0)
1741 |
1742 | reg:set(As[1], A, As[1])
1743 |
1744 | local ran = false
1745 | for id, v in observerA do
1746 | ran = true
1747 | CHECK(id == As[1])
1748 | CHECK(v == id)
1749 | end
1750 | CHECK(ran)
1751 | CHECK(#observerA == 0)
1752 | end
1753 |
1754 | do CASE "changed components updated"
1755 | for _, id in Bs do
1756 | reg:patch(id, B :: number, function(cur)
1757 | return cur + 1
1758 | end)
1759 | end
1760 |
1761 | for id, v in observerB:iter() do
1762 | CHECK(v == id + 1)
1763 | end
1764 |
1765 | CHECK(#observerB == 0)
1766 | end
1767 |
1768 | do CASE "removed components are not returned"
1769 | local removed = {}
1770 |
1771 | for _, id in Bs do
1772 | if flip() then
1773 | reg:remove(id, B)
1774 | removed[id] = true
1775 | end
1776 | end
1777 |
1778 | CHECK(not empty(removed))
1779 |
1780 | for id in observerB do
1781 | CHECK(not removed[id])
1782 | end
1783 | end
1784 |
1785 | observerA:disconnect()
1786 |
1787 | do CASE "changes not recorded if disconnected"
1788 | local size = #observerA
1789 | local id = reg:create()
1790 | reg:set(id, A, 1)
1791 | CHECK(#observerA == size)
1792 | end
1793 |
1794 | observerA:reconnect():clear()
1795 |
1796 | do CASE "changes recorded again after reconnect"
1797 | local cache = {}
1798 | for i = 1, 1e2 do
1799 | local id = reg:create()
1800 | reg:set(id, A, id)
1801 | cache[id] = true
1802 | table.insert(As, id)
1803 | end
1804 |
1805 | for id, v in observerA do
1806 | CHECK(cache[id])
1807 | CHECK(v == id)
1808 | end
1809 | end
1810 |
1811 | observerA:clear()
1812 | table.clear(As)
1813 |
1814 | -- do CASE "observer returns up-to-date values despite being changed while disconnected"
1815 | -- local observer = reg:track(A):clear()
1816 | -- local id1 = reg:create()
1817 | -- local id2 = reg:create()
1818 | -- local id3 = reg:create()
1819 |
1820 | -- reg:set(id1, A, 1)
1821 | -- reg:set(id2, A, 2)
1822 | -- reg:set(id3, A, 3)
1823 |
1824 | -- observer:disconnect()
1825 |
1826 | -- reg:remove(id1, A) -- remove id1
1827 | -- reg:set(id2, A, -2) -- change id2
1828 |
1829 | -- local values = {}
1830 |
1831 | -- for id, v in observer do
1832 | -- values[id] = v
1833 | -- end
1834 |
1835 | -- CHECK(#observer == 2)
1836 | -- CHECK(not values[id1])
1837 | -- CHECK(values[id2] == -2)
1838 | -- CHECK(values[id3] == 3)
1839 | -- end
1840 |
1841 | do CASE "disconnect non-empty observer"
1842 | local observer = reg:track(A)
1843 |
1844 | reg:handle():set(A, true)
1845 |
1846 | CHECK_ERR("attempt to disconnect a non-empty observer", function()
1847 | observer:disconnect()
1848 | end)
1849 |
1850 | observer:clear():disconnect()
1851 | end
1852 |
1853 | do CASE "observer garbage collection"
1854 | local wref = setmetatable({}, { __mode = "v" })
1855 | wref.observer = observerA
1856 | wref.pool = (observerA :: any).pool
1857 | observerA:clear():disconnect()
1858 | observerA = nil :: any
1859 |
1860 | if not game then -- check if running in roblox (collect is sandboxed)
1861 | collectgarbage("collect" :: any)
1862 | collectgarbage("collect" :: any)
1863 | collectgarbage("collect" :: any)
1864 | CHECK(not wref.observer)
1865 |
1866 | local id = reg:create()
1867 | reg:set(id, A, 1)
1868 | reg:remove(id, A) -- internal pool only cleared after event fired again
1869 |
1870 | collectgarbage("collect" :: any)
1871 | collectgarbage("collect" :: any)
1872 | collectgarbage("collect" :: any)
1873 | CHECK(not wref.pool)
1874 | end
1875 | end
1876 |
1877 | do CASE "track multiple"
1878 | local observer = reg:track(A, B, C)
1879 | observer:clear()
1880 |
1881 | local id1 = reg:create()
1882 | reg:set(id1, C, true)
1883 |
1884 | local id2 = reg:create()
1885 | reg:set(id2, A, true)
1886 |
1887 | local id3 = reg:create()
1888 | reg:set(id3, A, 1)
1889 | reg:set(id3, B, 2)
1890 | reg:set(id3, C, 3)
1891 |
1892 | local runcount = 0
1893 | for id, a, b, c in observer do
1894 | runcount += 1
1895 | CHECK(id == id3)
1896 | CHECK(a == 1 and b == 2 and c == 3)
1897 | end
1898 | CHECK(runcount == 1)
1899 | end
1900 | end)
1901 |
1902 | TEST("registry:group()", function()
1903 | local function flip() return math.random() > 0.3 end
1904 | local function empty(t) return next(t) == nil end
1905 | local function getids(group: ecr.Group<...unknown>): { [ecr.entity]: { unknown } }
1906 | local cache = {}
1907 |
1908 | for id, a, b, c, d, e in group do
1909 | cache[id] = { a, b, c, d, e }
1910 | end
1911 |
1912 | return cache
1913 | end
1914 |
1915 | local Z, X = ecr.component(), ecr.component()
1916 |
1917 | local reg = ecr.registry()
1918 | local groupAB = reg:group(A, B)
1919 | reg:group(C, D, E)
1920 |
1921 | local ids = {}
1922 | local Bs = {}
1923 | local ABs = {}
1924 | local CDEs = {}
1925 | local FGs = {}
1926 |
1927 | for i = 1, N do
1928 | local id = reg:create()
1929 | ids[i] = id
1930 |
1931 | local has = {}
1932 | for _, component in { A, B, C, D, E, F, G, H } do
1933 | if flip() then
1934 | reg:set(id, component, id)
1935 | has[component] = true
1936 | end
1937 | end
1938 |
1939 | if has[B] then table.insert(Bs, id) end
1940 | if has[A] and has[B] then table.insert(ABs, id) end
1941 | if has[C] and has[D] and has[E] then table.insert(CDEs, id) end
1942 | if has[F] and has[G] then table.insert(FGs, id) end
1943 | end
1944 |
1945 | do CASE "group size"
1946 | CHECK(#reg:group(A, B) == #ABs)
1947 | CHECK(#reg:group(C, D, E) == #CDEs)
1948 | end
1949 |
1950 | do CASE "group AB cached"
1951 | local viewed = getids(groupAB)
1952 | CHECK(not empty(viewed))
1953 | for _, id in ABs do
1954 | local v = viewed[id]
1955 | if not CHECK(v) then continue end
1956 | CHECK(v[1] == id)
1957 | CHECK(v[2] == id)
1958 | CHECK(v[3] == nil)
1959 | end
1960 | end
1961 |
1962 | do CASE "group AB"
1963 | local viewed = getids(reg:group(A, B))
1964 | CHECK(not empty(viewed))
1965 | for _, id in ABs do
1966 | local v = viewed[id]
1967 | if not CHECK(v) then continue end
1968 | CHECK(v[1] == id)
1969 | CHECK(v[2] == id)
1970 | CHECK(v[3] == nil)
1971 | end
1972 | end
1973 |
1974 | do CASE "group CDE"
1975 | local viewed = getids(reg:group(C, D, E))
1976 | CHECK(not empty(viewed))
1977 | for _, id in CDEs do
1978 | local v = viewed[id]
1979 | if not CHECK(v) then continue end
1980 | CHECK(v[1] == id)
1981 | CHECK(v[2] == id)
1982 | CHECK(v[3] == id)
1983 | CHECK(v[4] == nil)
1984 | end
1985 | end
1986 |
1987 | do CASE "late initialized group FG"
1988 | CHECK(#reg:group(F, G) == #FGs)
1989 | local viewed = getids(reg:group(F, G))
1990 | CHECK(not empty(viewed))
1991 | for _, id in FGs do
1992 | local v = viewed[id]
1993 | if not CHECK(v) then continue end
1994 | CHECK(v[1] == id)
1995 | CHECK(v[2] == id)
1996 | CHECK(v[3] == nil)
1997 | end
1998 | end
1999 |
2000 | do CASE "removing components during group iteration"
2001 | local cache = {}
2002 | local removed = {}
2003 |
2004 | for id, a, b in reg:group(A, B) do
2005 | CHECK(not cache[id]) -- ensure iterators arent invalidated
2006 | cache[id] = { a, b }
2007 | if flip() then
2008 | removed[id] = true
2009 | reg:remove(id, A)
2010 | end
2011 | end
2012 |
2013 | CHECK(#reg:group(A, B) > 0)
2014 |
2015 | for id, a, b in reg:group(A, B) do
2016 | local v = cache[id]
2017 | CHECK(v)
2018 | CHECK(v[1] == a) -- check association is kept after remove
2019 | CHECK(v[2] == b)
2020 | cache[id] = nil
2021 | end
2022 |
2023 | CHECK(not empty(cache))
2024 |
2025 | for id in cache do
2026 | local v = removed[id]
2027 | if not v then
2028 | print(id, KEY(id))
2029 | end
2030 | CHECK(removed[id]) -- check only removed ids are left
2031 | end
2032 | end
2033 |
2034 | do CASE "clearing with groups"
2035 | reg:clear(A)
2036 | CHECK(#reg:group(A, B) == 0)
2037 | reg:set(ABs[1], A, 1)
2038 | CHECK(#reg:group(A, B) == 1)
2039 | end
2040 |
2041 | do CASE "removing owned component from entity not in group"
2042 | local id = reg:create()
2043 | reg:set(id, A, 1)
2044 | reg:set(id, C, 3)
2045 | reg:remove(id, A)
2046 | CHECK(reg:try_get(id, A) == nil and reg:get(id, C) == 3)
2047 | end
2048 |
2049 | do CASE "using owned component for new group errors"
2050 | CHECK_ERR(
2051 | "cannot create group; component (arg #2) is not owned by the same group as previous args",
2052 | function()
2053 | reg:group(F, H)
2054 | end
2055 | )
2056 | end
2057 |
2058 | do CASE "single component group errors"
2059 | local ok = pcall(function()
2060 | reg:group(H)
2061 | end)
2062 | CHECK(not ok)
2063 | end
2064 |
2065 | do CASE "group ABCDE"
2066 | local reg2 = ecr.registry()
2067 | local ids2 = {}
2068 | for i = 1, 1e2 do
2069 | local id = reg2:create()
2070 | reg2:set(id, A, id)
2071 | reg2:set(id, B, id)
2072 | reg2:set(id, C, id)
2073 | reg2:set(id, D, id)
2074 | reg2:set(id, E, id)
2075 | ids2[i] = id
2076 | end
2077 |
2078 | reg2:group(A, B, C, D, E)
2079 |
2080 | local viewed = getids(reg2:group(A, B, C, D, E))
2081 |
2082 | CHECK(not empty(viewed))
2083 | for _, id in ids2 do
2084 | local v = viewed[id]
2085 | if not CHECK(v) then break end
2086 | CHECK(v[1] == id)
2087 | CHECK(v[2] == id)
2088 | CHECK(v[3] == id)
2089 | CHECK(v[4] == id)
2090 | CHECK(v[5] == id)
2091 | end
2092 | end
2093 |
2094 | do CASE "invalidation check"
2095 | reg:group(Z, X)
2096 |
2097 | local e1 = reg:handle()
2098 | e1:set(Z, true)
2099 |
2100 | local e2 = reg:handle()
2101 | e2:set(Z, true)
2102 |
2103 | CHECK_ERR("group reordered during iteration", function()
2104 | for id in reg:view(Z) do
2105 | reg:set(id, X, true)
2106 | end
2107 | end)
2108 |
2109 | CHECK(not e1:has(X)) -- confirm invalidation occured
2110 | end
2111 | end)
2112 |
2113 | TEST("registry:handle()", function()
2114 | local reg = ecr.registry()
2115 |
2116 | do CASE "call with nil"
2117 | local e = reg:handle()
2118 |
2119 | CHECK(reg:contains(e.entity))
2120 |
2121 | e:set(A, 1)
2122 | :set(B, 2)
2123 |
2124 | CHECK(e:has(A, B))
2125 | end
2126 |
2127 | do CASE "call with existing"
2128 | local id = reg:create()
2129 | local e = reg:handle(id)
2130 | CHECK(e.entity == id)
2131 | end
2132 |
2133 | do CASE "caching"
2134 | local e = reg:handle()
2135 | local e2 = reg:handle(e.entity)
2136 | CHECK(e == e2)
2137 |
2138 | local id = reg:create()
2139 | local e_b = reg:handle(id)
2140 | local e_b2 = reg:handle(id)
2141 | CHECK(e_b == e_b2)
2142 | end
2143 |
2144 | do CASE "garbage collection"
2145 | local weak = setmetatable({}, { __mode = "v" })
2146 |
2147 | local id = reg:create()
2148 |
2149 | do
2150 | weak.v = reg:handle(id)
2151 | end
2152 |
2153 | (collectgarbage :: any)("collect")
2154 |
2155 | CHECK(weak.v == nil :: any)
2156 | end
2157 | end)
2158 |
2159 | TEST("registry:context()", function()
2160 | local reg = ecr.registry()
2161 |
2162 | do CASE "initially invalid"
2163 | CHECK(not reg:contains(ecr.context))
2164 | CHECK(#reg:view(ecr.entity) == 0)
2165 | end
2166 |
2167 | local ctx = reg:context()
2168 |
2169 | do CASE "valid"
2170 | CHECK(reg:contains(ctx.entity))
2171 | end
2172 |
2173 | ctx:set(A, 1):set(B, 2)
2174 |
2175 | do CASE "components"
2176 | CHECK(ctx:get(A) == 1)
2177 | CHECK(ctx:get(B) == 2)
2178 | end
2179 |
2180 | do CASE "sizes"
2181 | CHECK(#reg:view(ecr.entity) == 1)
2182 | CHECK(#reg:view(A) == 1)
2183 | end
2184 |
2185 | do CASE "destroy"
2186 | ctx:destroy()
2187 | CHECK(not reg:contains(ctx.entity))
2188 | reg:context()
2189 | CHECK(not ctx:has(A) and not ctx:has(B))
2190 | end
2191 |
2192 | reg:clear()
2193 |
2194 | do CASE "clearing affects"
2195 | CHECK(not reg:contains(ctx.entity))
2196 | end
2197 | end)
2198 |
2199 | TEST("registry:storage()", function()
2200 | do CASE "get storage"
2201 | local reg = ecr.registry()
2202 |
2203 | local ids = BULK_CREATE_IDS(reg, N)
2204 |
2205 | for i = 1, N do
2206 | reg:set(ids[i], A, i)
2207 | end
2208 |
2209 | local pool = reg:storage(A)
2210 |
2211 | local entities = ecr.buffer_to_array(pool.entities, pool.size)
2212 |
2213 | for i, id in entities do
2214 | CHECK(ids[i] == id)
2215 | CHECK(pool.values[i] == i)
2216 | end
2217 | end
2218 |
2219 | do CASE "get all storages"
2220 | local reg = ecr.registry()
2221 |
2222 | local pool_A = reg:storage(A)
2223 | local pool_B = reg:storage(B)
2224 |
2225 | local cache = {}
2226 |
2227 | for ctype, pool in reg:storage() do
2228 | cache[ctype] = pool
2229 | end
2230 |
2231 | CHECK(cache[A] == pool_A)
2232 | CHECK(cache[B] == pool_B)
2233 | end
2234 | end)
2235 |
2236 | TEST("registry:copy()", function()
2237 | local reg = ecr.registry()
2238 |
2239 | do CASE "copy"
2240 | local entities = {
2241 | reg:handle():set(A, "value").entity,
2242 | reg:handle():set(A, "value").entity,
2243 | reg:handle():set(A, "value").entity,
2244 | reg:handle():set(A, "value").entity,
2245 | }
2246 |
2247 | for id, b in reg:view(B) do
2248 | CHECK(false)
2249 | end
2250 |
2251 | reg:copy(A, B)
2252 |
2253 | for id, b in reg:view(B) do
2254 | CHECK(b == "value")
2255 | end
2256 |
2257 | for _, id in entities do
2258 | reg:destroy(id)
2259 | end
2260 | end
2261 |
2262 | do CASE "copies nil"
2263 | local e = reg:handle():set(B, "value")
2264 |
2265 | for id, b in reg:view(B) do
2266 | CHECK(b == "value")
2267 | end
2268 |
2269 | reg:copy(A, B)
2270 |
2271 | for id, b in reg:view(B) do
2272 | CHECK(false)
2273 | end
2274 | e:destroy()
2275 | end
2276 |
2277 | do CASE "copies tags"
2278 | local e = reg:handle():set(TA)
2279 |
2280 | for id, b in reg:view(TB) do
2281 | CHECK(false)
2282 | end
2283 |
2284 | reg:copy(TA, TB)
2285 |
2286 | local pass = false
2287 | for id, b in reg:view(TB) do
2288 | CHECK(reg:has(id, TB, TA))
2289 | pass = true
2290 | end
2291 | CHECK(pass)
2292 | e:destroy()
2293 | end
2294 |
2295 | do CASE "throws error if signal"
2296 | reg:handle():set(TA)
2297 | reg:on_add(TB):connect(function(a0: number, a1: nil) end)
2298 |
2299 | for id, b in reg:view(TB) do
2300 | CHECK(false)
2301 | end
2302 |
2303 | local ok = pcall(reg.copy, reg, TA, TB)
2304 | CHECK(not ok)
2305 | end
2306 | end)
2307 |
2308 | TEST("ecr.component()", function()
2309 | do CASE "components have unique ids"
2310 | local cache = {}
2311 | for i = 1, 1000 do
2312 | local c = ecr.component()
2313 | CHECK(cache[c] == nil)
2314 | cache[c] = true
2315 | end
2316 | end
2317 | end)
2318 |
2319 | TEST("ecr.queue()", function()
2320 | do
2321 | local queue = ecr.queue()
2322 |
2323 | do CASE "iterate single value"
2324 | for i = 1, 10 do
2325 | queue:add(i)
2326 | end
2327 |
2328 | CHECK(#queue == 10)
2329 |
2330 | local i = 0
2331 | for v in queue do
2332 | i += 1
2333 | CHECK(v == i)
2334 | end
2335 |
2336 | CHECK(#queue == 0)
2337 | CHECK(i == 10)
2338 | end
2339 |
2340 | do CASE "iterate multiple values"
2341 | for i = 1, 10 do
2342 | (queue :: any):add(i+1, i+2, i+3)
2343 | end
2344 |
2345 | CHECK(#queue == 10)
2346 |
2347 | local i = 0
2348 | for a, b, c in queue do
2349 | i += 1
2350 | CHECK(a == i+1)
2351 | CHECK(b == i+2)
2352 | CHECK(c == i+3)
2353 | end
2354 |
2355 | CHECK(#queue == 0)
2356 | CHECK(i == 10)
2357 | end
2358 | end
2359 |
2360 | do CASE "new iter clear"
2361 | local queue = ecr.queue()
2362 | queue:add(1)
2363 | for _ in queue:iter() do end
2364 | CHECK(#queue == 0)
2365 | end
2366 |
2367 | do CASE "manual clear"
2368 | local queue = ecr.queue()
2369 | for i = 1, 10 do
2370 | queue:add(i, i+1, i+2)
2371 | end
2372 |
2373 | CHECK(#queue == 10)
2374 |
2375 | queue:clear()
2376 |
2377 | CHECK(#queue == 0)
2378 |
2379 | for _ in queue do
2380 | CHECK(false)
2381 | end
2382 | end
2383 |
2384 | do CASE "uneven values"
2385 | local queue = ecr.queue()
2386 | queue:add(1)
2387 | ;(queue :: any):add(1, 2, nil)
2388 | ;(queue :: any):add(1, nil, 3)
2389 |
2390 | local i = 0
2391 | for a, b, c in queue do
2392 | i += 1
2393 | if i == 1 then CHECK(a == 1 and b == nil and c == nil)
2394 | elseif i == 2 then CHECK(a == 1 and b == 2 and c == nil)
2395 | elseif i == 3 then CHECK(a == 1 and b == nil and c == 3)
2396 | else CHECK(false) end
2397 | end
2398 | end
2399 |
2400 | do CASE "nil value first"
2401 | local queue = ecr.queue()
2402 | CHECK_ERR("first argument cannot be nil", function()
2403 | queue:add(nil, 1, 2, 3)
2404 | end)
2405 | end
2406 |
2407 | do CASE "automatic signal connection"
2408 | local listener: (...any) -> ()
2409 | local signal = {
2410 | connect = function(_, fn)
2411 | listener = fn
2412 | end
2413 | }
2414 |
2415 | local queue = ecr.queue(signal)
2416 |
2417 | listener(1, 2, 3)
2418 |
2419 | CHECK(#queue == 1)
2420 | for a, b, c in queue do
2421 | CHECK(a == 1 and b == 2 and c == 3)
2422 | end
2423 | end
2424 | end)
2425 |
2426 | TEST("ecr.name()", function()
2427 | local cts = ecr.name {
2428 | A = ecr.component() :: number,
2429 | B = ecr.component() :: string
2430 | }
2431 |
2432 | local reg = ecr.registry()
2433 |
2434 | local id = reg:create()
2435 |
2436 | CHECK_ERR(`entity does not have component "A"`, function()
2437 | reg:get(id, cts.A)
2438 | end)
2439 |
2440 | CHECK_ERR(`no constructor defined for component "B"`, function()
2441 | reg:add(id, cts.B)
2442 | end)
2443 |
2444 | CHECK(ecr.name(cts.A) == "A")
2445 | end)
2446 |
2447 | TEST("ecr.tag()", function()
2448 | do CASE "is tag"
2449 | CHECK(ecr.is_tag(TA))
2450 | CHECK(not ecr.is_tag(A))
2451 | end
2452 |
2453 | local reg = ecr.registry()
2454 |
2455 | local observer = reg:track(TA, TB, TC, TD, A)
2456 |
2457 | local e = reg:handle()
2458 |
2459 | do CASE "add"
2460 | local ran = false
2461 | reg:on_add(TA):connect(function(id, v)
2462 | ran = true
2463 | CHECK(id == e.entity)
2464 | CHECK(v == nil)
2465 | end)
2466 |
2467 | e:add(TA)
2468 |
2469 | CHECK(e:has(TA))
2470 | CHECK(e:get(TA) == nil)
2471 | CHECK(ran)
2472 | end
2473 |
2474 | do CASE "view"
2475 | local i = 0
2476 | for id, tag in reg:view(TA) do
2477 | i += 1
2478 | CHECK(i == 1)
2479 | CHECK(tag == nil)
2480 | end
2481 | end
2482 |
2483 | -- multi-type views return values by unpacking a table
2484 | -- the concern here is since tag values are nil, there is potential for
2485 | -- undefined behavior with unpacking an array with holes
2486 | -- we pre-allocate the arrays so the above should not be an issue
2487 | do CASE "view unpack"
2488 | e:add(TB, TC, TD)
2489 | e:set(A, true)
2490 |
2491 | local ran = 0
2492 |
2493 | for id, ta, tb, tc, td, a in reg:view(TA, TB, TC, TD, A) do
2494 | ran += 1
2495 | CHECK(a == true)
2496 | end
2497 |
2498 | for id, ta, tb, tc, td, a in observer do
2499 | ran += 1
2500 | CHECK(a == true)
2501 | end
2502 |
2503 | for id, ta, tb, tc, td, a in reg:group(TA, TB, TC, TD, A) do
2504 | ran += 1
2505 | CHECK(a == true)
2506 | end
2507 |
2508 | CHECK(ran == 3)
2509 | end
2510 |
2511 | do CASE "remove"
2512 | e:remove(TA)
2513 | CHECK(not e:has(TA))
2514 | end
2515 |
2516 | -- tags are implemented as normal components internally
2517 | -- further testing should not be necessary
2518 | end)
2519 |
2520 | TEST("buffer helpers", function()
2521 | local reg = ecr.registry()
2522 |
2523 | local ids = BULK_CREATE_IDS(reg, 10)
2524 | local buf = buffer.create(10 * ecr.id_size)
2525 |
2526 | do CASE "array to buffer"
2527 | ecr.array_to_buffer(ids, 10, buf)
2528 | CHECK(true)
2529 | end
2530 |
2531 | do CASE "buffer to array"
2532 | local arr = table.create(10)
2533 | ecr.buffer_to_array(buf, 10, arr)
2534 | for i = 1, 10 do
2535 | CHECK(arr[i] == ids[i])
2536 | end
2537 | end
2538 |
2539 | do CASE "buffer to buffer"
2540 | buffer.fill(buf, 0, 0)
2541 | local pool = reg:storage(ecr.entity)
2542 | ecr.buffer_to_buffer(pool.entities, pool.size, buf)
2543 | local arr = ecr.buffer_to_array(buf, pool.size)
2544 | for i = 1, 10 do
2545 | CHECK(arr[i] == ids[i])
2546 | end
2547 | end
2548 | end)
2549 |
2550 | if DEFER_ID_REUSE then
2551 |
2552 | TEST("deferred id reuse", function()
2553 | do CASE "version overflow"
2554 | local reg2 = ecr.registry()
2555 |
2556 | reg2:release( reg2:create() ); -- ensure id internally exists
2557 |
2558 | SET_KEY_VERSION_AND_ENSURE_EXISTS(reg2, 1, MAX_VER - 1)
2559 |
2560 | -- verify version was set to MAX-1 correctly
2561 | local id = reg2:create()
2562 | CHECK(VER(id) == MAX_VER - 1)
2563 | CHECK(KEY(id) == 1)
2564 | -- trigger version increment
2565 | reg2:release(id)
2566 |
2567 | -- verify version is now MAX
2568 | id = reg2:create()
2569 | CHECK(VER(id) == MAX_VER)
2570 | CHECK(KEY(id) == 1)
2571 |
2572 | -- id deprecated
2573 | reg2:release(id)
2574 | CHECK(VER(id) == MAX_VER)
2575 |
2576 | -- should produce a new key and not recycle as the previous key is at MAX version
2577 | id = reg2:create()
2578 | CHECK(VER(id) == 1)
2579 | CHECK(KEY(id) == 2)
2580 | end
2581 |
2582 | do CASE "deprecated key"
2583 | local reg = ecr.registry()
2584 | CHECK(GET_KEY_VERSION(reg, KEY(CREATE_DEPRECATED_ID(reg))) == 0)
2585 | end
2586 |
2587 | do CASE "add component to deprecated id"
2588 | local reg = ecr.registry()
2589 |
2590 | CHECK_ERR("invalid entity", function()
2591 | local id = CREATE_DEPRECATED_ID(reg)
2592 | reg:add(id, A)
2593 | end)
2594 | end
2595 |
2596 | do CASE "add component to deprecated id"
2597 | local reg = ecr.registry()
2598 |
2599 | CHECK_ERR("invalid entity", function()
2600 | local id = CREATE_DEPRECATED_ID(reg)
2601 | reg:set(id, A, 1)
2602 | end)
2603 | end
2604 |
2605 | do CASE "reuse"
2606 | local reg = ecr.registry()
2607 |
2608 | local ids = table.create(ecr._test.max_creatable)
2609 |
2610 | for i = 1, ecr._test.max_creatable do
2611 | ids[i] = reg:create(CREATE_ID(i, MAX_VER))
2612 | end
2613 |
2614 | for i = 1, ecr._test.max_creatable do
2615 | reg:release(ids[i])
2616 | end
2617 |
2618 | local id = reg:create()
2619 | CHECK(KEY(id) == 1)
2620 | CHECK(VER(id) == 1)
2621 | end
2622 | end)
2623 |
2624 | else
2625 |
2626 | TEST("immediate id reuse", function()
2627 | do CASE "reuse 1"
2628 | local reg = ecr.registry()
2629 |
2630 | for i = 1, MAX_VER do
2631 | reg:release(reg:create())
2632 | end
2633 |
2634 | local id = reg:create()
2635 | CHECK(KEY(id) == 1)
2636 | CHECK(VER(id) == 1)
2637 | end
2638 |
2639 | do CASE "clear"
2640 | local reg = ecr.registry()
2641 | local ids = BULK_CREATE_IDS(reg, ecr._test.max_creatable)
2642 |
2643 | local chosen = ecr._test.max_creatable // 2
2644 | local chosen_key = KEY(ids[chosen])
2645 |
2646 | reg:release(ids[chosen])
2647 | SET_KEY_VERSION_AND_ENSURE_EXISTS(reg, KEY(ids[chosen]), ecr._test.max_ver)
2648 | CHECK(reg:create() == ecr._test.create_id(chosen_key, ecr._test.max_ver))
2649 |
2650 | reg:clear()
2651 |
2652 | local ids_new = BULK_CREATE_IDS(reg, ecr._test.max_creatable)
2653 |
2654 | for i = 1, ecr._test.max_creatable do
2655 | local id = ids[i]
2656 | local id_new = ids_new[i]
2657 |
2658 | if KEY(id_new) == chosen_key then
2659 | CHECK(VER(id_new) == VER(id))
2660 | else
2661 | CHECK(VER(id) + 1 == VER(id_new))
2662 | end
2663 | end
2664 | end
2665 | end)
2666 |
2667 | end
2668 |
2669 | TEST("random test", function()
2670 | --[[
2671 |
2672 | This test simulates modifying a registry through a sequence of systems
2673 | by randomly creating, destroying, adding, updating, removing entities
2674 | and their components.
2675 |
2676 | Each component value contains the id of the entity that owns it as well
2677 | as a counter that is maintained separately from the registry.
2678 |
2679 | To check if the case passes we check if the two independent counters
2680 | match up and that the id in the component corresponds to the entity that
2681 | owns it.
2682 |
2683 | ]]
2684 |
2685 | -- map component to entity to value
2686 | local incs = {} :: Map>
2687 |
2688 | local cts = {} :: Array<{ id: ecr.entity, v: number }>
2689 |
2690 | for i = 1, 5 do
2691 | local ctype = ecr.component()
2692 | cts[i] = ctype :: any
2693 | incs[ctype] = {}
2694 | end
2695 |
2696 | local reg = ecr.registry()
2697 |
2698 | local function flip(p: number?)
2699 | return math.random() > (p and (1 - p) or 0.5)
2700 | end
2701 |
2702 | -- set of all ids in use
2703 | local idcount = 0
2704 | local ids = {} :: { [ecr.entity]: true }
2705 |
2706 | -- array of all destroyed ids
2707 | local frees = {} :: { ecr.entity }
2708 |
2709 |
2710 | local group1 = reg:group(cts[1], cts[3])
2711 | local group2 = reg:group(cts[2], cts[4], cts[5])
2712 |
2713 | for i = 1, N do
2714 | if flip(0.6) then
2715 | local id = reg:create()
2716 | ids[id] = true
2717 | idcount += 1
2718 | end
2719 |
2720 | do
2721 | local id = table.remove(frees)
2722 | if flip(0.2) and id then
2723 | local ok = pcall(reg.create, reg, id)
2724 | ids[id] = ok or nil :: any
2725 | idcount += 1
2726 | end
2727 | end
2728 |
2729 | for _, ctype in cts do
2730 | local inc = incs[ctype]
2731 |
2732 | do -- test iterators
2733 | local ctype2
2734 | local ctype3
2735 |
2736 | repeat
2737 | ctype2 = cts[math.random(1, #cts)]
2738 | until ctype2 ~= ctype
2739 |
2740 | if flip(0.3) then
2741 | repeat
2742 | ctype3 = cts[math.random(1, #cts)]
2743 | until ctype3 ~= ctype2 and ctype3 ~= ctype
2744 | end
2745 |
2746 | if ctype3 then
2747 | for _ in reg:view(ctype, ctype2, ctype3) do end
2748 | else
2749 | for _ in reg:view(ctype, ctype2) do end
2750 | end
2751 | end
2752 |
2753 | for id in ids do
2754 | -- add component
2755 | if flip() and not inc[id] then
2756 | inc[id] = 0
2757 | --
2758 | if not reg:try_get(id, ctype) then
2759 | reg:set(id, ctype, { id = id, v = 0 })
2760 | end
2761 | end
2762 |
2763 | -- increment component
2764 | if flip() and inc[id] then
2765 | inc[id] += 1
2766 | --
2767 | reg:patch(id, ctype, function(old)
2768 | return { id = old.id, v = old.v + 1 }
2769 | end)
2770 | end
2771 |
2772 | -- decrement component
2773 | if flip(0.2) and inc[id] then
2774 | inc[id] -= 1
2775 | --
2776 | local old = reg:get(id, ctype)
2777 | reg:set(id, ctype, { id = old.id, v = old.v - 1 })
2778 | end
2779 |
2780 | -- remove component
2781 | if flip(0.1) and inc[id] then
2782 | inc[id] = nil
2783 | --
2784 | reg:remove(id, ctype)
2785 | end
2786 | end
2787 | end
2788 |
2789 | for id in ids do
2790 | -- destroy id
2791 | if flip(0.005) then
2792 | ids[id] = nil
2793 | idcount -= 1
2794 | table.insert(frees, id)
2795 | for _, inc in incs do
2796 | inc[id] = nil
2797 | end
2798 | --
2799 | reg:destroy(id)
2800 | end
2801 | end
2802 | end
2803 |
2804 | local ran = 0
2805 |
2806 | for _, ctype in cts do
2807 | local inc = incs[ctype]
2808 |
2809 | for id in ids do
2810 | ran += 1
2811 | local component = reg:try_get(id, ctype) :: any
2812 | if inc[id] then
2813 | CHECK(component)
2814 | CHECK(inc[id] == component.v)
2815 | CHECK(component.id == id)
2816 | else
2817 | CHECK(not component)
2818 | end
2819 | end
2820 | end
2821 |
2822 | for id in reg:view(ecr.entity) do
2823 | CHECK(ids[id])
2824 | end
2825 |
2826 | CHECK(#group1 > 0 and #group2 > 0)
2827 | CHECK(ran > 0)
2828 | CHECK(idcount > 50)
2829 | end)
2830 |
2831 | local success = FINISH()
2832 | if not success then error(nil, 0) end
2833 |
--------------------------------------------------------------------------------