Compare commits
No commits in common. "main" and "0.5.5" have entirely different histories.
2
.gitattributes
vendored
|
@ -1,2 +0,0 @@
|
|||
*.luau text eol=lf
|
||||
*.html linguist-vendored
|
9
.github/ISSUE_TEMPLATE/BUG-REPORT.md
vendored
Executable file → Normal file
|
@ -1,25 +1,22 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: File a bug report for any behavior that you believe is unintentional or problematic
|
||||
title: ""
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ""
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe the bug
|
||||
|
||||
Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue.
|
||||
|
||||
This is a good place to put rbxl files or scripts that help explain your reproduction steps.
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
What you expect to happen
|
||||
|
||||
## Actual Behavior
|
||||
|
||||
What actually happens
|
7
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
Executable file → Normal file
|
@ -1,15 +1,14 @@
|
|||
---
|
||||
name: Documentation
|
||||
about: Open an issue to add, change, or otherwise modify any part of the documentation.
|
||||
title: ""
|
||||
title: "[DOCS]"
|
||||
labels: documentation
|
||||
assignees: ""
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Which Sections Does This Issue Cover?
|
||||
|
||||
[Put sections (e.g. Query Concepts), page links, etc as necessary]
|
||||
|
||||
## What Needs To Change?
|
||||
|
||||
What specifically needs to change and what suggestions do you have to change it?
|
6
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
vendored
Executable file → Normal file
|
@ -1,9 +1,10 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: File a feature request for something you believe should be added to Jecs
|
||||
title: ""
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe your Feature
|
||||
|
@ -21,7 +22,6 @@ What other alternative implementations or otherwise relevant information is impo
|
|||
## Considerations
|
||||
|
||||
Some questions that need to be answered include the following:
|
||||
|
||||
- Will old code break in response to this feature?
|
||||
- What are the performance impacts with this feature (if any)?
|
||||
- How is it useful to include?
|
||||
|
|
0
.github/PULL_REQUEST_TEMPLATE.md
vendored
Executable file → Normal file
0
.github/workflows/analysis.yaml
vendored
Executable file → Normal file
0
.github/workflows/dependabot.yml
vendored
Executable file → Normal file
0
.github/workflows/deploy-docs.yaml
vendored
Executable file → Normal file
0
.github/workflows/publish-npm.yml
vendored
Executable file → Normal file
6
.github/workflows/release.yaml
vendored
Executable file → Normal file
|
@ -2,7 +2,7 @@ name: release
|
|||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*", "workflow_dispatch"]
|
||||
tags: ["v*"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -22,7 +22,7 @@ jobs:
|
|||
run: rojo build --output build.rbxm default.project.json
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build.rbxm
|
||||
|
@ -38,7 +38,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Jecs Build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
|
2
.github/workflows/unit-testing.yaml
vendored
Executable file → Normal file
|
@ -15,7 +15,7 @@ jobs:
|
|||
- name: Install Luau
|
||||
uses: encodedvenom/install-luau@v4.3
|
||||
with:
|
||||
version: "0.667"
|
||||
version: "latest"
|
||||
verbose: "true"
|
||||
|
||||
- name: Run Unit Tests
|
||||
|
|
6
.gitignore
vendored
Executable file → Normal file
|
@ -65,9 +65,3 @@ drafts/
|
|||
|
||||
# Luau tools
|
||||
profile.*
|
||||
|
||||
# Patch files
|
||||
|
||||
*.patch
|
||||
|
||||
genhtml.perl
|
||||
|
|
6
.luaurc
Executable file → Normal file
|
@ -1,10 +1,8 @@
|
|||
{
|
||||
"aliases": {
|
||||
"jecs": "jecs",
|
||||
"testkit": "tools/testkit",
|
||||
"mirror": "mirror",
|
||||
"tools": "tools",
|
||||
"addons": "addons"
|
||||
"testkit": "test/testkit",
|
||||
"mirror": "mirror"
|
||||
},
|
||||
"languageMode": "strict"
|
||||
}
|
||||
|
|
0
.prettierrc
Executable file → Normal file
341
CHANGELOG.md
Executable file → Normal file
|
@ -1,190 +1,205 @@
|
|||
# Changelog
|
||||
# Jecs Changelog
|
||||
|
||||
## Unreleased
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 0.7.0
|
||||
The format is based on [Keep a Changelog][kac], and this project adheres to
|
||||
[Semantic Versioning][semver].
|
||||
|
||||
[kac]: https://keepachangelog.com/en/1.1.0/
|
||||
[semver]: https://semver.org/spec/v2.0.0.html
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- `[world]`:
|
||||
- 16% faster `world:get`
|
||||
- `world:has` no longer typechecks components after the 8th one.
|
||||
- `[typescript]`
|
||||
|
||||
- Fixed Entity type to default to `undefined | unknown` instead of just `undefined`
|
||||
|
||||
- `[query]`:
|
||||
- Fixed bug where `world:clear` did not invoke `jecs.OnRemove` hooks
|
||||
- Changed `query.__iter` to drain on iteration
|
||||
- It will initialize once wherever you left iteration off at last time
|
||||
- Changed `query:iter` to restart the iterator
|
||||
- Removed `query:drain` and `query:next`
|
||||
- If you want to get individual results outside of a for-loop, you need to call `query:iter` to initialize the iterator and then call the iterator function manually
|
||||
```lua
|
||||
local it = world:query(A, B, C):iter()
|
||||
local entity, a, b, c = it()
|
||||
entity, a, b, c = it() -- get next results
|
||||
```
|
||||
- `[world`
|
||||
- Fixed a bug with `world:clear` not invoking `jecs.OnRemove` hooks
|
||||
- `[typescript]`:
|
||||
- Changed pair to accept generics
|
||||
- Improved handling of Tags
|
||||
|
||||
## [0.3.2] - 2024-10-01
|
||||
|
||||
- `[world]`:
|
||||
- Changed `world:cleanup` to traverse a header type for graph edges. (Edit)
|
||||
- Fixed a regression that occurred when you call `world:set` following a `world:remove` using the same component
|
||||
- Remove explicit error in JECS_DEBUG for `world:target` when not applying an index parameter
|
||||
- `[typescript]` :
|
||||
- Fixed `world.set` with NoInfer<T>
|
||||
|
||||
## [0.3.1] - 2024-10-01
|
||||
|
||||
- `[world]`:
|
||||
- Added an index parameter to `world:target`
|
||||
- Added a way to change the components limit via `_G.JECS_HI_COMPONENT_ID`
|
||||
- Set it to whatever number you want but try to make it as close to the number of components you will use as possible
|
||||
- Make sure to set this before importing jecs or else it will not work
|
||||
- Added debug mode, enable via setting `_G.JECS_DEBUG` to true
|
||||
- Make sure to set this before importing jecs or else it will not work
|
||||
- Added `world:cleanup` which is called to cleanup empty archetypes manually
|
||||
- Changed `world:delete` to delete archetypes that are dependent on the passed entity
|
||||
- Changed `world:delete` to delete entity's children before the entity to prevent cycles
|
||||
- `[query]`:
|
||||
- Fixed the iterator to not drain by default
|
||||
- `[typescript]`
|
||||
- Fixed entry point of the package.json file to be `src` rather than `src/init`
|
||||
- Fixed `query.next` returning a query object whereas it would be expected to return a tuple containing the entity and the corresponding component values
|
||||
- Exported `query.archetypes`
|
||||
- Changed `pair` to return a number instead of an entity
|
||||
- Preventing direct usage of a pair as an entity while still allowing it to be used as a component
|
||||
- Exported built-in components `ChildOf` and `Name`
|
||||
- Exported `world.parent`
|
||||
|
||||
## [0.2.10] - 2024-09-07
|
||||
|
||||
- `[world]`:
|
||||
- Improved performance for hooks
|
||||
- Changed `world:set` to be idempotent when setting tags
|
||||
- `[traits]`:
|
||||
- Added cleanup condition `jecs.OnDelete` for when the entity or component is deleted
|
||||
- Added cleanup action `jecs.Remove` which removes instances of the specified (component) id from all entities
|
||||
- This is the default cleanup action
|
||||
- Added component trait `jecs.Tag` which allows for zero-cost components used as tags
|
||||
- Setting data to a component with this trait will do nothing
|
||||
- `[luau]`:
|
||||
- Exported `world:contains()`
|
||||
- Exported `query:drain()`
|
||||
- Exported `Query`
|
||||
- Improved types for the hook `OnAdd`, `OnSet`, `OnRemove`
|
||||
- Changed functions to accept any ID including pairs in type parameters
|
||||
- Applies to `world:add()`, `world:set()`, `world:remove()`, `world:get()`, `world:has()` and `world:query()`
|
||||
- New exported type `Id<T = nil> = Entity<T> | Pair`
|
||||
- Changed `world:contains()` to return a `boolean` instead of an entity which may or may not exist
|
||||
- Fixed `world:has()` to take the correct parameters
|
||||
|
||||
## [0.2.2] - 2024-07-07
|
||||
|
||||
### Added
|
||||
- `jecs.component_record` for retrieving the component_record of a component.
|
||||
- `Column<T>` and `ColumnsMap<T>` types for typescript.
|
||||
- `bulk_insert` and `bulk_remove` respectively for moving an entity to an archetype without intermediate steps.
|
||||
|
||||
- Added `query:replace(function(...T) return ...U end)` for replacing components in place
|
||||
- Method is fast pathed to replace the data to the components for each corresponding entity
|
||||
|
||||
### Changed
|
||||
- The fields `archetype.records[id]` and `archetype.counts[id` have been removed from the archetype struct and been moved to the component record `component_index[id].records[archetype.id]` and `component_index[id].counts[archetype.id]` respectively.
|
||||
- Removed the metatable `jecs.World`. Use `jecs.world()` to create your World.
|
||||
- Archetypes will no longer be garbage collected when invalidated, allowing them to be recycled to save a lot of performance during frequent deletion.
|
||||
- Removed `jecs.entity_index_try_get_fast`. Use `jecs.entity_index_try_get` instead.
|
||||
|
||||
## 0.6.1
|
||||
- Iterator now goes backwards instead to prevent common cases of iterator invalidation
|
||||
|
||||
## [0.2.1] - 2024-07-06
|
||||
|
||||
### Added
|
||||
|
||||
- Added `jecs.Component` built-in component which will be added to ids created with `world:component()`.
|
||||
- Used to find every component id with `query(jecs.Component)
|
||||
|
||||
## [0.2.0] - 2024-07-03
|
||||
|
||||
### Added
|
||||
|
||||
- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships.
|
||||
- Give a parent to an entity with `world:add($source, pair(ChildOf, $target))`
|
||||
- Use `world:parent(entity)` to find the target of the relationship
|
||||
- Added user-facing Luau types
|
||||
|
||||
### Changed
|
||||
- Entity types now unions with numbers should allow for easier time casting while not causing regressing previous behaviours
|
||||
|
||||
- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream:
|
||||
|
||||
## [0.1.1] - 2024-05-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added `world:clear(entity)` for removing the components to the corresponding entity
|
||||
- Added Typescript Types
|
||||
|
||||
## [0.1.0] - 2024-05-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Optimized iterator
|
||||
|
||||
## [0.1.0-rc.6] - 2024-05-13
|
||||
|
||||
### Added
|
||||
|
||||
- Added a `jecs.Wildcard` term
|
||||
- it lets you query any partially matched pairs
|
||||
|
||||
## [0.1.0-rc.5] - 2024-05-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added Entity relationships for creating logical connections between entities
|
||||
- Added `world:__iter method` which allows for iteration over the whole world to get every entity
|
||||
- used for reconciling whole worlds such as via replication, saving/loading, etc
|
||||
- Added `world:add(entity, component)` which adds a component to the entity
|
||||
- it is an idempotent function, so calling it twice and in any order should be fine
|
||||
|
||||
### Fixed
|
||||
- Fixed a critical bug with `(*, R)` pairs not being removed when `R` is deleted
|
||||
|
||||
## 0.6.0
|
||||
- Fixed component overriding when in disorder
|
||||
- Previously setting the components in different order results in it overriding component data because it incorrectly mapped the index of the column. So it took the index from the source archetype rather than the destination archetype
|
||||
|
||||
## [0.0.0-prototype.rc.3] - 2024-05-01
|
||||
|
||||
### Added
|
||||
- `World:range` to restrict entity range to allow for e.g. reserving ids `1000..5000` for clients and everything above that (5000+) for entities from the server. This makes it possible to receive ids from a server that don't have to be mapped to local ids.
|
||||
- `jecs.component`, `jecs.tag` and `jecs.meta` for preregistering ids and their metadata before a world
|
||||
- Overload to `World:entity` to create an entity at the desired id.
|
||||
|
||||
- Added observers
|
||||
- Added an arm to query `query:without()` for chaining invariants.
|
||||
|
||||
### Changed
|
||||
- `World:clear` to remove the `ID` from every entity instead of the previous behaviour of removing all of the components on the entity. You should prefer deleting the entity instead for the previous behaviour.
|
||||
- Entity ID layouts by putting the index in the lower bits, which should make every world function 1–5 nanoseconds faster.
|
||||
- Hooks now pass the full component ID which is useful for pairs when you need both the relation and the target.
|
||||
- Replaced `OnSet` with `OnChange`, which now only runs when the component ID was previously present on the entity.
|
||||
- `OnAdd` now runs after the value has been set and also passes the component ID and the value.
|
||||
- `OnRemove` now lazily looks up which archetype the entity will move to. This is meant to support interior structural changes within every hook.
|
||||
- Optimized `world:has` for both single and multiple component presence. This comes at the cost that it cannot check the component presence for more than 4 components at a time. If this is important, consider calling this function multiple times.
|
||||
|
||||
### Fixed
|
||||
- `World:delete` not removing every pair with an unalive target. Specifically happened when you had at least two pairs of different relations with multiple targets each.
|
||||
- Separates ranges for components and entity IDs.
|
||||
|
||||
## 0.5.0
|
||||
- IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` which is a significant boost
|
||||
|
||||
### Added
|
||||
- `World:each` to find entities with a specific Tag.
|
||||
- `World:children` to find children of an entity.
|
||||
- `Query:cached` to add a query cache that updates itself when an archetype matching the query gets created or deleted.
|
||||
- No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals
|
||||
- This was an issue with the iterator being invalidated when you move an entity to a different archetype.
|
||||
|
||||
### Fixedhttps://github.com/Ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3
|
||||
|
||||
- Fixed a bug where changing an existing component would be slow because it was always appending changing the row of the entity record
|
||||
- The fix dramatically improves times where it is basically down to just the speed of setting a field in a table
|
||||
|
||||
## [0.0.0-prototype.rc.2] - 2024-04-26
|
||||
|
||||
### Changed
|
||||
- Inference of entities' types using user-defined type functions.
|
||||
- `Pair<First, Second>` to return `Second` if `First` is a `Tag`; otherwise, returns `First`.
|
||||
|
||||
### Fixed
|
||||
- `World:target` not giving adjacent pairs.
|
||||
- Optimized the creation of the query
|
||||
- It will now finds the smallest archetype map to iterate over
|
||||
- Optimized the query iterator
|
||||
|
||||
## 0.4.0
|
||||
- It will now populates iterator with columns for faster indexing
|
||||
|
||||
### Added
|
||||
- Recycling support to `world:entity` so reused entity IDs now increment generation.
|
||||
- Renamed the insertion method from world:add to world:set to better reflect what it does.
|
||||
|
||||
### Removed
|
||||
- `Query:drain`
|
||||
- `Query:next`
|
||||
- `Query:replace`
|
||||
## [0.0.0-prototype.rc.2] - 2024-04-23
|
||||
|
||||
### Changed
|
||||
- `jecs.Pair` type in Luau now returns the first element's type to avoid manual typecasting.
|
||||
- Initial release
|
||||
|
||||
### Fixed
|
||||
- `Query:archetypes` now correctly takes `self`.
|
||||
|
||||
## 0.3.2 - 2024-10-01
|
||||
|
||||
### Changed
|
||||
- `World:cleanup` to traverse a header type for graph edges.
|
||||
|
||||
### Fixed
|
||||
- Regression when calling `World:set` after `World:remove` on the same component.
|
||||
- Removed explicit error in `JECS_DEBUG` for `World:target` missing index.
|
||||
- `World.set` type inference with `NoInfer<T>` in TypeScript.
|
||||
|
||||
## 0.3.1 - 2024-10-01
|
||||
|
||||
### Added
|
||||
- Index parameter to `World:target`.
|
||||
- Global config `_G.JECS_HI_COMPONENT_ID` to change component ID limit (must be set before importing JECS).
|
||||
- Debug mode via `_G.JECS_DEBUG` (must be set before importing JECS).
|
||||
- `world:cleanup` to manually clean up empty archetypes.
|
||||
|
||||
### Changed
|
||||
- `world:delete` now also deletes dependent archetypes and child entities.
|
||||
|
||||
### Fixed
|
||||
- `Query` iterator to not drain by default.
|
||||
- TypeScript package entry to `src` instead of `src/init`.
|
||||
- `Query.next` now returns expected result tuple in typescript.
|
||||
- `pair` returns a number instead of entity to prevent misuse.
|
||||
- Exported built-in components `ChildOf`, `Name`, and `world.parent`.
|
||||
|
||||
## 0.2.10
|
||||
|
||||
### Added
|
||||
- Trait `jecs.Tag` for zero-cost tag components.
|
||||
- Cleanup conditions: `jecs.OnDelete`, `jecs.Remove`.
|
||||
|
||||
### Changed
|
||||
- `world:set` is now idempotent when setting tags.
|
||||
|
||||
### Fixed
|
||||
- Improved performance for hooks.
|
||||
- Exported types and functions: `world:contains()`, `query:drain()`, `Query`.
|
||||
- Hook types: `OnAdd`, `OnSet`, `OnRemove`.
|
||||
- ID flexibility for `add`, `set`, `remove`, `get`, `has`, `query`.
|
||||
- `world:contains()` now returns `boolean`.
|
||||
- `world:has()` parameters now correct.
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Added
|
||||
- `query:replace(fn)` for in-place replacement of component values.
|
||||
|
||||
### Changed
|
||||
- Iterator now goes backwards to avoid invalidation during iteration.
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Added
|
||||
- Built-in `jecs.Component` used to find all component IDs.
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Added
|
||||
- `world:parent(entity)` and `jecs.ChildOf` for parent-child relationships.
|
||||
|
||||
### Changed
|
||||
- Iteration performance improved by 20–40% through manual indexing.
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Added
|
||||
- `world:clear(entity)` for removing all components from an entity.
|
||||
- TypeScript types.
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Changed
|
||||
- Optimized iterator.
|
||||
|
||||
## 0.1.0-rc.6
|
||||
|
||||
### Added
|
||||
- `jecs.Wildcard` term to query partially matched pairs.
|
||||
|
||||
## 0.1.0-rc.5
|
||||
|
||||
### Added
|
||||
- Entity relationships.
|
||||
- `world:__iter()` for full world iteration.
|
||||
- `world:add(entity, component)` (idempotent).
|
||||
|
||||
### Fixed
|
||||
- Component overriding when set out of order.
|
||||
|
||||
## 0.0.0-prototype.rc.3
|
||||
|
||||
### Added
|
||||
- Observers.
|
||||
- `query:without()` for invariant queries.
|
||||
|
||||
### Changed
|
||||
- Separate ID ranges for entities and components.
|
||||
- Avoid caching pointers; cache stable column indices instead.
|
||||
|
||||
### Fixed
|
||||
- Slow component updates due to unnecessary row changes.
|
||||
|
||||
## 0.0.0-prototype.rc.2 - 2024-04-26
|
||||
|
||||
### Changed
|
||||
- Query now uses smallest archetype map.
|
||||
- Optimized query iterator.
|
||||
- Renamed `world:add` to `world:set`.
|
||||
|
||||
## 0.0.0-prototype.rc.1
|
||||
|
||||
### Added
|
||||
- Initial release.
|
||||
[unreleased]: https://github.com/ukendio/jecs/compare/v0.0.0.0-prototype.rc.2...HEAD
|
||||
[0.2.2]: https://github.com/ukendio/jecs/releases/tag/v0.2.2
|
||||
[0.2.1]: https://github.com/ukendio/jecs/releases/tag/v0.2.1
|
||||
[0.2.0]: https://github.com/ukendio/jecs/releases/tag/v0.2.0
|
||||
[0.1.1]: https://github.com/ukendio/jecs/releases/tag/v0.1.1
|
||||
[0.1.0]: https://github.com/ukendio/jecs/releases/tag/v0.1.0
|
||||
[0.1.0-rc.6]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.6
|
||||
[0.1.0-rc.5]: https://github.com/ukendio/jecs/releases/tag/v0.1.0-rc.5
|
||||
[0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3
|
||||
[0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2
|
||||
[0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1
|
||||
|
|
0
LICENSE
Executable file → Normal file
25
README.md
Executable file → Normal file
|
@ -2,7 +2,7 @@
|
|||
<img src="assets/image-5.png" width=35%/>
|
||||
</p>
|
||||
|
||||
[](LICENSE) [](https://wally.run/package/ukendio/jecs) [](https://github.com/Ukendio/jecs/actions/workflows/unit-testing.yaml)
|
||||
[](LICENSE) [](https://wally.run/package/ukendio/jecs)
|
||||
|
||||
Just a stupidly fast Entity Component System
|
||||
|
||||
|
@ -12,22 +12,7 @@ Just a stupidly fast Entity Component System
|
|||
- Zero-dependency package
|
||||
- Optimized for column-major operations
|
||||
- Cache friendly [archetype/SoA](https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9) storage
|
||||
- Rigorously [unit tested](https://github.com/Ukendio/jecs/actions/workflows/unit-testing.yaml) for stability
|
||||
|
||||
### Installation
|
||||
|
||||
With [Wally](https://wally.run/):
|
||||
```bash
|
||||
jecs = "ukendio/jecs@0.6.0" # Inside wally.toml
|
||||
```
|
||||
With [pesde](https://pesde.dev/):
|
||||
```bash
|
||||
pesde add wally#ukendio/jecs@0.6.0
|
||||
```
|
||||
With [npm](https://www.npmjs.com/package/@rbxts/jecs) ([roblox-ts](https://roblox-ts.com/)):
|
||||
```bash
|
||||
npm i @rbxts/jecs
|
||||
```
|
||||
- Rigorously [unit tested](https://github.com/Ukendio/jecs/actions/workflows/ci.yaml) for stability
|
||||
|
||||
### Example
|
||||
|
||||
|
@ -60,8 +45,8 @@ world:set(sara, Name, "sara")
|
|||
|
||||
print(getName(parent(sara)))
|
||||
|
||||
for e, name in world:query(Name, pair(ChildOf, alice)) do
|
||||
print(name, "is the child of alice")
|
||||
for e in world:query(pair(ChildOf, alice)) do
|
||||
print(getName(e), "is the child of alice")
|
||||
end
|
||||
|
||||
-- Output
|
||||
|
@ -70,8 +55,6 @@ end
|
|||
-- sara is the child of alice
|
||||
```
|
||||
|
||||
### Benchmarks
|
||||
|
||||
21,000 entities 125 archetypes 4 random components queried.
|
||||

|
||||
Can be found under /benches/visual/query.luau
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
local jecs = require("@jecs")
|
||||
|
||||
export type PatchedWorld = jecs.World & {
|
||||
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
|
||||
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (),
|
||||
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
|
||||
observer: (
|
||||
PatchedWorld,
|
||||
any,
|
||||
(jecs.Entity) -> ()
|
||||
) -> (),
|
||||
monitor: (
|
||||
PatchedWorld,
|
||||
any,
|
||||
(jecs.Entity, jecs.Id) -> ()
|
||||
) -> ()
|
||||
}
|
||||
|
||||
local function observers_new(world, query, callback)
|
||||
local terms = query.filter_with :: { jecs.Id }
|
||||
if not terms then
|
||||
local ids = query.ids
|
||||
query.filter_with = ids
|
||||
terms = ids
|
||||
end
|
||||
|
||||
local entity_index = world.entity_index :: any
|
||||
local function emplaced(entity, id, value)
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any)
|
||||
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
local archetype = r.archetype
|
||||
|
||||
if jecs.query_match(query, archetype) then
|
||||
callback(entity)
|
||||
end
|
||||
end
|
||||
|
||||
for _, term in terms do
|
||||
world:added(term, emplaced)
|
||||
world:changed(term, emplaced)
|
||||
end
|
||||
end
|
||||
|
||||
local function join(world, component)
|
||||
local sparse_array = {}
|
||||
local dense_array = {}
|
||||
local values = {}
|
||||
local max_id = 0
|
||||
|
||||
world:added(component, function(entity, id, value)
|
||||
max_id += 1
|
||||
sparse_array[entity] = max_id
|
||||
dense_array[max_id] = entity
|
||||
values[max_id] = value
|
||||
end)
|
||||
|
||||
world:removed(component, function(entity, id)
|
||||
local e_swap = dense_array[max_id]
|
||||
local v_swap = values[max_id]
|
||||
|
||||
local dense = sparse_array[entity]
|
||||
dense_array[dense] = e_swap
|
||||
values[dense] = v_swap
|
||||
|
||||
sparse_array[entity] = nil
|
||||
dense_array[max_id] = nil
|
||||
values[max_id] = nil
|
||||
max_id -= 1
|
||||
end)
|
||||
|
||||
world:changed(component, function(entity, id, value)
|
||||
values[sparse_array[entity]] = value
|
||||
end)
|
||||
|
||||
return function()
|
||||
local i = max_id
|
||||
return function(): ...any
|
||||
i -= 1
|
||||
if i == 0 then
|
||||
return nil
|
||||
end
|
||||
local e = dense_array[i]
|
||||
return e, values[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function monitors_new(world, query, callback)
|
||||
local terms = query.filter_with :: { jecs.Id }
|
||||
if not terms then
|
||||
local ids = query.ids
|
||||
query.filter_with = ids
|
||||
terms = ids
|
||||
end
|
||||
|
||||
local entity_index = world.entity_index :: any
|
||||
local function emplaced(entity: jecs.Entity)
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any)
|
||||
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
local archetype = r.archetype
|
||||
|
||||
if jecs.query_match(query, archetype) then
|
||||
callback(entity, jecs.OnAdd)
|
||||
end
|
||||
end
|
||||
|
||||
local function removed(entity: jecs.Entity, component: jecs.Id)
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any)
|
||||
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
local archetype = r.archetype
|
||||
|
||||
if jecs.query_match(query, archetype) then
|
||||
local EcsOnRemove = jecs.OnRemove :: jecs.Id
|
||||
callback(entity, EcsOnRemove)
|
||||
end
|
||||
end
|
||||
|
||||
for _, term in terms do
|
||||
world:added(term, emplaced)
|
||||
world:removed(term, removed)
|
||||
end
|
||||
end
|
||||
|
||||
local function observers_add(world: jecs.World): PatchedWorld
|
||||
type Signal = { [jecs.Entity]: { (...any) -> () } }
|
||||
|
||||
local world_mut = world :: jecs.World & {[string]: any}
|
||||
|
||||
local signals = {
|
||||
added = {} :: Signal,
|
||||
emplaced = {} :: Signal,
|
||||
removed = {} :: Signal
|
||||
}
|
||||
|
||||
world_mut.added = function<T>(
|
||||
_: jecs.World,
|
||||
component: jecs.Id<T>,
|
||||
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
|
||||
)
|
||||
local listeners = signals.added[component]
|
||||
if not listeners then
|
||||
listeners = {}
|
||||
signals.added[component] = listeners
|
||||
|
||||
local function on_add(entity, id, value)
|
||||
for _, listener in listeners :: any do
|
||||
listener(entity, id, value)
|
||||
end
|
||||
end
|
||||
local existing_hook = world:get(component, jecs.OnAdd)
|
||||
if existing_hook then
|
||||
table.insert(listeners, existing_hook)
|
||||
end
|
||||
|
||||
local idr = world.component_index[component]
|
||||
if idr then
|
||||
idr.hooks.on_add = on_add
|
||||
else
|
||||
world:set(component, jecs.OnAdd, on_add)
|
||||
end
|
||||
end
|
||||
table.insert(listeners, fn)
|
||||
return function()
|
||||
local n = #listeners
|
||||
local i = table.find(listeners, fn)
|
||||
listeners[i] = listeners[n]
|
||||
listeners[n] = nil
|
||||
end
|
||||
end
|
||||
|
||||
world_mut.changed = function<T>(
|
||||
_: jecs.World,
|
||||
component: jecs.Id<T>,
|
||||
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
|
||||
)
|
||||
local listeners = signals.emplaced[component]
|
||||
if not listeners then
|
||||
listeners = {}
|
||||
signals.emplaced[component] = listeners
|
||||
local function on_change(entity, id, value: any)
|
||||
for _, listener in listeners :: any do
|
||||
listener(entity, id, value)
|
||||
end
|
||||
end
|
||||
local existing_hook = world:get(component, jecs.OnChange)
|
||||
if existing_hook then
|
||||
table.insert(listeners, existing_hook)
|
||||
end
|
||||
local idr = world.component_index[component]
|
||||
if idr then
|
||||
idr.hooks.on_change = on_change
|
||||
else
|
||||
world:set(component, jecs.OnChange, on_change)
|
||||
end
|
||||
end
|
||||
table.insert(listeners, fn)
|
||||
return function()
|
||||
local n = #listeners
|
||||
local i = table.find(listeners, fn)
|
||||
listeners[i] = listeners[n]
|
||||
listeners[n] = nil
|
||||
end
|
||||
end
|
||||
|
||||
world_mut.removed = function<T>(
|
||||
_: jecs.World,
|
||||
component: jecs.Id<T>,
|
||||
fn: (e: jecs.Entity, id: jecs.Id) -> ()
|
||||
)
|
||||
local listeners = signals.removed[component]
|
||||
if not listeners then
|
||||
listeners = {}
|
||||
signals.removed[component] = listeners
|
||||
local function on_remove(entity, id)
|
||||
for _, listener in listeners :: any do
|
||||
listener(entity, id)
|
||||
end
|
||||
end
|
||||
local existing_hook = world:get(component, jecs.OnRemove)
|
||||
if existing_hook then
|
||||
table.insert(listeners, existing_hook)
|
||||
end
|
||||
|
||||
local idr = world.component_index[component]
|
||||
if idr then
|
||||
idr.hooks.on_remove = on_remove
|
||||
else
|
||||
world:set(component, jecs.OnRemove, on_remove)
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(listeners, fn)
|
||||
|
||||
return function()
|
||||
local n = #listeners
|
||||
local i = table.find(listeners, fn)
|
||||
listeners[i] = listeners[n]
|
||||
listeners[n] = nil
|
||||
end
|
||||
end
|
||||
|
||||
world_mut.signals = signals
|
||||
|
||||
world_mut.observer = observers_new
|
||||
|
||||
world_mut.monitor = monitors_new
|
||||
|
||||
world_mut.trackers = {}
|
||||
|
||||
return world_mut :: PatchedWorld
|
||||
end
|
||||
|
||||
return observers_add
|
0
assets/image-1.png
Executable file → Normal file
Before Width: | Height: | Size: 392 KiB After Width: | Height: | Size: 392 KiB |
0
assets/image-2.png
Executable file → Normal file
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 391 KiB |
0
assets/image-3.png
Executable file → Normal file
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
0
assets/image-4.png
Executable file → Normal file
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
0
assets/image-5.png
Executable file → Normal file
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
0
assets/jecs_darkmode.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
assets/jecs_lightmode.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
assets/logo_old.png
Executable file → Normal file
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
0
bench.project.json
Executable file → Normal file
2
benches/cached.luau
Executable file → Normal file
|
@ -1,5 +1,5 @@
|
|||
local jecs = require("@jecs")
|
||||
local mirror = require("@mirror")
|
||||
local mirror = require("../mirror/init")
|
||||
|
||||
type i53 = number
|
||||
|
||||
|
|
48
benches/general.luau
Executable file → Normal file
|
@ -14,7 +14,7 @@ local pair = jecs.pair
|
|||
|
||||
do
|
||||
TITLE("create")
|
||||
local world = jecs.world()
|
||||
local world = jecs.World.new()
|
||||
|
||||
BENCH("entity", function()
|
||||
for i = 1, START(N) do
|
||||
|
@ -35,8 +35,8 @@ end
|
|||
do
|
||||
TITLE("set")
|
||||
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local world = jecs.World.new()
|
||||
local A = world:entity()
|
||||
|
||||
local entities = table.create(N)
|
||||
|
||||
|
@ -69,8 +69,8 @@ end
|
|||
do
|
||||
TITLE("set relationship")
|
||||
|
||||
local world = jecs.world()
|
||||
local A = world:component()
|
||||
local world = jecs.World.new()
|
||||
local A = world:entity()
|
||||
|
||||
local entities = table.create(N)
|
||||
|
||||
|
@ -103,7 +103,7 @@ end
|
|||
do
|
||||
TITLE("get")
|
||||
|
||||
local world = jecs.world()
|
||||
local world = jecs.World.new()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
@ -147,7 +147,7 @@ do
|
|||
TITLE("target")
|
||||
|
||||
BENCH("1st target", function()
|
||||
local world = jecs.world()
|
||||
local world = jecs.World.new()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
@ -179,7 +179,7 @@ do
|
|||
|
||||
local function view_bench(n: number)
|
||||
BENCH(`{n} entities per archetype`, function()
|
||||
local world = jecs.world()
|
||||
local world = jecs.World.new()
|
||||
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
|
@ -194,18 +194,17 @@ do
|
|||
world:set(id, B, true)
|
||||
world:set(id, C, true)
|
||||
world:set(id, D, true)
|
||||
world:add(id, ct)
|
||||
world:set(id, ct, true)
|
||||
end
|
||||
end
|
||||
|
||||
local q = world:query(A, B, C, D)
|
||||
START()
|
||||
for id in q do
|
||||
for id in world:query(A, B, C, D) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH(`inlined query`, function()
|
||||
local world = jecs.world()
|
||||
local world = jecs.World.new()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
|
@ -219,28 +218,17 @@ do
|
|||
world:set(id, B, true)
|
||||
world:set(id, C, true)
|
||||
world:set(id, D, true)
|
||||
world:add(id, ct)
|
||||
world:set(id, ct, true)
|
||||
end
|
||||
end
|
||||
|
||||
local archetypes = world:query(A, B, C, D):archetypes()
|
||||
START()
|
||||
-- for _, archetype in archetypes do
|
||||
-- local columns, records = archetype.columns, archetype.records
|
||||
-- local a = columns[records[A]]
|
||||
-- local b = columns[records[B]]
|
||||
-- local c = columns[records[C]]
|
||||
-- local d = columns[records[D]]
|
||||
-- for row in archetype.entities do
|
||||
-- local _1, _2, _3, _4 = a[row], b[row], c[row], d[row]
|
||||
-- end
|
||||
-- end
|
||||
for _, archetype in archetypes do
|
||||
local columns_map = archetype.columns_map
|
||||
local a = columns_map[A]
|
||||
local b = columns_map[B]
|
||||
local c = columns_map[C]
|
||||
local d = columns_map[D]
|
||||
for _, archetype in world:query(A, B, C, D):archetypes() do
|
||||
local columns, records = archetype.columns, archetype.records
|
||||
local a = columns[records[A].column]
|
||||
local b = columns[records[B].column]
|
||||
local c = columns[records[C].column]
|
||||
local d = columns[records[D].column]
|
||||
for row in archetype.entities do
|
||||
local _1, _2, _3, _4 = a[row], b[row], c[row], d[row]
|
||||
end
|
||||
|
|
2
benches/query.luau
Executable file → Normal file
|
@ -15,7 +15,7 @@ type i53 = number
|
|||
|
||||
do
|
||||
TITLE(testkit.color.white_underline("Jecs query"))
|
||||
local ecs = jecs.world()
|
||||
local ecs = jecs.World.new()
|
||||
do
|
||||
TITLE("one component in common")
|
||||
|
||||
|
|
66
benches/visual/despawn.bench.luau
Executable file → Normal file
|
@ -5,57 +5,41 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local pair = jecs.pair
|
||||
local ecs = jecs.world()
|
||||
local mirror = require(ReplicatedStorage.mirror)
|
||||
local mcs = mirror.World.new()
|
||||
local newWorld = Matter.World.new()
|
||||
local ecs = jecs.World.new()
|
||||
|
||||
local C1 = ecs:component()
|
||||
local C2 = ecs:entity()
|
||||
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local C3 = ecs:entity()
|
||||
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local C4 = ecs:entity()
|
||||
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local E1 = mcs:component()
|
||||
local E2 = mcs:entity()
|
||||
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local E3 = mcs:entity()
|
||||
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local E4 = mcs:entity()
|
||||
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local A, B = Matter.component(), Matter.component()
|
||||
local C, D = ecs:component(), ecs:component()
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
local j = ecs:entity()
|
||||
ecs:set(j, C1, true)
|
||||
local m = mcs:entity()
|
||||
mcs:set(m, E1, true)
|
||||
for i = 1, 1000 do
|
||||
local friend1 = ecs:entity()
|
||||
local friend2 = mcs:entity()
|
||||
|
||||
ecs:add(friend1, pair(C2, j))
|
||||
ecs:add(friend1, pair(C3, j))
|
||||
ecs:add(friend1, pair(C4, j))
|
||||
|
||||
mcs:add(friend2, pair(E2, m))
|
||||
mcs:add(friend2, pair(E3, m))
|
||||
mcs:add(friend2, pair(E4, m))
|
||||
end
|
||||
return {
|
||||
m = m,
|
||||
j = j,
|
||||
local matter_entities = {}
|
||||
local jecs_entities = {}
|
||||
local entities = {
|
||||
matter = matter_entities,
|
||||
jecs = jecs_entities,
|
||||
}
|
||||
for i = 1, 1000 do
|
||||
table.insert(matter_entities, newWorld:spawn(A(), B()))
|
||||
local e = ecs:entity()
|
||||
ecs:set(e, C, {})
|
||||
ecs:set(e, D, {})
|
||||
table.insert(jecs_entities, e)
|
||||
end
|
||||
return entities
|
||||
end,
|
||||
|
||||
Functions = {
|
||||
Mirror = function(_, a)
|
||||
mcs:delete(a.m)
|
||||
Matter = function(_, entities)
|
||||
for _, entity in entities.matter do
|
||||
newWorld:despawn(entity)
|
||||
end
|
||||
end,
|
||||
|
||||
Jecs = function(_, a)
|
||||
ecs:delete(a.j)
|
||||
Jecs = function(_, entities)
|
||||
for _, entity in entities.jecs do
|
||||
ecs:delete(entity)
|
||||
end
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
|
116
benches/visual/insertion.bench.luau
Executable file → Normal file
|
@ -2,58 +2,98 @@
|
|||
--!native
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jecs = require(ReplicatedStorage.Lib:Clone())
|
||||
local ecs = jecs.world()
|
||||
local mirror = require(ReplicatedStorage.mirror:Clone())
|
||||
local mcs = mirror.world()
|
||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local newWorld = Matter.World.new()
|
||||
local ecs = jecs.World.new()
|
||||
local mirror = require(ReplicatedStorage.mirror)
|
||||
local mcs = mirror.World.new()
|
||||
|
||||
local C1 = ecs:component()
|
||||
local C2 = ecs:component()
|
||||
local C3 = ecs:component()
|
||||
local C4 = ecs:component()
|
||||
local C5 = ecs:component()
|
||||
local C6 = ecs:component()
|
||||
local C7 = ecs:component()
|
||||
local C8 = ecs:component()
|
||||
local E1 = mcs:component()
|
||||
local E2 = mcs:component()
|
||||
local E3 = mcs:component()
|
||||
local E4 = mcs:component()
|
||||
local E5 = mcs:component()
|
||||
local E6 = mcs:component()
|
||||
local E7 = mcs:component()
|
||||
local E8 = mcs:component()
|
||||
local A1 = Matter.component()
|
||||
local A2 = Matter.component()
|
||||
local A3 = Matter.component()
|
||||
local A4 = Matter.component()
|
||||
local A5 = Matter.component()
|
||||
local A6 = Matter.component()
|
||||
local A7 = Matter.component()
|
||||
local A8 = Matter.component()
|
||||
|
||||
local B1 = ecr.component()
|
||||
local B2 = ecr.component()
|
||||
local B3 = ecr.component()
|
||||
local B4 = ecr.component()
|
||||
local B5 = ecr.component()
|
||||
local B6 = ecr.component()
|
||||
local B7 = ecr.component()
|
||||
local B8 = ecr.component()
|
||||
|
||||
local C1 = ecs:entity()
|
||||
local C2 = ecs:entity()
|
||||
local C3 = ecs:entity()
|
||||
local C4 = ecs:entity()
|
||||
local C5 = ecs:entity()
|
||||
local C6 = ecs:entity()
|
||||
local C7 = ecs:entity()
|
||||
local C8 = ecs:entity()
|
||||
local E1 = mcs:entity()
|
||||
local E2 = mcs:entity()
|
||||
local E3 = mcs:entity()
|
||||
local E4 = mcs:entity()
|
||||
local E5 = mcs:entity()
|
||||
local E6 = mcs:entity()
|
||||
local E7 = mcs:entity()
|
||||
local E8 = mcs:entity()
|
||||
|
||||
local registry2 = ecr.registry()
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
return
|
||||
end,
|
||||
|
||||
Functions = {
|
||||
Mirror = function()
|
||||
local e = mcs:entity()
|
||||
Matter = function()
|
||||
local e = newWorld:spawn()
|
||||
for i = 1, 5000 do
|
||||
mcs:set(e, E1, false)
|
||||
mcs:set(e, E2, false)
|
||||
mcs:set(e, E3, false)
|
||||
mcs:set(e, E4, false)
|
||||
mcs:set(e, E5, false)
|
||||
mcs:set(e, E6, false)
|
||||
mcs:set(e, E7, false)
|
||||
mcs:set(e, E8, false)
|
||||
newWorld:insert(
|
||||
e,
|
||||
A1({ value = true }),
|
||||
A2({ value = true }),
|
||||
A3({ value = true }),
|
||||
A4({ value = true }),
|
||||
A5({ value = true }),
|
||||
A6({ value = true }),
|
||||
A7({ value = true }),
|
||||
A8({ value = true })
|
||||
)
|
||||
end
|
||||
end,
|
||||
|
||||
ECR = function()
|
||||
local e = registry2.create()
|
||||
for i = 1, 5000 do
|
||||
registry2:set(e, B1, { value = false })
|
||||
registry2:set(e, B2, { value = false })
|
||||
registry2:set(e, B3, { value = false })
|
||||
registry2:set(e, B4, { value = false })
|
||||
registry2:set(e, B5, { value = false })
|
||||
registry2:set(e, B6, { value = false })
|
||||
registry2:set(e, B7, { value = false })
|
||||
registry2:set(e, B8, { value = false })
|
||||
end
|
||||
end,
|
||||
|
||||
Jecs = function()
|
||||
local e = ecs:entity()
|
||||
for i = 1, 5000 do
|
||||
ecs:set(e, C1, false)
|
||||
ecs:set(e, C2, false)
|
||||
ecs:set(e, C3, false)
|
||||
ecs:set(e, C4, false)
|
||||
ecs:set(e, C5, false)
|
||||
ecs:set(e, C6, false)
|
||||
ecs:set(e, C7, false)
|
||||
ecs:set(e, C8, false)
|
||||
ecs:set(e, C1, { value = false })
|
||||
ecs:set(e, C2, { value = false })
|
||||
ecs:set(e, C3, { value = false })
|
||||
ecs:set(e, C4, { value = false })
|
||||
ecs:set(e, C5, { value = false })
|
||||
ecs:set(e, C6, { value = false })
|
||||
ecs:set(e, C7, { value = false })
|
||||
ecs:set(e, C8, { value = false })
|
||||
end
|
||||
end,
|
||||
},
|
||||
|
|
45
benches/visual/query.bench.luau
Executable file → Normal file
|
@ -2,14 +2,14 @@
|
|||
--!native
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Matter = require(ReplicatedStorage.DevPackages.matter)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter)
|
||||
local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr)
|
||||
local newWorld = Matter.World.new()
|
||||
|
||||
local jecs = require(ReplicatedStorage.Lib:Clone())
|
||||
local mirror = require(ReplicatedStorage.mirror:Clone())
|
||||
local mcs = mirror.world()
|
||||
local ecs = jecs.world()
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local mirror = require(ReplicatedStorage.mirror)
|
||||
local mcs = mirror.World.new()
|
||||
local ecs = jecs.World.new()
|
||||
|
||||
local A1 = Matter.component()
|
||||
local A2 = Matter.component()
|
||||
|
@ -38,14 +38,14 @@ local D6 = ecs:component()
|
|||
local D7 = ecs:component()
|
||||
local D8 = ecs:component()
|
||||
|
||||
local E1 = mcs:component()
|
||||
local E2 = mcs:component()
|
||||
local E3 = mcs:component()
|
||||
local E4 = mcs:component()
|
||||
local E5 = mcs:component()
|
||||
local E6 = mcs:component()
|
||||
local E7 = mcs:component()
|
||||
local E8 = mcs:component()
|
||||
local E1 = mcs:entity()
|
||||
local E2 = mcs:entity()
|
||||
local E3 = mcs:entity()
|
||||
local E4 = mcs:entity()
|
||||
local E5 = mcs:entity()
|
||||
local E6 = mcs:entity()
|
||||
local E7 = mcs:entity()
|
||||
local E8 = mcs:entity()
|
||||
|
||||
local registry2 = ecr.registry()
|
||||
|
||||
|
@ -146,18 +146,13 @@ return {
|
|||
end,
|
||||
|
||||
Functions = {
|
||||
-- Matter = function()
|
||||
-- for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
|
||||
-- end
|
||||
-- end,
|
||||
Matter = function()
|
||||
for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
|
||||
end
|
||||
end,
|
||||
|
||||
-- ECR = function()
|
||||
-- for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
|
||||
-- end
|
||||
-- end,
|
||||
--
|
||||
Mirror = function()
|
||||
for entityId, firstComponent in mcs:query(E2, E4, E6, E8) do
|
||||
ECR = function()
|
||||
for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
|
||||
end
|
||||
end,
|
||||
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local pair = jecs.pair
|
||||
local ecs = jecs.world()
|
||||
local mirror = require(ReplicatedStorage.mirror)
|
||||
local mcs = mirror.world()
|
||||
|
||||
local C1 = ecs:component()
|
||||
local C2 = ecs:entity()
|
||||
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local C3 = ecs:entity()
|
||||
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local C4 = ecs:entity()
|
||||
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local E1 = mcs:component()
|
||||
local E2 = mcs:entity()
|
||||
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local E3 = mcs:entity()
|
||||
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
local E4 = mcs:entity()
|
||||
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
end,
|
||||
|
||||
Functions = {
|
||||
Mirror = function()
|
||||
local m = mcs:entity()
|
||||
for i = 1, 1000 do
|
||||
mcs:add(m, E3)
|
||||
mcs:remove(m, E3)
|
||||
end
|
||||
end,
|
||||
|
||||
Jecs = function()
|
||||
local j = ecs:entity()
|
||||
for i = 1, 1000 do
|
||||
ecs:add(j, C3)
|
||||
ecs:remove(j, C3)
|
||||
end
|
||||
end,
|
||||
},
|
||||
}
|
34
benches/visual/spawn.bench.luau
Executable file → Normal file
|
@ -2,32 +2,36 @@
|
|||
--!native
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jecs = require(ReplicatedStorage.Lib:Clone())
|
||||
local mirror = require(ReplicatedStorage.mirror:Clone())
|
||||
|
||||
|
||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local newWorld = Matter.World.new()
|
||||
local ecs = jecs.World.new()
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
local ecs = jecs.world()
|
||||
ecs:range(1000, 20000)
|
||||
local mcs = mirror.World.new()
|
||||
return ecs, mcs
|
||||
local registry2 = ecr.registry()
|
||||
|
||||
return registry2
|
||||
end,
|
||||
|
||||
Functions = {
|
||||
Mirror = function(_, ecs, mcs)
|
||||
for i = 1, 100 do
|
||||
|
||||
mcs:entity()
|
||||
Matter = function()
|
||||
for i = 1, 1000 do
|
||||
newWorld:spawn()
|
||||
end
|
||||
end,
|
||||
|
||||
Jecs = function(_, ecs, mcs)
|
||||
for i = 1, 100 do
|
||||
ECR = function(_, registry2)
|
||||
for i = 1, 1000 do
|
||||
registry2.create()
|
||||
end
|
||||
end,
|
||||
|
||||
Jecs = function()
|
||||
for i = 1, 1000 do
|
||||
ecs:entity()
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
0
benches/visual/wally.toml
Executable file → Normal file
0
default.project.json
Executable file → Normal file
BIN
demo.rbxl
Normal file
18
demo/.config/blink
Normal file
|
@ -0,0 +1,18 @@
|
|||
option ClientOutput = "../net/client.luau"
|
||||
option ServerOutput = "../net/server.luau"
|
||||
|
||||
event UpdateTransform {
|
||||
From: Server,
|
||||
Type: Unreliable,
|
||||
Call: SingleSync,
|
||||
Poll: true,
|
||||
Data: (f64, CFrame)
|
||||
}
|
||||
|
||||
event SpawnMob {
|
||||
From: Server,
|
||||
Type: Reliable,
|
||||
Call: SingleSync,
|
||||
Poll: true,
|
||||
Data: (f64, CFrame, u8)
|
||||
}
|
0
demo/.gitignore
vendored
Executable file → Normal file
0
demo/README.md
Executable file → Normal file
127
demo/default.project.json
Executable file → Normal file
|
@ -1,55 +1,76 @@
|
|||
{
|
||||
"name": "demo",
|
||||
"emitLegacyScripts": false,
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"$path": "src/ReplicatedStorage",
|
||||
"ecs": {
|
||||
"$path": "../jecs.luau"
|
||||
},
|
||||
"Packages": {
|
||||
"$path": "Packages"
|
||||
}
|
||||
},
|
||||
"ServerScriptService": {
|
||||
"$className": "ServerScriptService",
|
||||
"$path": "src/ServerScriptService"
|
||||
},
|
||||
"Workspace": {
|
||||
"$properties": {
|
||||
"FilteringEnabled": true
|
||||
},
|
||||
"Baseplate": {
|
||||
"$className": "Part",
|
||||
"$properties": {
|
||||
"Anchored": true,
|
||||
"Color": [0.38823, 0.37254, 0.38823],
|
||||
"Locked": true,
|
||||
"Position": [0, -10, 0],
|
||||
"Size": [512, 20, 512]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Lighting": {
|
||||
"$properties": {
|
||||
"Ambient": [0, 0, 0],
|
||||
"Brightness": 2,
|
||||
"GlobalShadows": true,
|
||||
"Outlines": false,
|
||||
"Technology": "Voxel"
|
||||
}
|
||||
},
|
||||
"SoundService": {
|
||||
"$properties": {
|
||||
"RespectFilteringEnabled": true
|
||||
}
|
||||
},
|
||||
"StarterPlayer": {
|
||||
"StarterPlayerScripts": {
|
||||
"$path": "src/StarterPlayer/StarterPlayerScripts"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "demo",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"$path": "src/ReplicatedStorage",
|
||||
"ecs": {
|
||||
"$path": "../jecs.luau"
|
||||
},
|
||||
"net": {
|
||||
"$path": "net/client.luau"
|
||||
},
|
||||
"Packages": {
|
||||
"$path": "Packages"
|
||||
}
|
||||
},
|
||||
"ServerScriptService": {
|
||||
"$className": "ServerScriptService",
|
||||
"$path": "src/ServerScriptService",
|
||||
"net": {
|
||||
"$path": "net/server.luau"
|
||||
}
|
||||
},
|
||||
"Workspace": {
|
||||
"$properties": {
|
||||
"FilteringEnabled": true
|
||||
},
|
||||
"Baseplate": {
|
||||
"$className": "Part",
|
||||
"$properties": {
|
||||
"Anchored": true,
|
||||
"Color": [
|
||||
0.38823,
|
||||
0.37254,
|
||||
0.38823
|
||||
],
|
||||
"Locked": true,
|
||||
"Position": [
|
||||
0,
|
||||
-10,
|
||||
0
|
||||
],
|
||||
"Size": [
|
||||
512,
|
||||
20,
|
||||
512
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Lighting": {
|
||||
"$properties": {
|
||||
"Ambient": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"Brightness": 2,
|
||||
"GlobalShadows": true,
|
||||
"Outlines": false,
|
||||
"Technology": "Voxel"
|
||||
}
|
||||
},
|
||||
"SoundService": {
|
||||
"$properties": {
|
||||
"RespectFilteringEnabled": true
|
||||
}
|
||||
},
|
||||
"StarterPlayer": {
|
||||
"StarterPlayerScripts": {
|
||||
"$path": "src/StarterPlayer/StarterPlayerScripts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
336
demo/net/client.luau
Normal file
|
@ -0,0 +1,336 @@
|
|||
--!strict
|
||||
--!native
|
||||
--!optimize 2
|
||||
--!nolint LocalShadow
|
||||
--#selene: allow(shadowing)
|
||||
-- File generated by Blink v0.14.1 (https://github.com/1Axen/Blink)
|
||||
-- This file is not meant to be edited
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
if not RunService:IsClient() then
|
||||
error("Client network module can only be required from the client.")
|
||||
end
|
||||
|
||||
local Reliable: RemoteEvent = ReplicatedStorage:WaitForChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent
|
||||
local Unreliable: UnreliableRemoteEvent =
|
||||
ReplicatedStorage:WaitForChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
|
||||
|
||||
local Invocations = 0
|
||||
|
||||
local SendSize = 64
|
||||
local SendOffset = 0
|
||||
local SendCursor = 0
|
||||
local SendBuffer = buffer.create(64)
|
||||
local SendInstances = {}
|
||||
|
||||
local RecieveCursor = 0
|
||||
local RecieveBuffer = buffer.create(64)
|
||||
|
||||
local RecieveInstances = {}
|
||||
local RecieveInstanceCursor = 0
|
||||
|
||||
type Entry = {
|
||||
value: any,
|
||||
next: Entry?,
|
||||
}
|
||||
|
||||
type Queue = {
|
||||
head: Entry?,
|
||||
tail: Entry?,
|
||||
}
|
||||
|
||||
type BufferSave = {
|
||||
Size: number,
|
||||
Cursor: number,
|
||||
Buffer: buffer,
|
||||
Instances: { Instance },
|
||||
}
|
||||
|
||||
local function Read(Bytes: number)
|
||||
local Offset = RecieveCursor
|
||||
RecieveCursor += Bytes
|
||||
return Offset
|
||||
end
|
||||
|
||||
local function Save(): BufferSave
|
||||
return {
|
||||
Size = SendSize,
|
||||
Cursor = SendCursor,
|
||||
Buffer = SendBuffer,
|
||||
Instances = SendInstances,
|
||||
}
|
||||
end
|
||||
|
||||
local function Load(Save: BufferSave?)
|
||||
if Save then
|
||||
SendSize = Save.Size
|
||||
SendCursor = Save.Cursor
|
||||
SendOffset = Save.Cursor
|
||||
SendBuffer = Save.Buffer
|
||||
SendInstances = Save.Instances
|
||||
return
|
||||
end
|
||||
|
||||
SendSize = 64
|
||||
SendCursor = 0
|
||||
SendOffset = 0
|
||||
SendBuffer = buffer.create(64)
|
||||
SendInstances = {}
|
||||
end
|
||||
|
||||
local function Invoke()
|
||||
if Invocations == 255 then
|
||||
Invocations = 0
|
||||
end
|
||||
|
||||
local Invocation = Invocations
|
||||
Invocations += 1
|
||||
return Invocation
|
||||
end
|
||||
|
||||
local function Allocate(Bytes: number)
|
||||
local InUse = (SendCursor + Bytes)
|
||||
if InUse > SendSize then
|
||||
--> Avoid resizing the buffer for every write
|
||||
while InUse > SendSize do
|
||||
SendSize *= 1.5
|
||||
end
|
||||
|
||||
local Buffer = buffer.create(SendSize)
|
||||
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
|
||||
SendBuffer = Buffer
|
||||
end
|
||||
|
||||
SendOffset = SendCursor
|
||||
SendCursor += Bytes
|
||||
|
||||
return SendOffset
|
||||
end
|
||||
|
||||
local function CreateQueue(): Queue
|
||||
return {
|
||||
head = nil,
|
||||
tail = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local function Pop(queue: Queue): any
|
||||
local head = queue.head
|
||||
if head == nil then
|
||||
return
|
||||
end
|
||||
|
||||
queue.head = head.next
|
||||
return head.value
|
||||
end
|
||||
|
||||
local function Push(queue: Queue, value: any)
|
||||
local entry: Entry = {
|
||||
value = value,
|
||||
next = nil,
|
||||
}
|
||||
|
||||
if queue.tail ~= nil then
|
||||
queue.tail.next = entry
|
||||
end
|
||||
|
||||
queue.tail = entry
|
||||
|
||||
if queue.head == nil then
|
||||
queue.head = entry
|
||||
end
|
||||
end
|
||||
|
||||
local Types = {}
|
||||
local Calls = table.create(256)
|
||||
|
||||
local Events: any = {
|
||||
Reliable = table.create(256),
|
||||
Unreliable = table.create(256),
|
||||
}
|
||||
|
||||
local Queue: any = {
|
||||
Reliable = table.create(256),
|
||||
Unreliable = table.create(256),
|
||||
}
|
||||
|
||||
Queue.Unreliable[0] = CreateQueue()
|
||||
Queue.Reliable[0] = CreateQueue()
|
||||
|
||||
function Types.ReadEVENT_UpdateTransform(): (number, CFrame)
|
||||
-- Read BLOCK: 32 bytes
|
||||
local BLOCK_START = Read(32)
|
||||
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
|
||||
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
|
||||
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
|
||||
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
|
||||
local Position = Vector3.new(X, Y, Z)
|
||||
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
|
||||
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
|
||||
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
|
||||
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
|
||||
return Value1, Value2
|
||||
end
|
||||
|
||||
function Types.WriteEVENT_UpdateTransform(Value1: number, Value2: CFrame): ()
|
||||
-- Allocate BLOCK: 33 bytes
|
||||
local BLOCK_START = Allocate(33)
|
||||
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
|
||||
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
|
||||
local Vector = Value2.Position
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
|
||||
local rX, rY, rZ = Value2:ToOrientation()
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
|
||||
end
|
||||
|
||||
function Types.ReadEVENT_SpawnMob(): (number, CFrame, number)
|
||||
-- Read BLOCK: 33 bytes
|
||||
local BLOCK_START = Read(33)
|
||||
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
|
||||
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
|
||||
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
|
||||
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
|
||||
local Position = Vector3.new(X, Y, Z)
|
||||
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
|
||||
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
|
||||
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
|
||||
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
|
||||
local Value3 = buffer.readu8(RecieveBuffer, BLOCK_START + 32)
|
||||
return Value1, Value2, Value3
|
||||
end
|
||||
|
||||
function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: number): ()
|
||||
-- Allocate BLOCK: 34 bytes
|
||||
local BLOCK_START = Allocate(34)
|
||||
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
|
||||
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
|
||||
local Vector = Value2.Position
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
|
||||
local rX, rY, rZ = Value2:ToOrientation()
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
|
||||
buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3)
|
||||
end
|
||||
|
||||
local function StepReplication()
|
||||
if SendCursor <= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local Buffer = buffer.create(SendCursor)
|
||||
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
|
||||
Reliable:FireServer(Buffer, SendInstances)
|
||||
|
||||
SendSize = 64
|
||||
SendCursor = 0
|
||||
SendOffset = 0
|
||||
SendBuffer = buffer.create(64)
|
||||
table.clear(SendInstances)
|
||||
end
|
||||
|
||||
local Elapsed = 0
|
||||
RunService.Heartbeat:Connect(function(DeltaTime: number)
|
||||
Elapsed += DeltaTime
|
||||
if Elapsed >= (1 / 61) then
|
||||
Elapsed -= (1 / 61)
|
||||
StepReplication()
|
||||
end
|
||||
end)
|
||||
|
||||
Reliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance })
|
||||
RecieveCursor = 0
|
||||
RecieveBuffer = Buffer
|
||||
RecieveInstances = Instances
|
||||
RecieveInstanceCursor = 0
|
||||
local Size = buffer.len(RecieveBuffer)
|
||||
while RecieveCursor < Size do
|
||||
-- Read BLOCK: 1 bytes
|
||||
local BLOCK_START = Read(1)
|
||||
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
|
||||
if Index == 0 then
|
||||
Push(Queue.Reliable[0], table.pack(Types.ReadEVENT_SpawnMob()))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
Unreliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance })
|
||||
RecieveCursor = 0
|
||||
RecieveBuffer = Buffer
|
||||
RecieveInstances = Instances
|
||||
RecieveInstanceCursor = 0
|
||||
local Size = buffer.len(RecieveBuffer)
|
||||
while RecieveCursor < Size do
|
||||
-- Read BLOCK: 1 bytes
|
||||
local BLOCK_START = Read(1)
|
||||
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
|
||||
if Index == 0 then
|
||||
Push(Queue.Unreliable[0], table.pack(Types.ReadEVENT_UpdateTransform()))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
return {
|
||||
StepReplication = StepReplication,
|
||||
|
||||
UpdateTransform = {
|
||||
Iter = function(): () -> (number, number, CFrame)
|
||||
local index = 0
|
||||
local queue = Queue.Unreliable[0]
|
||||
return function(): (number, number, CFrame)
|
||||
index += 1
|
||||
local arguments = Pop(queue)
|
||||
if arguments ~= nil then
|
||||
return index, unpack(arguments, 1, arguments.n)
|
||||
end
|
||||
return
|
||||
end
|
||||
end,
|
||||
Next = function(): () -> (number, number, CFrame)
|
||||
local index = 0
|
||||
local queue = Queue.Unreliable[0]
|
||||
return function(): (number, number, CFrame)
|
||||
index += 1
|
||||
local arguments = Pop(queue)
|
||||
if arguments ~= nil then
|
||||
return index, unpack(arguments, 1, arguments.n)
|
||||
end
|
||||
return
|
||||
end
|
||||
end,
|
||||
},
|
||||
SpawnMob = {
|
||||
Iter = function(): () -> (number, number, CFrame, number)
|
||||
local index = 0
|
||||
local queue = Queue.Reliable[0]
|
||||
return function(): (number, number, CFrame, number)
|
||||
index += 1
|
||||
local arguments = Pop(queue)
|
||||
if arguments ~= nil then
|
||||
return index, unpack(arguments, 1, arguments.n)
|
||||
end
|
||||
return
|
||||
end
|
||||
end,
|
||||
Next = function(): () -> (number, number, CFrame, number)
|
||||
local index = 0
|
||||
local queue = Queue.Reliable[0]
|
||||
return function(): (number, number, CFrame, number)
|
||||
index += 1
|
||||
local arguments = Pop(queue)
|
||||
if arguments ~= nil then
|
||||
return index, unpack(arguments, 1, arguments.n)
|
||||
end
|
||||
return
|
||||
end
|
||||
end,
|
||||
},
|
||||
}
|
372
demo/net/server.luau
Normal file
|
@ -0,0 +1,372 @@
|
|||
--!strict
|
||||
--!native
|
||||
--!optimize 2
|
||||
--!nolint LocalShadow
|
||||
--#selene: allow(shadowing)
|
||||
-- File generated by Blink v0.14.1 (https://github.com/1Axen/Blink)
|
||||
-- This file is not meant to be edited
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
if not RunService:IsServer() then
|
||||
error("Server network module can only be required from the server.")
|
||||
end
|
||||
|
||||
local Reliable: RemoteEvent = ReplicatedStorage:FindFirstChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent
|
||||
if not Reliable then
|
||||
local RemoteEvent = Instance.new("RemoteEvent")
|
||||
RemoteEvent.Name = "BLINK_RELIABLE_REMOTE"
|
||||
RemoteEvent.Parent = ReplicatedStorage
|
||||
Reliable = RemoteEvent
|
||||
end
|
||||
|
||||
local Unreliable: UnreliableRemoteEvent =
|
||||
ReplicatedStorage:FindFirstChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
|
||||
if not Unreliable then
|
||||
local UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent")
|
||||
UnreliableRemoteEvent.Name = "BLINK_UNRELIABLE_REMOTE"
|
||||
UnreliableRemoteEvent.Parent = ReplicatedStorage
|
||||
Unreliable = UnreliableRemoteEvent
|
||||
end
|
||||
|
||||
local Invocations = 0
|
||||
|
||||
local SendSize = 64
|
||||
local SendOffset = 0
|
||||
local SendCursor = 0
|
||||
local SendBuffer = buffer.create(64)
|
||||
local SendInstances = {}
|
||||
|
||||
local RecieveCursor = 0
|
||||
local RecieveBuffer = buffer.create(64)
|
||||
|
||||
local RecieveInstances = {}
|
||||
local RecieveInstanceCursor = 0
|
||||
|
||||
type Entry = {
|
||||
value: any,
|
||||
next: Entry?,
|
||||
}
|
||||
|
||||
type Queue = {
|
||||
head: Entry?,
|
||||
tail: Entry?,
|
||||
}
|
||||
|
||||
type BufferSave = {
|
||||
Size: number,
|
||||
Cursor: number,
|
||||
Buffer: buffer,
|
||||
Instances: { Instance },
|
||||
}
|
||||
|
||||
local function Read(Bytes: number)
|
||||
local Offset = RecieveCursor
|
||||
RecieveCursor += Bytes
|
||||
return Offset
|
||||
end
|
||||
|
||||
local function Save(): BufferSave
|
||||
return {
|
||||
Size = SendSize,
|
||||
Cursor = SendCursor,
|
||||
Buffer = SendBuffer,
|
||||
Instances = SendInstances,
|
||||
}
|
||||
end
|
||||
|
||||
local function Load(Save: BufferSave?)
|
||||
if Save then
|
||||
SendSize = Save.Size
|
||||
SendCursor = Save.Cursor
|
||||
SendOffset = Save.Cursor
|
||||
SendBuffer = Save.Buffer
|
||||
SendInstances = Save.Instances
|
||||
return
|
||||
end
|
||||
|
||||
SendSize = 64
|
||||
SendCursor = 0
|
||||
SendOffset = 0
|
||||
SendBuffer = buffer.create(64)
|
||||
SendInstances = {}
|
||||
end
|
||||
|
||||
local function Invoke()
|
||||
if Invocations == 255 then
|
||||
Invocations = 0
|
||||
end
|
||||
|
||||
local Invocation = Invocations
|
||||
Invocations += 1
|
||||
return Invocation
|
||||
end
|
||||
|
||||
local function Allocate(Bytes: number)
|
||||
local InUse = (SendCursor + Bytes)
|
||||
if InUse > SendSize then
|
||||
--> Avoid resizing the buffer for every write
|
||||
while InUse > SendSize do
|
||||
SendSize *= 1.5
|
||||
end
|
||||
|
||||
local Buffer = buffer.create(SendSize)
|
||||
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
|
||||
SendBuffer = Buffer
|
||||
end
|
||||
|
||||
SendOffset = SendCursor
|
||||
SendCursor += Bytes
|
||||
|
||||
return SendOffset
|
||||
end
|
||||
|
||||
local function CreateQueue(): Queue
|
||||
return {
|
||||
head = nil,
|
||||
tail = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local function Pop(queue: Queue): any
|
||||
local head = queue.head
|
||||
if head == nil then
|
||||
return
|
||||
end
|
||||
|
||||
queue.head = head.next
|
||||
return head.value
|
||||
end
|
||||
|
||||
local function Push(queue: Queue, value: any)
|
||||
local entry: Entry = {
|
||||
value = value,
|
||||
next = nil,
|
||||
}
|
||||
|
||||
if queue.tail ~= nil then
|
||||
queue.tail.next = entry
|
||||
end
|
||||
|
||||
queue.tail = entry
|
||||
|
||||
if queue.head == nil then
|
||||
queue.head = entry
|
||||
end
|
||||
end
|
||||
|
||||
local Types = {}
|
||||
local Calls = table.create(256)
|
||||
|
||||
local Events: any = {
|
||||
Reliable = table.create(256),
|
||||
Unreliable = table.create(256),
|
||||
}
|
||||
|
||||
local Queue: any = {
|
||||
Reliable = table.create(256),
|
||||
Unreliable = table.create(256),
|
||||
}
|
||||
|
||||
function Types.ReadEVENT_UpdateTransform(): (number, CFrame)
|
||||
-- Read BLOCK: 32 bytes
|
||||
local BLOCK_START = Read(32)
|
||||
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
|
||||
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
|
||||
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
|
||||
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
|
||||
local Position = Vector3.new(X, Y, Z)
|
||||
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
|
||||
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
|
||||
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
|
||||
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
|
||||
return Value1, Value2
|
||||
end
|
||||
|
||||
function Types.WriteEVENT_UpdateTransform(Value1: number, Value2: CFrame): ()
|
||||
-- Allocate BLOCK: 33 bytes
|
||||
local BLOCK_START = Allocate(33)
|
||||
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
|
||||
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
|
||||
local Vector = Value2.Position
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
|
||||
local rX, rY, rZ = Value2:ToOrientation()
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
|
||||
end
|
||||
|
||||
function Types.ReadEVENT_SpawnMob(): (number, CFrame, number)
|
||||
-- Read BLOCK: 33 bytes
|
||||
local BLOCK_START = Read(33)
|
||||
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
|
||||
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
|
||||
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
|
||||
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
|
||||
local Position = Vector3.new(X, Y, Z)
|
||||
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
|
||||
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
|
||||
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
|
||||
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
|
||||
local Value3 = buffer.readu8(RecieveBuffer, BLOCK_START + 32)
|
||||
return Value1, Value2, Value3
|
||||
end
|
||||
|
||||
function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: number): ()
|
||||
-- Allocate BLOCK: 34 bytes
|
||||
local BLOCK_START = Allocate(34)
|
||||
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
|
||||
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
|
||||
local Vector = Value2.Position
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
|
||||
local rX, rY, rZ = Value2:ToOrientation()
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
|
||||
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
|
||||
buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3)
|
||||
end
|
||||
|
||||
local PlayersMap: { [Player]: BufferSave } = {}
|
||||
|
||||
Players.PlayerRemoving:Connect(function(Player)
|
||||
PlayersMap[Player] = nil
|
||||
end)
|
||||
|
||||
local function StepReplication()
|
||||
for Player, Send in PlayersMap do
|
||||
if Send.Cursor <= 0 then
|
||||
continue
|
||||
end
|
||||
|
||||
local Buffer = buffer.create(Send.Cursor)
|
||||
buffer.copy(Buffer, 0, Send.Buffer, 0, Send.Cursor)
|
||||
Reliable:FireClient(Player, Buffer, Send.Instances)
|
||||
|
||||
Send.Size = 64
|
||||
Send.Cursor = 0
|
||||
Send.Buffer = buffer.create(64)
|
||||
table.clear(Send.Instances)
|
||||
end
|
||||
end
|
||||
|
||||
RunService.Heartbeat:Connect(StepReplication)
|
||||
|
||||
Reliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance })
|
||||
RecieveCursor = 0
|
||||
RecieveBuffer = Buffer
|
||||
RecieveInstances = Instances
|
||||
RecieveInstanceCursor = 0
|
||||
local Size = buffer.len(RecieveBuffer)
|
||||
while RecieveCursor < Size do
|
||||
-- Read BLOCK: 1 bytes
|
||||
local BLOCK_START = Read(1)
|
||||
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
|
||||
end
|
||||
end)
|
||||
|
||||
Unreliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance })
|
||||
RecieveCursor = 0
|
||||
RecieveBuffer = Buffer
|
||||
RecieveInstances = Instances
|
||||
RecieveInstanceCursor = 0
|
||||
local Size = buffer.len(RecieveBuffer)
|
||||
while RecieveCursor < Size do
|
||||
-- Read BLOCK: 1 bytes
|
||||
local BLOCK_START = Read(1)
|
||||
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
|
||||
end
|
||||
end)
|
||||
|
||||
return {
|
||||
StepReplication = StepReplication,
|
||||
|
||||
UpdateTransform = {
|
||||
Fire = function(Player: Player, Value1: number, Value2: CFrame): ()
|
||||
Load()
|
||||
Types.WriteEVENT_UpdateTransform(Value1, Value2)
|
||||
local Buffer = buffer.create(SendCursor)
|
||||
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
|
||||
Unreliable:FireClient(Player, Buffer, SendInstances)
|
||||
end,
|
||||
FireAll = function(Value1: number, Value2: CFrame): ()
|
||||
Load()
|
||||
Types.WriteEVENT_UpdateTransform(Value1, Value2)
|
||||
local Buffer = buffer.create(SendCursor)
|
||||
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
|
||||
Unreliable:FireAllClients(Buffer, SendInstances)
|
||||
end,
|
||||
FireList = function(List: { Player }, Value1: number, Value2: CFrame): ()
|
||||
Load()
|
||||
Types.WriteEVENT_UpdateTransform(Value1, Value2)
|
||||
local Buffer = buffer.create(SendCursor)
|
||||
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
|
||||
for _, Player in List do
|
||||
Unreliable:FireClient(Player, Buffer, SendInstances)
|
||||
end
|
||||
end,
|
||||
FireExcept = function(Except: Player, Value1: number, Value2: CFrame): ()
|
||||
Load()
|
||||
Types.WriteEVENT_UpdateTransform(Value1, Value2)
|
||||
local Buffer = buffer.create(SendCursor)
|
||||
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
|
||||
for _, Player in Players:GetPlayers() do
|
||||
if Player == Except then
|
||||
continue
|
||||
end
|
||||
Unreliable:FireClient(Player, Buffer, SendInstances)
|
||||
end
|
||||
end,
|
||||
},
|
||||
SpawnMob = {
|
||||
Fire = function(Player: Player, Value1: number, Value2: CFrame, Value3: number): ()
|
||||
Load(PlayersMap[Player])
|
||||
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
|
||||
PlayersMap[Player] = Save()
|
||||
end,
|
||||
FireAll = function(Value1: number, Value2: CFrame, Value3: number): ()
|
||||
Load()
|
||||
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
|
||||
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
|
||||
for _, Player in Players:GetPlayers() do
|
||||
Load(PlayersMap[Player])
|
||||
local Position = Allocate(Size)
|
||||
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
|
||||
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
|
||||
PlayersMap[Player] = Save()
|
||||
end
|
||||
end,
|
||||
FireList = function(List: { Player }, Value1: number, Value2: CFrame, Value3: number): ()
|
||||
Load()
|
||||
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
|
||||
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
|
||||
for _, Player in List do
|
||||
Load(PlayersMap[Player])
|
||||
local Position = Allocate(Size)
|
||||
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
|
||||
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
|
||||
PlayersMap[Player] = Save()
|
||||
end
|
||||
end,
|
||||
FireExcept = function(Except: Player, Value1: number, Value2: CFrame, Value3: number): ()
|
||||
Load()
|
||||
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
|
||||
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
|
||||
for _, Player in Players:GetPlayers() do
|
||||
if Player == Except then
|
||||
continue
|
||||
end
|
||||
Load(PlayersMap[Player])
|
||||
local Position = Allocate(Size)
|
||||
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
|
||||
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
|
||||
PlayersMap[Player] = Save()
|
||||
end
|
||||
end,
|
||||
},
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
local function collect<T...>(
|
||||
signal: {
|
||||
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
|
||||
}
|
||||
): () -> (T...)
|
||||
local enqueued = {}
|
||||
|
||||
local i = 0
|
||||
|
||||
local connection = (signal :: any):Connect(function(...)
|
||||
table.insert(enqueued, { ... })
|
||||
i += 1
|
||||
end)
|
||||
|
||||
return function(): any
|
||||
if i == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
i -= 1
|
||||
|
||||
local args: any = table.remove(enqueued, 1)
|
||||
|
||||
return unpack(args)
|
||||
end, connection
|
||||
end
|
||||
|
||||
return collect
|
|
@ -1,36 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local types = require("./types")
|
||||
|
||||
local Networked = jecs.tag()
|
||||
local NetworkedPair = jecs.tag()
|
||||
|
||||
local Renderable = jecs.component() :: jecs.Id<Instance>
|
||||
jecs.meta(Renderable, Networked)
|
||||
|
||||
|
||||
local Poison = jecs.component() :: jecs.Id<number>
|
||||
jecs.meta(Poison, Networked)
|
||||
|
||||
local Health = jecs.component() :: jecs.Id<number>
|
||||
jecs.meta(Health, Networked)
|
||||
|
||||
local Player = jecs.component() :: jecs.Id<Player>
|
||||
jecs.meta(Player, Networked)
|
||||
|
||||
|
||||
local components = {
|
||||
Renderable = Renderable,
|
||||
Player = Player,
|
||||
Poison = Poison,
|
||||
Health = Health,
|
||||
|
||||
Networked = Networked,
|
||||
NetworkedPair = NetworkedPair,
|
||||
}
|
||||
|
||||
for name, component in components do
|
||||
jecs.meta(component, jecs.Name, name)
|
||||
end
|
||||
|
||||
return components
|
4
demo/src/ReplicatedStorage/ecs_init.luau
Normal file
|
@ -0,0 +1,4 @@
|
|||
_G.JECS_DEBUG = true
|
||||
_G.JECS_HI_COMPONENT_ID = 32
|
||||
require(game:GetService("ReplicatedStorage").ecs)
|
||||
return
|
|
@ -1,13 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local schedule = require(ReplicatedStorage.schedule)
|
||||
local observers_add = require(ReplicatedStorage.observers_add)
|
||||
|
||||
local SYSTEM = schedule.SYSTEM
|
||||
local RUN = schedule.RUN
|
||||
require(ReplicatedStorage.components)
|
||||
local world = observers_add(jecs.world())
|
||||
|
||||
local systems = ReplicatedStorage.systems
|
||||
SYSTEM(world, systems.receive_replication)
|
||||
RUN(world)
|
|
@ -1,190 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
|
||||
type Observer<T...> = {
|
||||
callback: (jecs.Entity) -> (),
|
||||
query: jecs.Query<T...>,
|
||||
}
|
||||
|
||||
export type PatchedWorld = jecs.World & {
|
||||
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
|
||||
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
|
||||
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
|
||||
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
|
||||
observer: (PatchedWorld, Observer<any>) -> (),
|
||||
monitor: (PatchedWorld, Observer<any>) -> (),
|
||||
}
|
||||
|
||||
local function observers_new(world, description)
|
||||
local query = description.query
|
||||
local callback = description.callback
|
||||
local terms = query.filter_with :: { jecs.Id }
|
||||
if not terms then
|
||||
local ids = query.ids
|
||||
query.filter_with = ids
|
||||
terms = ids
|
||||
end
|
||||
|
||||
local entity_index = world.entity_index :: any
|
||||
local function emplaced(entity: jecs.Entity)
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any)
|
||||
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
local archetype = r.archetype
|
||||
|
||||
if jecs.query_match(query, archetype) then
|
||||
callback(entity)
|
||||
end
|
||||
end
|
||||
|
||||
for _, term in terms do
|
||||
world:added(term, emplaced)
|
||||
world:changed(term, emplaced)
|
||||
end
|
||||
end
|
||||
|
||||
local function monitors_new(world, description)
|
||||
local query = description.query
|
||||
local callback = description.callback
|
||||
local terms = query.filter_with :: { jecs.Id }
|
||||
if not terms then
|
||||
local ids = query.ids
|
||||
query.filter_with = ids
|
||||
terms = ids
|
||||
end
|
||||
|
||||
local entity_index = world.entity_index :: any
|
||||
local function emplaced(entity: jecs.Entity)
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any)
|
||||
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
local archetype = r.archetype
|
||||
|
||||
if jecs.query_match(query, archetype) then
|
||||
callback(entity, jecs.OnAdd)
|
||||
end
|
||||
end
|
||||
|
||||
local function removed(entity: jecs.Entity, component: jecs.Id)
|
||||
local r = jecs.entity_index_try_get_fast(
|
||||
entity_index, entity :: any)
|
||||
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
|
||||
local archetype = r.archetype
|
||||
|
||||
if jecs.query_match(query, archetype) then
|
||||
callback(entity, jecs.OnRemove)
|
||||
end
|
||||
end
|
||||
|
||||
for _, term in terms do
|
||||
world:added(term, emplaced)
|
||||
world:removed(term, removed)
|
||||
end
|
||||
end
|
||||
|
||||
local function observers_add(world: jecs.World): PatchedWorld
|
||||
local signals = {
|
||||
added = {},
|
||||
emplaced = {},
|
||||
removed = {},
|
||||
deleted = {}
|
||||
}
|
||||
|
||||
world = world :: jecs.World & {[string]: any}
|
||||
|
||||
world.added = function(_, component, fn)
|
||||
local listeners = signals.added[component]
|
||||
if not listeners then
|
||||
listeners = {}
|
||||
signals.added[component] = listeners
|
||||
|
||||
local idr = jecs.id_record_ensure(world :: any, component :: any)
|
||||
local rw = jecs.pair(component, jecs.Wildcard)
|
||||
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
|
||||
local function on_add(entity: number, id: number, value: any)
|
||||
for _, listener in listeners do
|
||||
listener(entity, id, value)
|
||||
end
|
||||
end
|
||||
world:set(component, jecs.OnAdd, on_add)
|
||||
idr.hooks.on_add = on_add :: any
|
||||
idr_r.hooks.on_add = on_add :: any
|
||||
end
|
||||
table.insert(listeners, fn)
|
||||
end
|
||||
|
||||
world.changed = function(_, component, fn)
|
||||
local listeners = signals.emplaced[component]
|
||||
if not listeners then
|
||||
listeners = {}
|
||||
signals.emplaced[component] = listeners
|
||||
local idr = jecs.id_record_ensure(world :: any, component :: any)
|
||||
local rw = jecs.pair(component, jecs.Wildcard)
|
||||
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
|
||||
local function on_change(entity: number, id: number, value: any)
|
||||
for _, listener in listeners do
|
||||
listener(entity, id, value)
|
||||
end
|
||||
end
|
||||
world:set(component, jecs.OnChange, on_change)
|
||||
idr.hooks.on_change = on_change :: any
|
||||
idr_r.hooks.on_change = on_change :: any
|
||||
end
|
||||
table.insert(listeners, fn)
|
||||
end
|
||||
|
||||
world.removed = function(_, component, fn)
|
||||
local listeners = signals.removed[component]
|
||||
if not listeners then
|
||||
listeners = {}
|
||||
signals.removed[component] = listeners
|
||||
local idr = jecs.id_record_ensure(world :: any, component :: any)
|
||||
local rw = jecs.pair(component, jecs.Wildcard)
|
||||
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
|
||||
local function on_remove(entity: number, id: number, value: any)
|
||||
for _, listener in listeners do
|
||||
listener(entity, id, value)
|
||||
end
|
||||
end
|
||||
world:set(component, jecs.OnRemove, on_remove)
|
||||
idr.hooks.on_remove = on_remove :: any
|
||||
idr_r.hooks.on_remove = on_remove :: any
|
||||
end
|
||||
table.insert(listeners, fn)
|
||||
end
|
||||
|
||||
world.signals = signals
|
||||
|
||||
world.observer = observers_new
|
||||
|
||||
world.monitor = monitors_new
|
||||
|
||||
-- local world_delete = world.delete
|
||||
|
||||
-- world.deleted = function(_, fn)
|
||||
-- local listeners = signals.deleted
|
||||
-- table.insert(listeners, fn)
|
||||
-- end
|
||||
-- world.delete = function(world, entity)
|
||||
-- world_delete(world, entity)
|
||||
-- for _, fn in signals.deleted do
|
||||
-- fn(entity)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
return world :: PatchedWorld
|
||||
end
|
||||
|
||||
return observers_add
|
|
@ -1,50 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local types = require("../ReplicatedStorage/types")
|
||||
|
||||
type Signal<T...> = {
|
||||
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
|
||||
}
|
||||
type Remote<T...> = {
|
||||
FireClient: (Remote<T...>, T...) -> (),
|
||||
FireAllClients: (Remote<T...>, T...) -> (),
|
||||
FireServer: (Remote<T...>) -> (),
|
||||
OnServerEvent: {
|
||||
Connect: (any, fn: (Player, T...) -> () ) -> ()
|
||||
},
|
||||
OnClientEvent: {
|
||||
Connect: (any, fn: (T...) -> () ) -> ()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
local function stream_ensure(name): Remote<any>
|
||||
local remote = ReplicatedStorage:FindFirstChild(name)
|
||||
if not remote then
|
||||
remote = Instance.new("RemoteEvent")
|
||||
remote.Name = name
|
||||
remote.Parent = ReplicatedStorage
|
||||
end
|
||||
return remote :: any
|
||||
end
|
||||
|
||||
local function datagram_ensure(name): Remote<any>
|
||||
local remote = ReplicatedStorage:FindFirstChild(name)
|
||||
if not remote then
|
||||
remote = Instance.new("UnreliableRemoteEvent")
|
||||
remote.Name = name
|
||||
remote.Parent = ReplicatedStorage
|
||||
end
|
||||
return remote :: any
|
||||
end
|
||||
|
||||
return {
|
||||
input = datagram_ensure("input") :: Remote<string>,
|
||||
replication = stream_ensure("replication") :: Remote<{
|
||||
[string]: {
|
||||
set: { types.Entity }?,
|
||||
values: { any }?,
|
||||
removed: { types.Entity }?
|
||||
}
|
||||
}>,
|
||||
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jabby = require(ReplicatedStorage.Packages.jabby)
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
|
||||
jabby.set_check_function(function() return true end)
|
||||
|
||||
local scheduler = jabby.scheduler.create("jabby scheduler")
|
||||
|
||||
jabby.register({
|
||||
applet = jabby.applets.scheduler,
|
||||
name = "Scheduler",
|
||||
configuration = {
|
||||
scheduler = scheduler,
|
||||
},
|
||||
})
|
||||
|
||||
local ContextActionService = game:GetService("ContextActionService")
|
||||
|
||||
local function create_widget(_, state: Enum.UserInputState)
|
||||
local client = jabby.obtain_client()
|
||||
if state ~= Enum.UserInputState.Begin then return end
|
||||
client.spawn_app(client.apps.home, nil)
|
||||
end
|
||||
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local System = jecs.component() :: jecs.Id<{
|
||||
fn: () -> (),
|
||||
name: string,
|
||||
}>
|
||||
local DependsOn = jecs.component()
|
||||
local Phase = jecs.tag()
|
||||
local Event = jecs.component() :: jecs.Id<RBXScriptSignal>
|
||||
|
||||
local pair = jecs.pair
|
||||
|
||||
local types = require(ReplicatedStorage.types)
|
||||
|
||||
local function ECS_PHASE(world, after: types.Entity)
|
||||
local phase = world:entity()
|
||||
world:add(phase, Phase)
|
||||
if after then
|
||||
local dependency = pair(DependsOn, after)
|
||||
world:add(phase, dependency)
|
||||
end
|
||||
|
||||
return phase
|
||||
end
|
||||
|
||||
local Heartbeat = jecs.tag()
|
||||
jecs.meta(Heartbeat, Phase)
|
||||
jecs.meta(Heartbeat, Event, RunService.Heartbeat)
|
||||
|
||||
local PreSimulation = jecs.tag()
|
||||
jecs.meta(PreSimulation, Phase)
|
||||
jecs.meta(PreSimulation, Event, RunService.PreSimulation)
|
||||
|
||||
local PreAnimation = jecs.tag()
|
||||
jecs.meta(PreAnimation, Phase)
|
||||
jecs.meta(PreAnimation, Event, RunService.PreAnimation)
|
||||
|
||||
local PreRender = jecs.tag()
|
||||
jecs.meta(PreRender, Phase)
|
||||
jecs.meta(PreRender, Event, RunService.PreRender)
|
||||
|
||||
local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?)
|
||||
local system = world:entity()
|
||||
local p = phase or Heartbeat
|
||||
local fn = require(mod) :: (...any) -> ()
|
||||
world:set(system, System, {
|
||||
fn = fn(world, 0) or fn,
|
||||
name = mod.Name,
|
||||
})
|
||||
|
||||
local depends_on = DependsOn :: jecs.Entity
|
||||
world:add(system, pair(depends_on, p))
|
||||
end
|
||||
local function find_systems_w_phase(world: types.World, systems, phase: types.Entity)
|
||||
local phase_name = world:get(phase, jecs.Name) :: string
|
||||
for _, s in world:query(System):with(pair(DependsOn, phase)) do
|
||||
table.insert(systems, {
|
||||
id = scheduler:register_system({
|
||||
phase = phase_name,
|
||||
name = s.name,
|
||||
}),
|
||||
fn = s.fn
|
||||
})
|
||||
end
|
||||
for after in world:query(Phase, pair(DependsOn, phase)) do
|
||||
find_systems_w_phase(world, systems, after)
|
||||
end
|
||||
return systems
|
||||
end
|
||||
|
||||
local function ECS_RUN(world: types.World)
|
||||
|
||||
jabby.register({
|
||||
applet = jabby.applets.world,
|
||||
name = "MyWorld",
|
||||
configuration = {
|
||||
world = world,
|
||||
},
|
||||
})
|
||||
|
||||
if RunService:IsClient() then
|
||||
ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4)
|
||||
end
|
||||
|
||||
for phase, event in world:query(Event, Phase) do
|
||||
local systems = find_systems_w_phase(world, {}, phase)
|
||||
event:Connect(function(...)
|
||||
for _, system in systems do
|
||||
scheduler:run(system.id, system.fn, world, ...)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
return {
|
||||
PHASE = ECS_PHASE,
|
||||
SYSTEM = ECS_SYSTEM,
|
||||
RUN = ECS_RUN,
|
||||
phases = {
|
||||
Heartbeat = Heartbeat,
|
||||
PreSimulation = PreSimulation,
|
||||
PreAnimation = PreAnimation,
|
||||
PreRender = PreRender
|
||||
},
|
||||
components = {
|
||||
System = System,
|
||||
DependsOn = DependsOn,
|
||||
Phase = Phase,
|
||||
Event = Event,
|
||||
}
|
||||
}
|
34
demo/src/ReplicatedStorage/start.luau
Normal file
|
@ -0,0 +1,34 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
local jabby = require(ReplicatedStorage.Packages.jabby)
|
||||
local std = ReplicatedStorage.std
|
||||
local scheduler = require(std.scheduler)
|
||||
local world = require(std.world)
|
||||
|
||||
local function start(modules)
|
||||
for _, module in modules do
|
||||
require(module)
|
||||
end
|
||||
local events = scheduler.COLLECT()
|
||||
scheduler.BEGIN(events)
|
||||
jabby.set_check_function(function(player)
|
||||
return true
|
||||
end)
|
||||
if RunService:IsClient() then
|
||||
local player = game:GetService("Players").LocalPlayer
|
||||
local playergui = player:WaitForChild("PlayerGui")
|
||||
local client = jabby.obtain_client()
|
||||
UserInputService.InputBegan:Connect(function(input)
|
||||
if input.KeyCode == Enum.KeyCode.F4 then
|
||||
local home = playergui:FindFirstChild("Home")
|
||||
if home then
|
||||
home:Destroy()
|
||||
end
|
||||
client.spawn_app(client.apps.home)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return start
|
40
demo/src/ReplicatedStorage/std/bt.luau
Normal file
|
@ -0,0 +1,40 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
|
||||
-- original author @centau
|
||||
|
||||
local FAILURE = -1
|
||||
local RUNNING = 0
|
||||
local SUCCESS = 1
|
||||
|
||||
local function SEQUENCE(nodes)
|
||||
return function(...)
|
||||
for _, node in nodes do
|
||||
local status = node(...)
|
||||
if status <= RUNNING then
|
||||
return status
|
||||
end
|
||||
end
|
||||
return SUCCESS
|
||||
end
|
||||
end
|
||||
|
||||
local function FALLBACK(nodes)
|
||||
return function(...)
|
||||
for _, node in nodes do
|
||||
local status = node(...)
|
||||
if status > FAILURE then
|
||||
return status
|
||||
end
|
||||
end
|
||||
return FAILURE
|
||||
end
|
||||
end
|
||||
|
||||
local bt = {
|
||||
SEQUENCE = SEQUENCE,
|
||||
FALLBACK = FALLBACK,
|
||||
RUNNING = RUNNING,
|
||||
}
|
||||
|
||||
return bt
|
67
demo/src/ReplicatedStorage/std/collect.luau
Normal file
|
@ -0,0 +1,67 @@
|
|||
--!nonstrict
|
||||
|
||||
--[[
|
||||
local signal = Signal.new() :: Signal.Signal<string, string>
|
||||
local events = collect(signal)
|
||||
local function system(world)
|
||||
for id, str1, str2 in events do
|
||||
--
|
||||
end
|
||||
end
|
||||
]]
|
||||
|
||||
--[[
|
||||
original author by @memorycode
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Michael
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
--]]
|
||||
|
||||
type Signal<T...> = { [any]: any }
|
||||
local function collect<T...>(event: Signal<T...>)
|
||||
local storage = {}
|
||||
local mt = {}
|
||||
local iter = function()
|
||||
local n = #storage
|
||||
return function()
|
||||
if n <= 0 then
|
||||
mt.__iter = nil
|
||||
return nil
|
||||
end
|
||||
|
||||
n -= 1
|
||||
return n + 1, unpack(table.remove(storage, 1) :: any)
|
||||
end
|
||||
end
|
||||
|
||||
local disconnect = event:Connect(function(...)
|
||||
table.insert(storage, { ... })
|
||||
mt.__iter = iter
|
||||
end)
|
||||
|
||||
setmetatable(storage, mt)
|
||||
return (storage :: any) :: () -> (number, T...), function()
|
||||
disconnect()
|
||||
end
|
||||
end
|
||||
|
||||
return collect
|
30
demo/src/ReplicatedStorage/std/components.luau
Normal file
|
@ -0,0 +1,30 @@
|
|||
local jecs = require(game:GetService("ReplicatedStorage").ecs)
|
||||
local world = require(script.Parent.world)
|
||||
|
||||
type Entity<T = nil> = jecs.Entity<T>
|
||||
local components: {
|
||||
Character: Entity<Model>,
|
||||
Mob: Entity,
|
||||
Model: Entity<Model>,
|
||||
Player: Entity,
|
||||
Target: Entity,
|
||||
Transform: Entity<{ new: CFrame, old: CFrame }>,
|
||||
Velocity: Entity<number>,
|
||||
Previous: Entity,
|
||||
} =
|
||||
{
|
||||
Character = world:component(),
|
||||
Mob = world:component(),
|
||||
Model = world:component(),
|
||||
Player = world:component(),
|
||||
Target = world:component(),
|
||||
Transform = world:component(),
|
||||
Velocity = world:component(),
|
||||
Previous = world:component(),
|
||||
}
|
||||
|
||||
for name, component in components :: {[string]: jecs.Entity} do
|
||||
world:set(component, jecs.Name, name)
|
||||
end
|
||||
|
||||
return table.freeze(components)
|
19
demo/src/ReplicatedStorage/std/interval.luau
Normal file
|
@ -0,0 +1,19 @@
|
|||
local function interval(s)
|
||||
local pin
|
||||
|
||||
local function throttle()
|
||||
if not pin then
|
||||
pin = os.clock()
|
||||
end
|
||||
|
||||
local elapsed = os.clock() - pin > s
|
||||
if elapsed then
|
||||
pin = os.clock()
|
||||
end
|
||||
|
||||
return elapsed
|
||||
end
|
||||
return throttle
|
||||
end
|
||||
|
||||
return interval
|
14
demo/src/ReplicatedStorage/std/phases.luau
Normal file
|
@ -0,0 +1,14 @@
|
|||
local std = game:GetService("ReplicatedStorage").std
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local scheduler = require(std.scheduler)
|
||||
local PHASE = scheduler.PHASE
|
||||
|
||||
return {
|
||||
PlayerAdded = PHASE({
|
||||
event = Players.PlayerAdded
|
||||
}),
|
||||
PlayerRemoved = PHASE({
|
||||
event = Players.PlayerRemoving
|
||||
})
|
||||
}
|
26
demo/src/ReplicatedStorage/std/ref.luau
Normal file
|
@ -0,0 +1,26 @@
|
|||
local world = require(script.Parent.world)
|
||||
local jecs = require(game:GetService("ReplicatedStorage").ecs)
|
||||
local refs: {[any]: jecs.Entity} = {}
|
||||
|
||||
local function fini(key): () -> ()
|
||||
return function()
|
||||
refs[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function noop() end
|
||||
|
||||
local function ref(key): (jecs.Entity, () -> ())
|
||||
if not key then
|
||||
return world:entity(), noop
|
||||
end
|
||||
local e = refs[key]
|
||||
if not e then
|
||||
e = world:entity()
|
||||
refs[key] = e
|
||||
end
|
||||
-- Cannot cache handles because they will get invalidated
|
||||
return e, fini(key)
|
||||
end
|
||||
|
||||
return ref
|
171
demo/src/ReplicatedStorage/std/scheduler.luau
Normal file
|
@ -0,0 +1,171 @@
|
|||
--!native
|
||||
--!optimize 2
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
local jabby = require(ReplicatedStorage.Packages.jabby)
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local pair = jecs.pair
|
||||
local Name = jecs.Name
|
||||
|
||||
type World = jecs.World
|
||||
type Entity<T = nil> = jecs.Entity<T>
|
||||
type Id<T = unknown> = jecs.Id<T>
|
||||
|
||||
type System = {
|
||||
callback: (world: World) -> (),
|
||||
id: number,
|
||||
}
|
||||
|
||||
type Systems = { System }
|
||||
|
||||
type Events = {
|
||||
RenderStepped: Systems,
|
||||
Heartbeat: Systems,
|
||||
}
|
||||
|
||||
local world = require(script.Parent.world)
|
||||
local Disabled = world:entity()
|
||||
local System = world:component() :: Id<{ callback: (any) -> (), name: string}>
|
||||
local DependsOn = world:entity()
|
||||
local Event = world:component() :: Id<RBXScriptSignal>
|
||||
local Phase = world:entity()
|
||||
|
||||
local PreRender = world:entity()
|
||||
local Heartbeat = world:entity()
|
||||
local PreAnimation = world:entity()
|
||||
local PreSimulation = world:entity()
|
||||
|
||||
local sys: System
|
||||
local dt: number
|
||||
|
||||
local jabby_scheduler = jabby.scheduler.create("Scheduler")
|
||||
|
||||
local a, b, c, d
|
||||
local function run()
|
||||
local id = sys.id
|
||||
jabby_scheduler:run(id, sys.callback, a, b, c, d)
|
||||
return nil
|
||||
end
|
||||
|
||||
world:add(Heartbeat, Phase)
|
||||
world:set(Heartbeat, Event, RunService.Heartbeat)
|
||||
|
||||
world:add(PreSimulation, Phase)
|
||||
world:set(PreSimulation, Event, RunService.PreSimulation)
|
||||
|
||||
world:add(PreAnimation, Phase)
|
||||
world:set(PreAnimation, Event, RunService.PreAnimation)
|
||||
|
||||
jabby.register({
|
||||
applet = jabby.applets.world,
|
||||
name = "MyWorld",
|
||||
configuration = {
|
||||
world = world,
|
||||
},
|
||||
})
|
||||
|
||||
jabby.register({
|
||||
applet = jabby.applets.scheduler,
|
||||
name = "Scheduler",
|
||||
configuration = {
|
||||
scheduler = jabby_scheduler,
|
||||
},
|
||||
})
|
||||
|
||||
if RunService:IsClient() then
|
||||
world:add(PreRender, Phase)
|
||||
world:set(PreRender, Event, (RunService :: RunService).PreRender)
|
||||
end
|
||||
|
||||
local function begin(events: { [RBXScriptSignal]: Systems })
|
||||
local connections = {}
|
||||
for event, systems in events do
|
||||
if not event then
|
||||
continue
|
||||
end
|
||||
local event_name = tostring(event)
|
||||
connections[event] = event:Connect(function(...)
|
||||
debug.profilebegin(event_name)
|
||||
for _, s in systems do
|
||||
sys = s
|
||||
a, b, c, d = ...
|
||||
|
||||
for _ in run do
|
||||
break
|
||||
end
|
||||
|
||||
end
|
||||
debug.profileend()
|
||||
end)
|
||||
end
|
||||
return connections
|
||||
end
|
||||
|
||||
local function scheduler_collect_systems_under_phase_recursive(systems, phase: Entity)
|
||||
local phase_name = world:get(phase, Name)
|
||||
for _, s in world:query(System):with(pair(DependsOn, phase)) do
|
||||
table.insert(systems, {
|
||||
id = jabby_scheduler:register_system({
|
||||
name = s.name,
|
||||
phase = phase_name,
|
||||
} :: any),
|
||||
callback = s.callback,
|
||||
})
|
||||
end
|
||||
for after in world:query(Phase):with(pair(DependsOn, phase)):iter() do
|
||||
scheduler_collect_systems_under_phase_recursive(systems, after)
|
||||
end
|
||||
end
|
||||
|
||||
local function scheduler_collect_systems_under_event(event)
|
||||
local systems = {}
|
||||
scheduler_collect_systems_under_phase_recursive(systems, event)
|
||||
return systems
|
||||
end
|
||||
|
||||
local function scheduler_collect_systems_all()
|
||||
local events = {}
|
||||
for phase, event in world:query(Event):with(Phase) do
|
||||
events[event] = scheduler_collect_systems_under_event(phase)
|
||||
end
|
||||
return events
|
||||
end
|
||||
|
||||
local function scheduler_phase_new(d: { after: Entity?, event: RBXScriptSignal? })
|
||||
local phase = world:entity()
|
||||
world:add(phase, Phase)
|
||||
local after = d.after
|
||||
if after then
|
||||
local dependency = pair(DependsOn, after :: Entity)
|
||||
world:add(phase, dependency)
|
||||
end
|
||||
|
||||
local event = d.event
|
||||
if event then
|
||||
world:set(phase, Event, event)
|
||||
end
|
||||
return phase
|
||||
end
|
||||
|
||||
local function scheduler_systems_new(callback: (any) -> (), phase: Entity?)
|
||||
local system = world:entity()
|
||||
world:set(system, System, { callback = callback, name = debug.info(callback, "n") })
|
||||
local depends_on = DependsOn :: jecs.Entity
|
||||
local p: Entity = phase or Heartbeat
|
||||
world:add(system, pair(depends_on, p))
|
||||
|
||||
return system
|
||||
end
|
||||
|
||||
return {
|
||||
SYSTEM = scheduler_systems_new,
|
||||
BEGIN = begin,
|
||||
PHASE = scheduler_phase_new,
|
||||
COLLECT = scheduler_collect_systems_all,
|
||||
phases = {
|
||||
Heartbeat = Heartbeat,
|
||||
PreSimulation = PreSimulation,
|
||||
PreAnimation = PreAnimation,
|
||||
PreRender = PreRender
|
||||
}
|
||||
}
|
4
demo/src/ReplicatedStorage/std/world.luau
Normal file
|
@ -0,0 +1,4 @@
|
|||
local jecs = require(game:GetService("ReplicatedStorage").ecs)
|
||||
|
||||
-- I like the idea of only having the world be a singleton.
|
||||
return jecs.World.new() :: jecs.World
|
|
@ -1,87 +0,0 @@
|
|||
local types = require("../types")
|
||||
local jecs = require(game:GetService("ReplicatedStorage").ecs)
|
||||
local remotes = require("../remotes")
|
||||
local collect = require("../collect")
|
||||
local client_ids = {}
|
||||
|
||||
|
||||
local function ecs_map_get(world, id)
|
||||
local deserialised_id = client_ids[id]
|
||||
|
||||
if not deserialised_id then
|
||||
if world:has(id, jecs.Name) then
|
||||
deserialised_id = world:entity(id)
|
||||
else
|
||||
deserialised_id = world:entity()
|
||||
end
|
||||
|
||||
client_ids[id] = deserialised_id
|
||||
end
|
||||
|
||||
-- local deserialised_id = client_ids[id]
|
||||
-- if not deserialised_id then
|
||||
-- if world:has(id, jecs.Name) then
|
||||
-- deserialised_id = world:entity(id)
|
||||
-- else
|
||||
-- if world:exists(id) then
|
||||
-- deserialised_id = world:entity()
|
||||
-- else
|
||||
-- deserialised_id = world:entity(id)
|
||||
-- end
|
||||
-- end
|
||||
-- client_ids[id] = deserialised_id
|
||||
-- end
|
||||
|
||||
return deserialised_id
|
||||
end
|
||||
|
||||
local function ecs_make_alive_id(world, id)
|
||||
local rel = jecs.ECS_PAIR_FIRST(id)
|
||||
local tgt = jecs.ECS_PAIR_SECOND(id)
|
||||
|
||||
rel = ecs_map_get(world, rel)
|
||||
tgt = ecs_map_get(world, tgt)
|
||||
|
||||
return jecs.pair(rel, tgt)
|
||||
end
|
||||
|
||||
local snapshots = collect(remotes.replication.OnClientEvent)
|
||||
|
||||
return function(world: types.World)
|
||||
for snapshot in snapshots do
|
||||
for id, map in snapshot do
|
||||
id = tonumber(id)
|
||||
if jecs.IS_PAIR(id) then
|
||||
id = ecs_make_alive_id(world, id)
|
||||
end
|
||||
|
||||
local set = map.set
|
||||
if set then
|
||||
if jecs.is_tag(world, id) then
|
||||
for _, entity in set do
|
||||
entity = ecs_map_get(world, entity)
|
||||
world:add(entity, id)
|
||||
end
|
||||
else
|
||||
local values = map.values
|
||||
for i, entity in set do
|
||||
entity = ecs_map_get(world, entity)
|
||||
world:set(entity, id, values[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local removed = map.removed
|
||||
|
||||
if removed then
|
||||
for _, entity in removed do
|
||||
entity = ecs_map_get(world, entity)
|
||||
if not world:contains(entity) then
|
||||
continue
|
||||
end
|
||||
world:remove(entity, id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
local jecs = require(game:GetService("ReplicatedStorage").ecs)
|
||||
local observers_add = require("../ReplicatedStorage/observers_add")
|
||||
|
||||
export type World = typeof(observers_add(jecs.world()))
|
||||
export type Entity = jecs.Entity
|
||||
export type Id<T> = jecs.Id<T>
|
||||
export type Snapshot = {
|
||||
[string]: {
|
||||
set: { jecs.Entity }?,
|
||||
values: { any }?,
|
||||
removed: { jecs.Entity }?
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
19
demo/src/ServerScriptService/main.server.luau
Executable file → Normal file
|
@ -1,19 +1,4 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ServerScriptService = game:GetService("ServerScriptService")
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local schedule = require(ReplicatedStorage.schedule)
|
||||
local observers_add = require(ReplicatedStorage.observers_add)
|
||||
local start = require(ReplicatedStorage.start)
|
||||
|
||||
local SYSTEM = schedule.SYSTEM
|
||||
local RUN = schedule.RUN
|
||||
|
||||
require(ReplicatedStorage.components)
|
||||
local world = observers_add(jecs.world())
|
||||
|
||||
local systems = ServerScriptService.systems
|
||||
|
||||
SYSTEM(world, systems.replication)
|
||||
SYSTEM(world, systems.players_added)
|
||||
SYSTEM(world, systems.poison_hurts)
|
||||
SYSTEM(world, systems.life_is_painful)
|
||||
RUN(world, 0)
|
||||
start(script.Parent:WaitForChild("systems"):GetChildren())
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ct = require(ReplicatedStorage.components)
|
||||
local types = require(ReplicatedStorage.types)
|
||||
|
||||
return function(world: types.World, dt: number)
|
||||
for e in world:query(ct.Player):without(ct.Health) do
|
||||
world:set(e, ct.Health, 100)
|
||||
end
|
||||
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
|
||||
world:set(e, ct.Poison, 10)
|
||||
end
|
||||
end
|
88
demo/src/ServerScriptService/systems/mobs.luau
Normal file
|
@ -0,0 +1,88 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
--!strict
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local blink = require(game:GetService("ServerScriptService").net)
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local __ = jecs.Wildcard
|
||||
local std = ReplicatedStorage.std
|
||||
local ref = require(std.ref)
|
||||
local interval = require(std.interval)
|
||||
|
||||
local world = require(std.world)
|
||||
local cts = require(std.components)
|
||||
|
||||
local Mob = cts.Mob
|
||||
local Transform = cts.Transform
|
||||
local Velocity = cts.Velocity
|
||||
local Player = cts.Player
|
||||
local Character = cts.Character
|
||||
|
||||
local characters = world
|
||||
:query(Character)
|
||||
:with(Player)
|
||||
:cached()
|
||||
|
||||
|
||||
local moving_mobs = world
|
||||
:query(Transform, Velocity)
|
||||
:with(Mob)
|
||||
:cached()
|
||||
|
||||
|
||||
local function mobsMove(dt: number)
|
||||
local targets = {}
|
||||
|
||||
for _, character in characters do
|
||||
table.insert(targets, (character.PrimaryPart :: Part).Position)
|
||||
end
|
||||
|
||||
for mob, transform, v in moving_mobs do
|
||||
local cf = transform.new
|
||||
local p = cf.Position
|
||||
|
||||
local target
|
||||
local closest
|
||||
|
||||
for _, pos in targets do
|
||||
local distance = (p - pos).Magnitude
|
||||
if not target or distance < closest then
|
||||
target = pos
|
||||
closest = distance
|
||||
end
|
||||
end
|
||||
|
||||
if not target then
|
||||
continue
|
||||
end
|
||||
|
||||
local moving = CFrame.new(p + (target - p).Unit * dt * v)
|
||||
transform.new = moving
|
||||
blink.UpdateTransform.FireAll(mob, moving)
|
||||
end
|
||||
end
|
||||
|
||||
local throttle = interval(5)
|
||||
|
||||
local function spawnMobs()
|
||||
if throttle() then
|
||||
local p = Vector3.new(0, 5, 0)
|
||||
local cf = CFrame.new(p)
|
||||
local v = 5
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, Velocity, v)
|
||||
world:set(e, Transform, { new = cf })
|
||||
world:add(e, Mob)
|
||||
|
||||
blink.SpawnMob.FireAll(e, cf, v)
|
||||
end
|
||||
end
|
||||
|
||||
local scheduler = require(std.scheduler)
|
||||
|
||||
scheduler.SYSTEM(spawnMobs)
|
||||
scheduler.SYSTEM(mobsMove)
|
||||
|
||||
return 0
|
40
demo/src/ServerScriptService/systems/players.luau
Normal file
|
@ -0,0 +1,40 @@
|
|||
local Players = game:GetService("Players")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local std = ReplicatedStorage.std
|
||||
local ref = require(std.ref)
|
||||
local collect = require(std.collect)
|
||||
|
||||
local cts = require(std.components)
|
||||
local world = require(std.world)
|
||||
local Player = cts.Player
|
||||
local Character = cts.Character
|
||||
|
||||
local conn = {}
|
||||
|
||||
local function playersAdded(player: Player)
|
||||
local e = ref(player.UserId)
|
||||
world:set(e, Player, player)
|
||||
local characterAdd = player.CharacterAdded
|
||||
conn[e] = characterAdd:Connect(function(rig)
|
||||
while rig.Parent ~= workspace do
|
||||
task.wait()
|
||||
end
|
||||
world:set(e, Character, rig)
|
||||
end)
|
||||
end
|
||||
|
||||
local function playersRemoved(player: Player)
|
||||
local e = ref(player.UserId)
|
||||
world:clear(e)
|
||||
local connection = conn[e]
|
||||
connection:Disconnect()
|
||||
conn[e] = nil
|
||||
end
|
||||
|
||||
local scheduler = require(std.scheduler)
|
||||
local phases = require(std.phases)
|
||||
scheduler.SYSTEM(playersAdded, phases.PlayerAdded)
|
||||
scheduler.SYSTEM(playersRemoved, phases.PlayerRemoved)
|
||||
|
||||
return 0
|
|
@ -1,20 +0,0 @@
|
|||
local collect = require("../../ReplicatedStorage/collect")
|
||||
local types = require("../../ReplicatedStorage/types")
|
||||
local ct = require("../../ReplicatedStorage/components")
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local player_added = collect(Players.PlayerAdded)
|
||||
return function(world: types.World, dt: number)
|
||||
for player in player_added do
|
||||
local entity = world:entity()
|
||||
world:set(entity, ct.Player, player)
|
||||
end
|
||||
|
||||
for entity, player in world:query(ct.Player):without(ct.Renderable) do
|
||||
local character = player.Character
|
||||
if character then
|
||||
if not character.Parent then
|
||||
world:set(entity, ct.Renderable, character)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local ct = require(ReplicatedStorage.components)
|
||||
return function(world, dt)
|
||||
for e, poison, health in world:query(ct.Poison, ct.Health) do
|
||||
local health_after_tick = health - poison * dt * 0.05
|
||||
if health_after_tick < 0 then
|
||||
world:remove(e, ct.Health)
|
||||
continue
|
||||
end
|
||||
world:set(e, ct.Health, health_after_tick)
|
||||
end
|
||||
end
|
|
@ -1,190 +0,0 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local types = require("../../ReplicatedStorage/types")
|
||||
local ct = require("../../ReplicatedStorage/components")
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local remotes = require("../../ReplicatedStorage/remotes")
|
||||
local components = ct :: {[string]: jecs.Entity }
|
||||
|
||||
return function(world: ty.World)
|
||||
|
||||
--- integration test
|
||||
|
||||
-- for _ = 1, 10 do
|
||||
-- local e = world:entity()
|
||||
-- world:set(e, ct.TestA, true)
|
||||
-- end
|
||||
|
||||
local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }}
|
||||
local networked_components = {}
|
||||
local networked_pairs = {}
|
||||
|
||||
for component in world:each(ct.Networked) do
|
||||
local name = world:get(component, jecs.Name) :: string
|
||||
if components[name] == nil then
|
||||
continue
|
||||
end
|
||||
|
||||
storages[component] = {}
|
||||
|
||||
table.insert(networked_components, component)
|
||||
end
|
||||
|
||||
for relation in world:each(ct.NetworkedPair) do
|
||||
local name = world:get(relation, jecs.Name) :: string
|
||||
if not components[name] then
|
||||
continue
|
||||
end
|
||||
table.insert(networked_pairs, relation)
|
||||
end
|
||||
|
||||
for _, component in networked_components do
|
||||
local name = world:get(component, jecs.Name) :: string
|
||||
if not components[name] then
|
||||
error(`Networked Component (%id{component}%name{name})`)
|
||||
end
|
||||
local is_tag = jecs.is_tag(world, component)
|
||||
local storage = storages[component]
|
||||
if is_tag then
|
||||
world:added(component, function(entity)
|
||||
storage[entity] = true
|
||||
end)
|
||||
else
|
||||
world:added(component, function(entity, _, value)
|
||||
storage[entity] = value
|
||||
end)
|
||||
world:changed(component, function(entity, _, value)
|
||||
storage[entity] = value
|
||||
end)
|
||||
end
|
||||
|
||||
world:removed(component, function(entity)
|
||||
storage[entity] = "jecs.Remove"
|
||||
end)
|
||||
end
|
||||
|
||||
for _, relation in networked_pairs do
|
||||
world:added(relation, function(entity, id, value)
|
||||
local is_tag = jecs.is_tag(world, id)
|
||||
local storage = storages[id]
|
||||
if not storage then
|
||||
storage = {}
|
||||
storages[id] = storage
|
||||
end
|
||||
if is_tag then
|
||||
storage[entity] = true
|
||||
else
|
||||
storage[entity] = value
|
||||
end
|
||||
end)
|
||||
|
||||
world:changed(relation, function(entity, id, value)
|
||||
local is_tag = jecs.is_tag(world, id)
|
||||
if is_tag then
|
||||
return
|
||||
end
|
||||
|
||||
local storage = storages[id]
|
||||
if not storage then
|
||||
storage = {}
|
||||
storages[id] = storage
|
||||
end
|
||||
|
||||
storage[entity] = value
|
||||
end)
|
||||
|
||||
world:removed(relation, function(entity, id)
|
||||
local storage = storages[id]
|
||||
if not storage then
|
||||
storage = {}
|
||||
storages[id] = storage
|
||||
end
|
||||
|
||||
storage[entity] = "jecs.Remove"
|
||||
end)
|
||||
end
|
||||
|
||||
local players_added = collect(Players.PlayerAdded)
|
||||
|
||||
return function()
|
||||
local snapshot_lazy: ty.Snapshot
|
||||
local set_ids_lazy: { jecs.Entity }
|
||||
|
||||
for player in players_added do
|
||||
if not snapshot_lazy then
|
||||
snapshot_lazy, set_ids_lazy = {}, {}
|
||||
|
||||
for component, storage in storages do
|
||||
local set_values = {}
|
||||
local set_n = 0
|
||||
|
||||
local q = world:query(component)
|
||||
local is_tag = jecs.is_tag(world, component)
|
||||
for _, archetype in q:archetypes() do
|
||||
local entities = archetype.entities
|
||||
local entities_len = #entities
|
||||
table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy)
|
||||
if is_tag then
|
||||
set_values = table.create(entities_len, true)
|
||||
else
|
||||
local column = archetype.columns[archetype.records[component]]
|
||||
table.move(column, 1, entities_len, set_n + 1, set_values)
|
||||
end
|
||||
|
||||
set_n += entities_len
|
||||
end
|
||||
|
||||
local set = table.move(set_ids_lazy, 1, set_n, 1, {})
|
||||
|
||||
snapshot_lazy[tostring(component)] = {
|
||||
set = if set_n > 0 then set else nil,
|
||||
values = if set_n > 0 then set_values else nil,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
remotes.replication:FireClient(player, snapshot_lazy)
|
||||
end
|
||||
|
||||
local snapshot = {} :: ty.Snapshot
|
||||
|
||||
local set_ids = {}
|
||||
local removed_ids = {}
|
||||
|
||||
for component, storage in storages do
|
||||
local set_values = {} :: { any }
|
||||
local set_n = 0
|
||||
local removed_n = 0
|
||||
for e, v in storage do
|
||||
if v ~= "jecs.Remove" then
|
||||
set_n += 1
|
||||
set_ids[set_n] = e
|
||||
set_values[set_n] = v or true
|
||||
elseif world:contains(e) then
|
||||
removed_n += 1
|
||||
removed_ids[removed_n] = e
|
||||
end
|
||||
end
|
||||
|
||||
table.clear(storage)
|
||||
|
||||
local dirty = false
|
||||
|
||||
if set_n > 0 or removed_n > 0 then
|
||||
dirty = true
|
||||
end
|
||||
|
||||
if dirty then
|
||||
local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity }
|
||||
local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity }
|
||||
snapshot[tostring(component)] = {
|
||||
set = if set_n > 0 then set else nil,
|
||||
values = if set_n > 0 then set_values else nil,
|
||||
removed = if removed_n > 0 then removed else nil
|
||||
}
|
||||
end
|
||||
end
|
||||
if next(snapshot) ~= nil then
|
||||
remotes.replication:FireAllClients(snapshot)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local start = require(ReplicatedStorage.start)
|
||||
|
||||
start(script.Parent:WaitForChild("systems"):GetChildren())
|
67
demo/src/StarterPlayer/StarterPlayerScripts/systems/lol.luau
Normal file
|
@ -0,0 +1,67 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
--!strict
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local __ = jecs.Wildcard
|
||||
local std = ReplicatedStorage.std
|
||||
|
||||
local world = require(std.world)
|
||||
|
||||
local Position = world:component() :: jecs.Entity<vector>
|
||||
local Previous = jecs.Rest
|
||||
local pre = jecs.pair(Position, Previous)
|
||||
|
||||
local added = world
|
||||
:query(Position)
|
||||
:without(pre)
|
||||
:cached()
|
||||
local changed = world
|
||||
:query(Position, pre)
|
||||
:cached()
|
||||
local removed = world
|
||||
:query(pre)
|
||||
:without(Position)
|
||||
:cached()
|
||||
|
||||
local children = {}
|
||||
for i = 1, 10 do
|
||||
local e = world:entity()
|
||||
world:set(e, Position, vector.create(i, i, i))
|
||||
table.insert(children, e)
|
||||
end
|
||||
local function flip()
|
||||
return math.random() > 0.5
|
||||
end
|
||||
local function system()
|
||||
for i, child in children do
|
||||
world:set(child, Position, vector.create(i,i,i))
|
||||
end
|
||||
for e, p in added:iter() do
|
||||
world:set(e, pre, p)
|
||||
end
|
||||
for i, child in children do
|
||||
if flip() then
|
||||
world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
|
||||
end
|
||||
end
|
||||
for e, new, old in changed:iter() do
|
||||
if new ~= old then
|
||||
world:set(e, pre, new)
|
||||
end
|
||||
end
|
||||
|
||||
for i, child in children do
|
||||
world:remove(child, Position)
|
||||
end
|
||||
|
||||
for e in removed:iter() do
|
||||
world:remove(e, pre)
|
||||
end
|
||||
end
|
||||
local scheduler = require(std.scheduler)
|
||||
|
||||
scheduler.SYSTEM(system)
|
||||
|
||||
return 0
|
|
@ -0,0 +1,90 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
--!strict
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local jecs = require(ReplicatedStorage.ecs)
|
||||
local __ = jecs.Wildcard
|
||||
local std = ReplicatedStorage.std
|
||||
|
||||
local world = require(std.world)
|
||||
|
||||
local Position = world:component() :: jecs.Entity<vector>
|
||||
local Previous = jecs.Rest
|
||||
local pre = jecs.pair(Position, Previous)
|
||||
|
||||
local added = world
|
||||
:query(Position)
|
||||
:without(pre)
|
||||
:cached()
|
||||
local changed = world
|
||||
:query(Position, pre)
|
||||
:cached()
|
||||
local removed = world
|
||||
:query(pre)
|
||||
:without(Position)
|
||||
:cached()
|
||||
|
||||
local children = {}
|
||||
for i = 1, 10 do
|
||||
local e = world:entity()
|
||||
world:set(e, Position, vector.create(i, i, i))
|
||||
table.insert(children, e)
|
||||
end
|
||||
local function flip()
|
||||
return math.random() > 0.5
|
||||
end
|
||||
local entity_index = world.entity_index
|
||||
local function copy(archetypes, id)
|
||||
for _, archetype in archetypes do
|
||||
|
||||
local to = jecs.archetype_traverse_add(world, pre, archetype)
|
||||
local columns = to.columns
|
||||
local records = to.records
|
||||
local old = columns[records[pre].column]
|
||||
local new = columns[records[id].column]
|
||||
|
||||
if to ~= archetype then
|
||||
for _, entity in archetype.entities do
|
||||
local r = jecs.entity_index_try_get_fast(entity_index, entity)
|
||||
jecs.entity_move(entity_index, entity, r, to)
|
||||
end
|
||||
end
|
||||
|
||||
table.move(new, 1, #new, 1, old)
|
||||
|
||||
end
|
||||
end
|
||||
local function system2()
|
||||
for i, child in children do
|
||||
world:set(child, Position, vector.create(i,i,i))
|
||||
end
|
||||
for e, p in added:iter() do
|
||||
end
|
||||
copy(added:archetypes(), Position)
|
||||
for i, child in children do
|
||||
if flip() then
|
||||
world:set(child, Position, vector.create(i + 1, i + 1, i + 1))
|
||||
end
|
||||
end
|
||||
|
||||
for e, new, old in changed:iter() do
|
||||
if new ~= old then
|
||||
end
|
||||
end
|
||||
|
||||
copy(changed:archetypes(), Position)
|
||||
|
||||
for i, child in children do
|
||||
world:remove(child, Position)
|
||||
end
|
||||
|
||||
for e in removed:iter() do
|
||||
world:remove(e, pre)
|
||||
end
|
||||
end
|
||||
local scheduler = require(std.scheduler)
|
||||
|
||||
scheduler.SYSTEM(system2)
|
||||
|
||||
return 0
|
|
@ -0,0 +1,46 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local blink = require(ReplicatedStorage.net)
|
||||
local std = ReplicatedStorage.std
|
||||
local world = require(std.world)
|
||||
local ref = require(std.ref)
|
||||
|
||||
local cts = require(std.components)
|
||||
|
||||
local Model = cts.Model
|
||||
local Transform = cts.Transform
|
||||
|
||||
local moved_models = world:query(Model, Transform):cached()
|
||||
local updated_models = {}
|
||||
local i = 0
|
||||
local function processed(n)
|
||||
i += 1
|
||||
if i > n then
|
||||
i = 0
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function move(dt: number)
|
||||
for entity, model in moved_models do
|
||||
if updated_models[entity] then
|
||||
updated_models[entity] = nil
|
||||
model.PrimaryPart.CFrame = transform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function syncTransforms()
|
||||
for _, id, cf in blink.UpdateTransform.Iter() do
|
||||
local e = ref("server-" .. tostring(id))
|
||||
world:set(e, Transform, cf)
|
||||
moved_models[e] = true
|
||||
end
|
||||
end
|
||||
|
||||
local scheduler = require(std.scheduler)
|
||||
|
||||
scheduler.SYSTEM(move)
|
||||
scheduler.SYSTEM(syncTransforms)
|
||||
|
||||
return 0
|
|
@ -0,0 +1,31 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local blink = require(ReplicatedStorage.net)
|
||||
local std = ReplicatedStorage.std
|
||||
local ref = require(std.ref)
|
||||
local world = require(std.world)
|
||||
local cts = require(std.components)
|
||||
|
||||
local function syncMobs()
|
||||
for _, id, cf, vel in blink.SpawnMob.Iter() do
|
||||
local part = Instance.new("Part")
|
||||
part.Size = Vector3.one * 5
|
||||
part.BrickColor = BrickColor.Red()
|
||||
part.Anchored = true
|
||||
local model = Instance.new("Model")
|
||||
model.PrimaryPart = part
|
||||
part.Parent = model
|
||||
model.Parent = workspace
|
||||
|
||||
local e = ref("server-" .. tostring(id))
|
||||
world:set(e, cts.Transform, { new = cf, old = cf })
|
||||
world:set(e, cts.Velocity, vel)
|
||||
world:set(e, cts.Model, model)
|
||||
world:add(e, cts.Mob)
|
||||
end
|
||||
end
|
||||
|
||||
local scheduler = require(std.scheduler)
|
||||
scheduler.SYSTEM(syncMobs)
|
||||
|
||||
return 0
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local std = ReplicatedStorage.std
|
||||
local world = require(std.world)
|
||||
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
|
||||
local function flip()
|
||||
return math.random() >= 0.15
|
||||
end
|
||||
|
||||
for i = 1, 2^8 do
|
||||
local e = world:entity()
|
||||
if flip() then
|
||||
world:set(e, A, true)
|
||||
end
|
||||
if flip() then
|
||||
world:set(e, B, true)
|
||||
end
|
||||
if flip() then
|
||||
world:set(e, C, true)
|
||||
end
|
||||
if flip() then
|
||||
world:set(e, D, true)
|
||||
end
|
||||
end
|
||||
|
||||
local function uncached()
|
||||
for _ in world:query(A, B, C, D) do
|
||||
end
|
||||
end
|
||||
|
||||
local q = world:query(A, B, C, D):cached()
|
||||
local function cached()
|
||||
for _ in q do
|
||||
end
|
||||
end
|
||||
|
||||
local scheduler = require(std.scheduler)
|
||||
scheduler.SYSTEM(uncached)
|
||||
scheduler.SYSTEM(cached)
|
||||
return 0
|
2
demo/wally.toml
Executable file → Normal file
|
@ -5,4 +5,4 @@ registry = "https://github.com/UpliftGames/wally-index"
|
|||
realm = "shared"
|
||||
|
||||
[dependencies]
|
||||
jabby = "alicesaidhi/jabby@0.2.2"
|
||||
jabby = "alicesaidhi/jabby@0.2.0-rc.9"
|
||||
|
|
109
docs/.vitepress/config.mts
Executable file → Normal file
|
@ -1,50 +1,69 @@
|
|||
import { defineConfig } from "vitepress";
|
||||
import { defineConfig } from 'vitepress'
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "Jecs",
|
||||
base: "/jecs/",
|
||||
description: "A VitePress Site",
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: "Learn", link: "/learn/overview.md" },
|
||||
{ text: "API", link: "/api/jecs.md" },
|
||||
{ text: "Resources", link: "/resources" },
|
||||
],
|
||||
title: "Jecs",
|
||||
base: "/jecs/",
|
||||
description: "A VitePress Site",
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Learn', link: '/' },
|
||||
{ text: 'API', link: '/api/jecs.md' },
|
||||
{ text: 'Examples', link: 'https://github.com/Ukendio/jecs/tree/main/examples' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
"/api/": [
|
||||
{
|
||||
text: "Namespaces",
|
||||
items: [
|
||||
{ text: "jecs", link: "/api/jecs" },
|
||||
{ text: "World", link: "/api/world" },
|
||||
{ text: "Query", link: "/api/query" },
|
||||
],
|
||||
},
|
||||
],
|
||||
"/learn/": [
|
||||
{
|
||||
text: "API Reference",
|
||||
items: [
|
||||
{ text: "jecs", link: "/api/jecs" },
|
||||
{ text: "World", link: "/api/world" },
|
||||
{ text: "Query", link: "/api/query" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Contributing",
|
||||
items: [
|
||||
{ text: "Contribution Guidelines", link: "/learn/contributing/guidelines" },
|
||||
{ text: "Submitting Issues", link: "/learn/contributing/issues" },
|
||||
{ text: "Submitting Pull Requests", link: "/learn/contributing/pull-requests" },
|
||||
{ text: "Code Coverage", link: "/learn/contributing/coverage" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
sidebar: {
|
||||
"/api/": [
|
||||
{
|
||||
text: "API reference",
|
||||
items: [
|
||||
{ text: "jecs", link: "/api/jecs" },
|
||||
{ text: "World", link: "/api/world" },
|
||||
{ text: "Query", link: "/api/query" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"/learn/": [
|
||||
{
|
||||
text: "Introduction",
|
||||
items: [
|
||||
{ text: 'Getting Started', link: '/learn/overview/get-started' },
|
||||
{ text: 'First Jecs Project', link: '/learn/overview/first-jecs-project' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Concepts',
|
||||
items: [
|
||||
{ text: 'Entities and Components', link: '/learn/concepts/entities-and-components' },
|
||||
{ text: 'Queries', link: '/learn/concepts/queries' },
|
||||
{ text: 'Relationships', link: '/learn/concepts/relationships' },
|
||||
{ text: 'Component Traits', link: 'learn/concepts/component-traits' },
|
||||
{ text: 'Addons', link: '/learn/concepts/addons' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "FAQ",
|
||||
items: [
|
||||
{ text: 'How can I contribute?', link: '/learn/faq/contributing' }
|
||||
]
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: "github", link: "https://github.com/ukendio/jecs" }],
|
||||
},
|
||||
});
|
||||
],
|
||||
"/contributing/": [
|
||||
{
|
||||
text: 'Contributing',
|
||||
items: [
|
||||
{ text: 'Contribution Guidelines', link: '/learn/contributing/guidelines' },
|
||||
{ text: 'Submitting Issues', link: '/learn/contributing/issues' },
|
||||
{ text: 'Submitting Pull Requests', link: '/learn/contributing/pull-requests' },
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/ukendio/jecs' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
|
2
docs/api/jecs.md
Executable file → Normal file
|
@ -45,6 +45,6 @@ function jecs.pair(
|
|||
```
|
||||
::: info
|
||||
|
||||
While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
|
||||
Note that while relationship pairs can be used as components, meaning you can add data with it as an ID, however they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
|
||||
|
||||
:::
|
||||
|
|
0
docs/api/query.md
Executable file → Normal file
23
docs/api/world.md
Executable file → Normal file
|
@ -34,23 +34,24 @@ const myOtherWorld = new World();
|
|||
|
||||
## entity
|
||||
|
||||
Creates a new entity. It accepts the overload to create an entity with a specific ID.
|
||||
Creates a new entity.
|
||||
|
||||
```luau
|
||||
function World:entity<T>(
|
||||
id: Entity<T>? -- The desired id
|
||||
): Entity<T>
|
||||
function World:entity(): Entity
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local entity = world:entity()
|
||||
```
|
||||
|
||||
```ts [typescript]
|
||||
const entity = world.entity();
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## component
|
||||
|
@ -463,19 +464,23 @@ function World:each(
|
|||
```
|
||||
|
||||
Example:
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local id = world:entity()
|
||||
for entity in world:each(id) do
|
||||
-- Do something
|
||||
end
|
||||
```
|
||||
|
||||
```ts [typescript]
|
||||
const id = world.entity();
|
||||
for (const entity of world.each(id)) {
|
||||
// Do something
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## children
|
||||
|
@ -493,13 +498,3 @@ This is the same as calling:
|
|||
```luau
|
||||
world:each(pair(ChildOf, parent))
|
||||
```
|
||||
|
||||
## range
|
||||
|
||||
Enforces a check for entities to be created within a desired range.
|
||||
```luau
|
||||
function World:range(
|
||||
range_begin: number -- The starting point,
|
||||
range_begin: number? -- The end point (optional)
|
||||
)
|
||||
```
|
||||
|
|
3
docs/contributing/guidelines.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## TODO
|
||||
|
||||
This is a TODO stub.
|
3
docs/contributing/issues.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## TODO
|
||||
|
||||
This is a TODO stub.
|
3
docs/contributing/pull-requests.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## TODO
|
||||
|
||||
This is a TODO stub.
|
6
docs/index.md
Executable file → Normal file
|
@ -10,8 +10,8 @@ hero:
|
|||
alt: Jecs logo
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Overview
|
||||
link: learn/overview.md
|
||||
text: Get Started
|
||||
link: learn/overview/get-started.md
|
||||
- theme: alt
|
||||
text: API References
|
||||
link: /api/jecs.md
|
||||
|
@ -19,7 +19,7 @@ hero:
|
|||
features:
|
||||
- title: Stupidly Fast
|
||||
icon: 🔥
|
||||
details: Iterates 800,000 entities at 60 frames per second.
|
||||
details: Iterates 500,000 entities at 60 frames per second.
|
||||
- title: Strictly Typed API
|
||||
icon: 🔒
|
||||
details: Has typings for both Luau and Typescript.
|
||||
|
|
28
docs/learn/concepts/addons.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Addons
|
||||
|
||||
A collection of third-party jecs addons made by the community. If you would like to share what you're working on, [submit a pull request](https://github.com/Ukendio/jecs)!
|
||||
|
||||
# Debuggers
|
||||
|
||||
## [jabby](https://github.com/alicesaidhi/jabby)
|
||||
|
||||
A jecs debugger with a string-based query language and entity editing capabilities.
|
||||
|
||||
# Schedulers
|
||||
|
||||
## [lockstep scheduler](https://gist.github.com/1Axen/6d4f78b3454cf455e93794505588354b)
|
||||
|
||||
A simple fixed step system scheduler.
|
||||
|
||||
## [rubine](https://github.com/Mark-Marks/rubine)
|
||||
|
||||
An ergonomic, runtime agnostic scheduler for Jecs
|
||||
|
||||
## [jam](https://github.com/revvy02/Jam)
|
||||
|
||||
Provides hooks and a scheduler that implements jabby and a topographical runtime
|
||||
|
||||
## [planck](https://github.com/YetAnotherClown/planck)
|
||||
|
||||
An agnostic scheduler inspired by Bevy and Flecs, with core features including phases, pipelines, run conditions, and startup systems.
|
||||
Planck also provides plugins for Jabby, Matter Hooks, and more.
|
185
docs/learn/concepts/component-traits.md
Normal file
|
@ -0,0 +1,185 @@
|
|||
# Component Traits
|
||||
|
||||
Component traits are IDs and pairs that can be added to components to modify their behavior. Although it is possible to create custom traits, this manual only contains an overview of all builtin component traits supported by Jecs.
|
||||
|
||||
# Component
|
||||
|
||||
Every (component) ID comes with a `Component` which helps with the distinction between normal entities and component IDs.
|
||||
|
||||
# Tag
|
||||
|
||||
A (component) ID can be marked with `Tag´ in which the component will never contain any data. This allows for zero-cost components which improves performance for structural changes.
|
||||
|
||||
# Hooks
|
||||
|
||||
Hooks are part of the "interface" of a component. You could consider hooks as the counterpart to OOP methods in ECS. They define the behavior of a component, but can only be invoked through mutations on the component data. You can only configure a single `OnAdd`, `OnRemove` and `OnSet` hook per component, just like you can only have a single constructor and destructor.
|
||||
|
||||
## Examples
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local Transform= world:component()
|
||||
world:set(Transform, OnAdd, function(entity)
|
||||
-- A transform component has been added to an entity
|
||||
end)
|
||||
world:set(Transform, OnRemove, function(entity)
|
||||
-- A transform component has been removed from the entity
|
||||
end)
|
||||
world:set(Transform, OnSet, function(entity, value)
|
||||
-- A transform component has been assigned/changed to value on the entity
|
||||
end)
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Transform = world.component();
|
||||
world.set(Transform, OnAdd, (entity) => {
|
||||
// A transform component has been added to an entity
|
||||
});
|
||||
world.set(Transform, OnRemove, (entity) => {
|
||||
// A transform component has been removed from the entity
|
||||
});
|
||||
world.set(Transform, OnSet, (entity, value) => {
|
||||
// A transform component has been assigned/changed to value on the entity
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
# Cleanup Traits
|
||||
|
||||
When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references.
|
||||
|
||||
We also want to specify this per relationship. If an entity has `(Likes, parent)` we may not want to delete that entity, meaning the cleanup we want to perform for `Likes` and `ChildOf` may not be the same.
|
||||
|
||||
This is what cleanup traits are for: to specify which action needs to be executed under which condition. They are applied to entities that have a reference to the entity being deleted: if I delete the `Archer` tag I remove the tag from all entities that have it.
|
||||
|
||||
To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed.
|
||||
|
||||
There are two cleanup actions:
|
||||
|
||||
- `Remove`: removes instances of the specified (component) id from all entities (default)
|
||||
- `Delete`: deletes all entities with specified id
|
||||
|
||||
There are two cleanup conditions:
|
||||
|
||||
- `OnDelete`: the component, tag or relationship is deleted
|
||||
- `OnDeleteTarget`: a target used with the relationship is deleted
|
||||
|
||||
## Examples
|
||||
|
||||
The following examples show how to use cleanup traits
|
||||
|
||||
### (OnDelete, Remove)
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local Archer = world:component()
|
||||
world:add(Archer, pair(jecs.OnDelete, jecs.Remove))
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, Archer)
|
||||
|
||||
-- This will remove Archer from e
|
||||
world:delete(Archer)
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Archer = world.component();
|
||||
world.add(Archer, pair(jecs.OnDelete, jecs.Remove));
|
||||
|
||||
const e = world.entity();
|
||||
world.add(e, Archer);
|
||||
|
||||
// This will remove Archer from e
|
||||
world.delete(Archer);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### (OnDelete, Delete)
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local Archer = world:component()
|
||||
world:add(Archer, pair(jecs.OnDelete, jecs.Delete))
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, Archer)
|
||||
|
||||
-- This will delete entity e because the Archer component has a (OnDelete, Delete) cleanup trait
|
||||
world:delete(Archer)
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Archer = world.component();
|
||||
world.add(Archer, pair(jecs.OnDelete, jecs.Delete));
|
||||
|
||||
const e = world.entity();
|
||||
world.add(e, Archer);
|
||||
|
||||
// This will delete entity e because the Archer component has a (OnDelete, Delete) cleanup trait
|
||||
world.delete(Archer);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### (OnDeleteTarget, Remove)
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local OwnedBy = world:component()
|
||||
world:add(OwnedBy, pair(jecs.OnDeleteTarget, jecs.Remove))
|
||||
local loot = world:entity()
|
||||
local player = world:entity()
|
||||
world:add(loot, pair(OwnedBy, player))
|
||||
|
||||
-- This will remove (OwnedBy, player) from loot
|
||||
world:delete(player)
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const OwnedBy = world.component();
|
||||
world.add(OwnedBy, pair(jecs.OnDeleteTarget, jecs.Remove));
|
||||
const loot = world.entity();
|
||||
const player = world.entity();
|
||||
world.add(loot, pair(OwnedBy, player));
|
||||
|
||||
// This will remove (OwnedBy, player) from loot
|
||||
world.delete(player);
|
||||
```
|
||||
|
||||
### (OnDeleteTarget, Delete)
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local ChildOf = world:component()
|
||||
world:add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
|
||||
local parent = world:entity()
|
||||
local child = world:entity()
|
||||
world:add(child, pair(ChildOf, parent))
|
||||
|
||||
-- This will delete both parent and child
|
||||
world:delete(parent)
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const ChildOf = world.component();
|
||||
world.add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete));
|
||||
|
||||
const parent = world.entity();
|
||||
const child = world.entity();
|
||||
world.add(child, pair(ChildOf, parent));
|
||||
|
||||
// This will delete both parent and child
|
||||
world.delete(parent);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
This page takes wording and terminology directly from Flecs [documentation](https://www.flecs.dev/flecs/md_docs_2ComponentTraits.html)
|
140
docs/learn/concepts/entities-and-components.md
Normal file
|
@ -0,0 +1,140 @@
|
|||
# Entities and Components
|
||||
|
||||
## Entities
|
||||
|
||||
Entities represent things in a game. In a game there may be entities of characters, buildings, projectiles, particle effects etc.
|
||||
|
||||
By itself, an entity is just an unique identifier without any data
|
||||
|
||||
## Components
|
||||
|
||||
A component is something that is added to an entity. Components can simply tag an entity ("this entity is an `Npc`"), attach data to an entity ("this entity is at `Position` `Vector3.new(10, 20, 30)`") and create relationships between entities ("bob `Likes` alice") that may also contain data ("bob `Eats` `10` apples").
|
||||
|
||||
## Operations
|
||||
|
||||
| Operation | Description |
|
||||
| --------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `get` | Get a specific component or set of components from an entity. |
|
||||
| `add` | Adds component to an entity. If entity already has the component, `add` does nothing. |
|
||||
| `set` | Sets the value of a component for an entity. `set` behaves as a combination of `add` and `get` |
|
||||
| `remove` | Removes component from entity. If entity doesn't have the component, `remove` does nothing. |
|
||||
| `clear` | Remove all components from an entity. Clearing is more efficient than removing one by one. |
|
||||
|
||||
## Components are entities
|
||||
|
||||
In an ECS, components need to be uniquely identified. In Jecs this is done by making each component its own unique entity. If a game has a component Position and Velocity, there will be two entities, one for each component. Component entities can be distinguished from "regular" entities as they have a `Component` component. An example:
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local Position = world:component() :: jecs.Entity<Vector3>
|
||||
print(world:has(Position, jecs.Component))
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Position = world.component<Vector3>();
|
||||
print(world.has(Position, jecs.Component));
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
All of the APIs that apply to regular entities also apply to component entities. This means it is possible to contexualize components with logic by adding traits to components
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local Networked = world:component()
|
||||
local Type = world:component()
|
||||
local Name = world:component()
|
||||
local Position = world:component() :: jecs.Entity<Vector3>
|
||||
world:add(Position, Networked)
|
||||
world:set(Position, Name, "Position")
|
||||
world:set(Position, Type, { size = 12, type = "Vector3" } ) -- 12 bytes to represent a Vector3
|
||||
|
||||
for id, ty, name in world:query(Type, Name, Networked) do
|
||||
local batch = {}
|
||||
for entity, data in world:query(id) do
|
||||
table.insert(batch, { entity = entity, data = data })
|
||||
end
|
||||
-- entities are sized f64
|
||||
local packet = buffer.create(#batch * (8 + ty.size))
|
||||
local offset = 0
|
||||
for _, entityData in batch do
|
||||
offset+=8
|
||||
buffer.writef64(packet, offset, entityData.entity)
|
||||
if ty.type == "Vector3" then
|
||||
local vec3 = entity.data :: Vector3
|
||||
offset += 4
|
||||
buffer.writei32(packet, offset, vec3.X)
|
||||
offset += 4
|
||||
buffer.writei32(packet, offset, vec3.Y)
|
||||
offset += 4
|
||||
buffer.writei32(packet, offset, vec3.Z)
|
||||
end
|
||||
end
|
||||
|
||||
updatePositions:FireServer(packet)
|
||||
end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Networked = world.component();
|
||||
const Type = world.component();
|
||||
const Name = world.component();
|
||||
const Position = world.component<Vector3>();
|
||||
world.add(Position, Networked);
|
||||
world.set(Position, Name, "Position");
|
||||
world.set(Position, Type, { size: 12, type: "Vector3" }); // 12 bytes to represent a Vector3
|
||||
|
||||
for (const [id, ty, name] of world.query(Type, Name, Networked)) {
|
||||
const batch = new Array<{ entity: Entity; data: unknown }>();
|
||||
|
||||
for (const [entity, data] of world.query(id)) {
|
||||
batch.push({ entity, data });
|
||||
}
|
||||
// entities are sized f64
|
||||
const packet = buffer.create(batch.size() * (8 + ty.size));
|
||||
const offset = 0;
|
||||
for (const [_, entityData] of batch) {
|
||||
offset += 8;
|
||||
buffer.writef64(packet, offset, entityData.entity);
|
||||
if (ty.type == "Vector3") {
|
||||
const vec3 = entity.data as Vector3;
|
||||
offset += 4;
|
||||
buffer.writei32(packet, offsetm, vec3.X);
|
||||
offset += 4;
|
||||
buffer.writei32(packet, offset, vec3.Y);
|
||||
offset += 4;
|
||||
buffer.writei32(packet, offset, vec3.Z);
|
||||
}
|
||||
}
|
||||
|
||||
updatePositions.FireServer(packet);
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Singletons
|
||||
|
||||
Singletons are components for which only a single instance
|
||||
exists on the world. They can be accessed on the
|
||||
world directly and do not require providing an entity.
|
||||
Singletons are useful for global game resources, such as
|
||||
game state, a handle to a physics engine or a network socket. An example:
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [luau]
|
||||
local TimeOfDay = world:component() :: jecs.Entity<number>
|
||||
world:set(TimeOfDay, TimeOfDay, 0.5)
|
||||
local t = world:get(TimeOfDay, TimeOfDay)
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const TimeOfDay = world.component<number>();
|
||||
world.set(TimeOfDay, TimeOfDay, 0.5);
|
||||
const t = world.get(TimeOfDay, TimeOfDay);
|
||||
```
|
||||
|
||||
:::
|
186
docs/learn/concepts/queries.md
Normal file
|
@ -0,0 +1,186 @@
|
|||
# Queries
|
||||
|
||||
## Introductiuon
|
||||
|
||||
Queries enable games to quickly find entities that satifies provided conditions.
|
||||
|
||||
Jecs queries can do anything from returning entities that match a simple list of components, to matching against entity graphs.
|
||||
|
||||
This manual contains a full overview of the query features available in Jecs. Some of the features of Jecs queries are:
|
||||
|
||||
- Queries have support for relationships pairs which allow for matching against entity graphs without having to build complex data structures for it.
|
||||
- Queries support filters such as [`query:with(...)`](../../api/query.md#with) if entities are required to have the components but you don’t actually care about components value. And [`query:without(...)`](../../api/query.md#without) which selects entities without the components.
|
||||
- Queries can be drained or reset on when called, which lets you choose iterator behaviour.
|
||||
- Queries can be called with any ID, including entities created dynamically, this is useful for pairs.
|
||||
- Queries are already fast but can be futher inlined via [`query:archetypes()`](../../api/query.md#archetypes) for maximum performance to eliminate function call overhead which is roughly 70-80% of the cost for iteration.
|
||||
|
||||
## Performance and Caching
|
||||
|
||||
Understanding the basic architecture of queries helps to make the right tradeoffs when using queries in games.
|
||||
The biggest impact on query performance is whether a query is cached or not.
|
||||
This section goes over what caching is, how it can be used and when it makes sense to use it.
|
||||
|
||||
### Caching: what is it?
|
||||
|
||||
Jecs is an archetype ECS, which means that entities with exactly the same components are
|
||||
grouped together in an "archetype". Archetypes are created on the fly
|
||||
whenever a new component combination is created in the ECS. For example:
|
||||
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
local e1 = world:entity()
|
||||
world:set(e1, Position, Vector3.new(10, 20, 30)) -- create archetype [Position]
|
||||
world:set(e1, Velocity, Vector3.new(1, 2, 3)) -- create archetype [Position, Velocity]
|
||||
|
||||
local e2 = world:entity()
|
||||
world:set(e2, Position, Vector3.new(10, 20, 30)) -- archetype [Position] already exists
|
||||
world:set(e2, Velocity, Vector3.new(1, 2, 3)) -- archetype [Position, Velocity] already exists
|
||||
world:set(e3, Mass, 100) -- create archetype [Position, Velocity, Mass]
|
||||
|
||||
-- e1 is now in archetype [Position, Velocity]
|
||||
-- e2 is now in archetype [Position, Velocity, Mass]
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const e1 = world.entity();
|
||||
world.set(e1, Position, new Vector3(10, 20, 30)); // create archetype [Position]
|
||||
world.set(e1, Velocity, new Vector3(1, 2, 3)); // create archetype [Position, Velocity]
|
||||
|
||||
const e2 = world.entity();
|
||||
world.set(e2, Position, new Vector3(10, 20, 30)); // archetype [Position] already exists
|
||||
world.set(e2, Velocity, new Vector3(1, 2, 3)); // archetype [Position, Velocity] already exists
|
||||
world.set(e3, Mass, 100); // create archetype [Position, Velocity, Mass]
|
||||
|
||||
// e1 is now in archetype [Position, Velocity]
|
||||
// e2 is now in archetype [Position, Velocity, Mass]
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Archetypes are important for queries. Since all entities in an archetype have the same components, and a query matches entities with specific components, a query can often match entire archetypes instead of individual entities. This is one of the main reasons why queries in an archetype ECS are fast.
|
||||
|
||||
The second reason that queries in an archetype ECS are fast is that they are cheap to cache. While an archetype is created for each unique component combination, games typically only use a finite set of component combinations which are created quickly after game assets are loaded.
|
||||
|
||||
This means that instead of searching for archetypes each time a query is evaluated, a query can instead cache the list of matching archetypes. This is a cheap cache to maintain: even though entities can move in and out of archetypes, the archetypes themselves are often stable.
|
||||
|
||||
If none of that made sense, the main thing to remember is that a cached query does not actually have to search for entities. Iterating a cached query just means iterating a list of prematched results, and this is really, really fast.
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
Jecs has both cached and uncached queries. If cached queries are so fast, why even bother with uncached queries? There are four main reasons:
|
||||
|
||||
- Cached queries are really fast to iterate, but take more time to create because the cache must be initialized first.
|
||||
- Cached queries have a higher RAM utilization, whereas uncached queries have very little overhead and are stateless.
|
||||
- Cached queries add overhead to archetype creation/deletion, as these changes have to get propagated to caches.
|
||||
- While caching archetypes is fast, some query features require matching individual entities, which are not efficient to cache (and aren't cached).
|
||||
|
||||
As a rule of thumb, if you have a query that is evaluated each frame (as is typically the case with systems), they will benefit from being cached. If you need to create a query ad-hoc, an uncached query makes more sense.
|
||||
|
||||
Ad-hoc queries are often necessary when a game needs to find entities that match a condition that is only known at runtime, for example to find all child entities for a specific parent.
|
||||
|
||||
## Creating Queries
|
||||
|
||||
This section explains how to create queries in the different language bindings.
|
||||
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
for _ in world:query(Position, Velocity) do end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
for (const [_] of world.query(Position, Velocity)) {
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Components
|
||||
|
||||
A component is any single ID that can be added to an entity. This includes tags and regular entities, which are IDs that do not have the builtin `Component` component. To match a query, an entity must have all the requested components. An example:
|
||||
|
||||
```luau
|
||||
local e1 = world:entity()
|
||||
world:add(e1, Position)
|
||||
|
||||
local e2 = world:entity()
|
||||
world:add(e2, Position)
|
||||
world:add(e2, Velocity)
|
||||
|
||||
local e3 = world:entity()
|
||||
world:add(e3, Position)
|
||||
world:add(e3, Velocity)
|
||||
world:add(e3, Mass)
|
||||
|
||||
```
|
||||
|
||||
Only entities `e2` and `e3` match the query Position, Velocity.
|
||||
|
||||
### Wildcards
|
||||
|
||||
Jecs currently only supports the `Any` type of wildcards which a single result for the first component that it matches.
|
||||
|
||||
When using the `Any` type wildcard it is undefined which component will be matched, as this can be influenced by other parts of the query. It is guaranteed that iterating the same query twice on the same dataset will produce the same result.
|
||||
|
||||
If you want to iterate multiple targets for the same relation on a pair, then use [`world:target`](../../api/world.md#target)
|
||||
|
||||
Wildcards are particularly useful when used in combination with pairs (next section).
|
||||
|
||||
### Pairs
|
||||
|
||||
A pair is an ID that encodes two elements. Pairs, like components, can be added to entities and are the foundation for [Relationships](relationships.md).
|
||||
|
||||
The elements of a pair are allowed to be wildcards. When a query pair returns an `Any` type wildcard, the query returns at most a single matching pair on an entity.
|
||||
|
||||
The following sections describe how to create queries for pairs in the different language bindings.
|
||||
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
local Likes = world:entity()
|
||||
local bob = world:entity()
|
||||
for _ in world:query(pair(Likes, bob)) do end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Likes = world.entity();
|
||||
const bob = world.entity();
|
||||
for (const [_] of world.query(pair(Likes, bob))) {
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
When a query pair contains a wildcard, the `world:target()` function can be used to determine the target of the pair element that matched the query:
|
||||
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
for id in world:query(pair(Likes, jecs.Wildcard)) do
|
||||
print(`entity {getName(id)} likes {getName(world, world:target(id, Likes))}`)
|
||||
end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Likes = world.entity();
|
||||
const bob = world.entity();
|
||||
for (const [_] of world.query(pair(Likes, jecs.Wildcard))) {
|
||||
print(`entity ${getName(id)} likes ${getName(world.target(id, Likes))}`);
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Filters
|
||||
|
||||
Filters are extensions to queries which allow you to select entities from a more complex pattern but you don't actually care about the component values.
|
||||
|
||||
The following filters are supported by queries:
|
||||
|
||||
| Identifier | Description |
|
||||
| ---------- | ----------------------------------- |
|
||||
| With | Must match with all terms. |
|
||||
| Without | Must not match with provided terms. |
|
||||
|
||||
This page takes wording and terminology directly from Flecs [documentation](https://www.flecs.dev/flecs/md_docs_2Queries.html)
|
198
docs/learn/concepts/relationships.md
Normal file
|
@ -0,0 +1,198 @@
|
|||
# Relationships
|
||||
Relationships makes it possible to describe entity graphs natively in ECS.
|
||||
|
||||
Adding/removing relationships is similar to adding/removing regular components, with as difference that instead of a single component id, a relationship adds a pair of two things to an entity. In this pair, the first element represents the relationship (e.g. "Eats"), and the second element represents the relationship target (e.g. "Apples").
|
||||
|
||||
Relationships can be used to describe many things, from hierarchies to inventory systems to trade relationships between players in a game. The following sections go over how to use relationships, and what features they support.
|
||||
|
||||
## Definitions
|
||||
|
||||
Name | Description
|
||||
----------|------------
|
||||
Id | An id that can be added and removed
|
||||
Component | Id with a single element (same as an entity id)
|
||||
Relationship | Used to refer to first element of a pair
|
||||
Target | Used to refer to second element of a pair
|
||||
Source | Entity to which an id is added
|
||||
|
||||
## Relationship queries
|
||||
There are a number of ways a game can query for relationships. The following kinds of queries are available for all (unidirectional) relationships, and are all constant time:
|
||||
|
||||
Test if entity has a relationship pair
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:has(bob, pair(Eats, Apples))
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.has(bob, pair(Eats, Apples))
|
||||
```
|
||||
:::
|
||||
|
||||
Test if entity has a relationship wildcard
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:has(bob, pair(Eats, jecs.Wildcard)
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.has(bob, pair(Eats, jecs.Wildcard)
|
||||
```
|
||||
:::
|
||||
|
||||
Get parent for entity
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:parent(bob)
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.parent(bob)
|
||||
```
|
||||
:::
|
||||
|
||||
Find first target of a relationship for entity
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:target(bob, Eats)
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.target(bob, Eats)
|
||||
```
|
||||
:::
|
||||
|
||||
Find all entities with a pair
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
for id in world:query(pair(Eats, Apples)) do
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
```typescript [typescript]
|
||||
for (const [id] of world.query(pair(Eats, Apples)) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
Find all entities with a pair wildcard
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
for id in world:query(pair(Eats, jecs.Wildcard)) do
|
||||
local food = world:target(id, Eats) -- Apples, ...
|
||||
end
|
||||
```
|
||||
```typescript [typescript]
|
||||
for (const [id] of world.query(pair(Eats, jecs.Wildcard)) {
|
||||
const food = world.target(id, Eats) // Apples, ...
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
Iterate all children for a parent
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
for child in world:query(pair(jecs.ChildOf, parent)) do
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
```typescript [typescript]
|
||||
for (const [child] of world.query(pair(jecs.ChildOf, parent)) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
Relationship components
|
||||
|
||||
Relationship pairs, just like regular component, can be associated with data.
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
local Position = world:component()
|
||||
local Eats = world:component()
|
||||
local Apples = world:entity()
|
||||
local Begin = world:entity()
|
||||
local End = world:entity()
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, pair(Eats, Apples), { amount = 1 })
|
||||
|
||||
world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0))
|
||||
world:set(e, pair(End, Position), Vector3.new(10, 20, 30))
|
||||
|
||||
world:add(e, jecs.ChildOf, Position)
|
||||
|
||||
```
|
||||
```typescript [typescript]
|
||||
const Position = world.component()
|
||||
const Eats = world.component()
|
||||
const Apples = world.entity()
|
||||
const Begin = world.entity()
|
||||
const End = world.entity()
|
||||
|
||||
const e = world.entity()
|
||||
world.set(e, pair(Eats, Apples), { amount: 1 })
|
||||
|
||||
world.set(e, pair(Begin, Position), new Vector3(0, 0, 0))
|
||||
world.set(e, pair(End, Position), new Vector3(10, 20, 30))
|
||||
|
||||
world.add(e, jecs.ChildOf, Position)
|
||||
```
|
||||
:::
|
||||
|
||||
## Relationship wildcards
|
||||
|
||||
When querying for relationship pairs, it is often useful to be able to find all instances for a given relationship or target. To accomplish this, an game can use wildcard expressions.
|
||||
|
||||
Wildcards may used for the relationship or target part of a pair
|
||||
|
||||
```luau
|
||||
pair(Likes, jecs.Wildcard) -- Matches all Likes relationships
|
||||
pair(jecs.Wildcard, Alice) -- Matches all relationships with Alice as target
|
||||
```
|
||||
|
||||
## Relationship performance
|
||||
This section goes over the performance implications of using relationships.
|
||||
|
||||
### Introduction
|
||||
The ECS storage needs to know two things in order to store components for entities:
|
||||
- Which IDs are associated with an entity
|
||||
- Which types are associated with those ids
|
||||
Ids represent anything that can be added to an entity. An ID that is not associated with a type is called a tag. An ID associated with a type is a component. For regular components, the ID is a regular entity that has the builtin `Component` component.
|
||||
|
||||
### Storing relationships
|
||||
Relationships do not fundamentally change or extend the capabilities of the storage. Relationship pairs are two elements encoded into a single 53-bit ID, which means that on the storage level they are treated the same way as regular component IDs. What changes is the function that determines which type is associated with an id. For regular components this is simply a check on whether an entity has `Component`. To support relationships, new rules are added to determine the type of an id.
|
||||
|
||||
Because of this, adding/removing relationships to entities has the same performance as adding/removing regular components. This becomes more obvious when looking more closely at a function that adds a relationship pair.
|
||||
|
||||
### Id ranges
|
||||
Jecs reserves entity ids under a threshold (HI_COMPONENT_ID, default is 256) for components. This low id range is used by the storage to more efficiently encode graph edges between archetypes. Graph edges for components with low ids use direct array indexing, whereas graph edges for high ids use a hashmap. Graph edges are used to find the next archetype when adding/removing component ids, and are a contributing factor to the performance overhead of add/remove operations.
|
||||
|
||||
Because of the way pair IDs are encoded, a pair will never be in the low id range. This means that adding/removing a pair ID always uses a hashmap to find the next archetype. This introduces a small overhead.
|
||||
|
||||
### Fragmentation
|
||||
Fragmentation is a property of archetype-based ECS implementations where entities are spread out over more archetypes as the number of different component combinations increases. The overhead of fragmentation is visible in two areas:
|
||||
- Archetype creation
|
||||
- Queries (queries have to match & iterate more archetypes)
|
||||
Games that make extensive use of relationships might observe high levels of fragmentation, as relationships can introduce many different combinations of components. While the Jecs storage is optimized for supporting large amounts (hundreds of thousands) of archetypes, fragmentation is a factor to consider when using relationships.
|
||||
|
||||
Union relationships are planned along with other improvements to decrease the overhead of fragmentation introduced by relationships.
|
||||
|
||||
### Archetype Creation
|
||||
|
||||
When an ID added to an entity is deleted, all references to that ID are deleted from the storage. For example, when the component Position is deleted it is removed from all entities, and all archetypes with the Position component are deleted. While not unique to relationships, it is more common for relationships to trigger cleanup actions, as relationship pairs contain regular entities.
|
||||
|
||||
The opposite is also true. Because relationship pairs can contain regular entities which can be created on the fly, archetype creation is more common than in games that do not use relationships. While Jecs is optimized for fast archetypes creation, creating and cleaning up archetypes is inherently more expensive than creating/deleting an entity. Therefore archetypes creation is a factor to consider, especially for games that make extensive use of relationships.
|
||||
|
||||
### Indexing
|
||||
|
||||
To improve the speed of evaluating queries, Jecs has indices that store all archetypes for a given component ID. Whenever a new archetype is created, it is registered with the indices for the IDs the archetype has, including IDs for relationship pairs.
|
||||
|
||||
While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships.
|
||||
|
||||
This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html).
|
|
@ -1,17 +0,0 @@
|
|||
# Code Coverage Reports
|
||||
|
||||
All of the code coverage reports can be found here:
|
||||
|
||||
[Overview](/jecs/coverage/index.html){target="_self"}
|
||||
|
||||
[jecs.luau](/jecs/coverage/jecs.luau.html){target="_self"}
|
||||
|
||||
[ANSI](/jecs/coverage/ansi.luau.html){target="_self"}
|
||||
|
||||
[Entity Visualiser](/jecs/coverage/entity_visualiser.luau.html){target="_self"}
|
||||
|
||||
[Lifetime Tracker](/jecs/coverage/lifetime_tracker.luau.html){target="_self"}
|
||||
|
||||
[Testkit](/jecs/coverage/testkit.luau.html){target="_self"}
|
||||
|
||||
[Tests](/jecs/coverage/tests.luau.html){target="_self"}
|
|
@ -1,21 +0,0 @@
|
|||
# Contribution Guidelines
|
||||
|
||||
Whether you found an issue, or want to make a change to jecs, we'd love to hear back from the community on what features you want or bugs you've run into.
|
||||
|
||||
There's a few different ways you can go about this.
|
||||
|
||||
## Creating an Issue
|
||||
|
||||
This is what you should be filing if you have a bug you want to report.
|
||||
|
||||
[Click here](https://github.com/Ukendio/jecs/issues/new/choose) to file a bug report. We have a few templates ready for the most common issue types.
|
||||
|
||||
Additionally, see the [Submitting Issues](../contributing/issues) page for more information.
|
||||
|
||||
## Creating a Pull Request
|
||||
|
||||
This is what you should be filing if you have a change you want to merge into the main project.
|
||||
|
||||
[Click here](https://github.com/Ukendio/jecs/compare) to select the branch you want to merge from.
|
||||
|
||||
Additionally, see the [Submitting Pull Requests](../contributing/pull-requests) page for more information.
|
|
@ -1,24 +0,0 @@
|
|||
# Submitting Issues
|
||||
|
||||
When you're submitting an issue, generally they fall into a few categories:
|
||||
|
||||
## Bug
|
||||
|
||||
We need some information to figure out what's going wrong. At a minimum, you need to tell us:
|
||||
|
||||
(1) What's supposed to happen
|
||||
|
||||
(2) What actually happened
|
||||
|
||||
(3) Steps to reproduce
|
||||
|
||||
|
||||
Stack traces and other useful information that you find make a bug report more likely to be fixed.
|
||||
|
||||
Consult the template for a bug report if you don't know or have questions about how to format this.
|
||||
|
||||
## Documentation
|
||||
|
||||
Depending on how you go about it, this can be done as a [Pull Request](../contributing/pull-requests) instead of an issue. Generally, we need to know what was wrong, what you changed, and how it improved the documentation if it isn't obvious.
|
||||
|
||||
We just need to know what's wrong. You should fill out a [PR](../contributing/pull-requests) if you know what should be there instead.
|
|
@ -1,77 +0,0 @@
|
|||
# Submitting Pull Requests
|
||||
|
||||
When submitting a Pull Request, there's a few reasons to do so:
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
If there's something to change with the documentation, you should follow a similar format to this example:
|
||||
|
||||
An example of an appropriate typo-fixing PR would be:
|
||||
|
||||
>**Brief Description of your Changes**
|
||||
>
|
||||
>I fixed a couple of typos found in the /contributing/issues.md file.
|
||||
>
|
||||
>**Impact of your Changes**
|
||||
>
|
||||
>- Documentation is more clear and readable for the users.
|
||||
>
|
||||
>**Tests Performed**
|
||||
>
|
||||
>Ran `vitepress dev docs` and verified it was built successfully.
|
||||
>
|
||||
>**Additional Comments**
|
||||
>
|
||||
>[At Discretion]
|
||||
|
||||
## Change in Behavior
|
||||
|
||||
An example of an appropriate PR that adds a new feature would be:
|
||||
|
||||
>
|
||||
>**Brief Description of your Changes**
|
||||
>
|
||||
>I added `jecs.best_function`, which gives everyone who uses the module an immediate boost in concurrent player counts. (this is a joke)
|
||||
>
|
||||
>**Impact of your Changes**
|
||||
>
|
||||
>- jecs functionality is extended to better fit the needs of the community [explain why].
|
||||
>
|
||||
>**Tests Performed**
|
||||
>
|
||||
>Added a few test cases to ensure the function runs as expected [link to changes].
|
||||
>
|
||||
>**Additional Comments**
|
||||
>
|
||||
>[At Discretion]
|
||||
|
||||
## Addons
|
||||
|
||||
If you made something you think should be included into the [resources page](../../resources), let us know!
|
||||
|
||||
We have tons of examples of libraries and other tools which can be used in conjunction with jecs on this page.
|
||||
|
||||
One example of a PR that would be accepted is:
|
||||
|
||||
>**Brief Description of your Changes**
|
||||
>
|
||||
>I added `jecs observers` to the addons page.
|
||||
>
|
||||
>**Impact of your Changes**
|
||||
>
|
||||
>- jecs observers are a different and important way of handling queries which benefit the users of jecs by [explain why your tool benefits users here]
|
||||
>
|
||||
>- [talk about why you went with this design instead of maybe an alternative]
|
||||
>
|
||||
>**Tests Performed**
|
||||
>
|
||||
> I used this tool in conjunction with jecs and ensured it works as expected.
|
||||
>
|
||||
> [If you wrote unit tests for your tool, mention it here.]
|
||||
>
|
||||
>**Additional Comments**
|
||||
>
|
||||
>[At Discretion]
|
||||
|
||||
Keep in mind the list on the addons page is *not* exhaustive. If you came up with a tool that doesn't fit into any of the categories listed, we still want to hear from you!
|
3
docs/learn/faq/contributing.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## TODO
|
||||
|
||||
This is a TODO stub.
|
|
@ -1,697 +0,0 @@
|
|||
# Introduction
|
||||
Jecs is a standalone entity-component-system module written in Luau.
|
||||
ECS ("entity-component-system") describes one way to write games in a more data oriented design.
|
||||
|
||||
## Installation
|
||||
|
||||
Jecs supports the following installation methods using package managers:
|
||||
:::code-group
|
||||
```bash [wally]
|
||||
jecs = "ukendio/jecs@0.6.0" # Inside wally.toml
|
||||
```
|
||||
```bash [pesde]
|
||||
pesde add wally#ukendio/jecs@0.6.0
|
||||
```
|
||||
```bash [npm]
|
||||
npm i @rbxts/jecs
|
||||
```
|
||||
:::
|
||||
|
||||
Additionally an `rbxm` is published with [each release under the assets submenu](https://github.com/Ukendio/jecs/releases/latest).
|
||||
|
||||
## Hello World, Entity and Component
|
||||
It all has to start somewhere. A world stores entities and their components, and manages them. This tour will reference it for every operation.
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
local jecs = require(path/to/jecs)
|
||||
local world = jecs.world()
|
||||
```
|
||||
```typescript [typescript]
|
||||
import { World } from "@rbxts/jecs"
|
||||
const world = new World()
|
||||
// creates a new entity with no components and returns its identifier
|
||||
const entity = world.entity()
|
||||
|
||||
// deletes an entity and all its components
|
||||
world.delete(entity)
|
||||
```
|
||||
:::
|
||||
|
||||
## Entities
|
||||
|
||||
Entities represent things in a game. In a game there may be entities of characters, buildings, projectiles, particle effects etc.
|
||||
|
||||
By itself, an entity is just an unique entity identifier without any data. An entity identifier contains information about the entity itself and its generation.
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
-- creates a new entity with no components and returns its identifier
|
||||
local entity = world:entity()
|
||||
|
||||
-- deletes an entity and all its components
|
||||
world:delete(entity)
|
||||
```
|
||||
```typescript [typescript]
|
||||
// creates a new entity with no components and returns its identifier
|
||||
const entity = world.entity()
|
||||
|
||||
// deletes an entity and all its components
|
||||
world.delete(entity)
|
||||
```
|
||||
:::
|
||||
|
||||
The `entity` member function also accepts an overload that allows you to create an entity with a desired id which bypasses the [`entity range`](#Entity-Ranges).
|
||||
|
||||
## Components
|
||||
|
||||
A component is something that is added to an entity. Components can simply tag an entity ("this entity is an `Npc`"), attach data to an entity ("this entity is at `Position` `Vector3.new(10, 20, 30)`") and create relationships between entities ("bob `Likes` alice") that may also contain data ("bob `Eats` `10` apples").
|
||||
|
||||
## Operations
|
||||
|
||||
| Operation | Description |
|
||||
| --------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `get` | Get a specific component or set of components from an entity. |
|
||||
| `add` | Adds component to an entity. If entity already has the component, `add` does nothing. |
|
||||
| `set` | Sets the value of a component for an entity. `set` behaves as a combination of `add` and `get` |
|
||||
| `remove` | Removes component from entity. If entity doesn't have the component, `remove` does nothing. |
|
||||
| `clear` | Remove all components from an entity. Clearing is more efficient than removing one by one. |
|
||||
|
||||
## Components are entities
|
||||
|
||||
In an ECS, components need to be uniquely identified. In Jecs this is done by making each component its own unique entity. This means that everything is customizable. Components are no exception
|
||||
and all of the APIs that apply to regular entities also apply to component entities.
|
||||
|
||||
If a game has a component Position and Velocity, there will be two entities, one for each component. Component entities can be distinguished from "regular" entities as they have a `Component` component trait.
|
||||
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local Position = world:component() :: jecs.Entity<Vector3>
|
||||
world:set(Position, jecs.Name, "Position") -- Using regular apis to set metadata on component entities!
|
||||
|
||||
print(`{world:get(Position, jecs.Name)} is a Component: {world:has(Position, jecs.Component)}`);
|
||||
|
||||
-- Output:
|
||||
-- Position is a Component: true
|
||||
```
|
||||
```typescript [typescript]
|
||||
const Position = world.component<Vector3>();
|
||||
world.set(Position, jecs.Name, "Position") // Using regular apis to set metadata on component entities!
|
||||
|
||||
print(`${world.get(Position, jecs.Name)} is a Component: ${world.has(Position, jecs.Component)}`);
|
||||
// Output:
|
||||
// Position is a Component: true
|
||||
```
|
||||
:::
|
||||
|
||||
### Entity ranges
|
||||
Jecs reserves entity ids under a threshold (HI_COMPONENT_ID, default is 256) for components. That means that regular entities will start after this number. This number can be further specified via the `range` member function.
|
||||
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
world:range(1000, 5000) -- Defines the lower and upper bounds of the entity range respectively
|
||||
|
||||
local e = world:entity()
|
||||
print(e)
|
||||
-- Output:
|
||||
-- 1000
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.range(1000, 5000) // Defines the lower and upper bounds of the entity range respectively
|
||||
|
||||
const e = world.entity()
|
||||
print(e)
|
||||
// Output:
|
||||
// 1000
|
||||
```
|
||||
:::
|
||||
|
||||
### Hooks
|
||||
|
||||
Component data generally need to adhere to a specific interface, and sometimes requires side effects to run upon certain lifetime cycles. In `jecs`, there are hooks which are `component traits`, that can define the behaviour of a component and enforce invariants, but can only be invoked through mutations on the component data. You can only configure a single `OnAdd`, `OnRemove` and `OnChange` hook per component, just like you can only have a single constructor and destructor.
|
||||
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local Transform = world:component()
|
||||
world:set(Transform, OnAdd, function(entity, id, data)
|
||||
-- A transform component `id` has been added with `data` to `entity`
|
||||
end)
|
||||
world:set(Transform, OnRemove, function(entity, id)
|
||||
-- A transform component `id` has been removed from `entity`
|
||||
end)
|
||||
world:set(Transform, OnChange, function(entity, id, data)
|
||||
-- A transform component `id` has been changed to `data` on `entity`
|
||||
end)
|
||||
```
|
||||
```typescript [typescript]
|
||||
const Transform = world.component();
|
||||
world.set(Transform, OnAdd, (entity, id, data) => {
|
||||
// A transform component `id` has been added with `data` to `entity`
|
||||
});
|
||||
world.set(Transform, OnRemove, (entity, id) => {
|
||||
// A transform component `id` has been removed from `entity`
|
||||
});
|
||||
world.set(Transform, OnChange, (entity, id, data) => {
|
||||
// A transform component `id` has been changed to `data` on `entity`
|
||||
});
|
||||
```
|
||||
:::
|
||||
|
||||
### Cleanup Traits
|
||||
|
||||
When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references.
|
||||
|
||||
We also want to specify this per relationship. If an entity has `(Likes, parent)` we may not want to delete that entity, meaning the cleanup we want to perform for `Likes` and `ChildOf` may not be the same.
|
||||
|
||||
This is what cleanup traits are for: to specify which action needs to be executed under which condition. They are applied to entities that have a reference to the entity being deleted: if I delete the `Archer` tag I remove the tag from all entities that have it.
|
||||
|
||||
To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed.
|
||||
|
||||
There are two cleanup actions:
|
||||
|
||||
- `Remove`: removes instances of the specified (component) id from all entities (default)
|
||||
- `Delete`: deletes all entities with specified id
|
||||
|
||||
There are two cleanup conditions:
|
||||
|
||||
- `OnDelete`: the component, tag or relationship is deleted
|
||||
- `OnDeleteTarget`: a target used with the relationship is deleted
|
||||
|
||||
#### (OnDelete, Remove)
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local Archer = world:component()
|
||||
world:add(Archer, pair(jecs.OnDelete, jecs.Remove))
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, Archer)
|
||||
|
||||
-- This will remove Archer from e
|
||||
world:delete(Archer)
|
||||
```
|
||||
```typescript [typescript]
|
||||
const Archer = world.component();
|
||||
world.add(Archer, pair(jecs.OnDelete, jecs.Remove));
|
||||
|
||||
const e = world.entity();
|
||||
world.add(e, Archer);
|
||||
|
||||
// This will remove Archer from e
|
||||
world.delete(Archer);
|
||||
```
|
||||
:::
|
||||
|
||||
#### (OnDelete, Delete)
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local Archer = world:component()
|
||||
world:add(Archer, pair(jecs.OnDelete, jecs.Delete))
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, Archer)
|
||||
|
||||
-- This will delete entity e because the Archer component has a (OnDelete, Delete) cleanup trait
|
||||
world:delete(Archer)
|
||||
```
|
||||
```typescript [typescript]
|
||||
const Archer = world.component();
|
||||
world.add(Archer, pair(jecs.OnDelete, jecs.Delete));
|
||||
|
||||
const e = world.entity();
|
||||
world.add(e, Archer);
|
||||
|
||||
// This will delete entity e because the Archer component has a (OnDelete, Delete) cleanup trait
|
||||
world.delete(Archer);
|
||||
```
|
||||
:::
|
||||
|
||||
#### (OnDeleteTarget, Remove)
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local OwnedBy = world:component()
|
||||
world:add(OwnedBy, pair(jecs.OnDeleteTarget, jecs.Remove))
|
||||
local loot = world:entity()
|
||||
local player = world:entity()
|
||||
world:add(loot, pair(OwnedBy, player))
|
||||
|
||||
-- This will remove (OwnedBy, player) from loot
|
||||
world:delete(player)
|
||||
```
|
||||
```typescript [typescript]
|
||||
const OwnedBy = world.component();
|
||||
world.add(OwnedBy, pair(jecs.OnDeleteTarget, jecs.Remove));
|
||||
const loot = world.entity();
|
||||
const player = world.entity();
|
||||
world.add(loot, pair(OwnedBy, player));
|
||||
|
||||
// This will remove (OwnedBy, player) from loot
|
||||
world.delete(player);
|
||||
```
|
||||
:::
|
||||
#### (OnDeleteTarget, Delete)
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local ChildOf = world:component()
|
||||
world:add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||
|
||||
local parent = world:entity()
|
||||
local child = world:entity()
|
||||
world:add(child, pair(ChildOf, parent))
|
||||
|
||||
-- This will delete both parent and child
|
||||
world:delete(parent)
|
||||
```
|
||||
```typescript [typescript]
|
||||
const ChildOf = world.component();
|
||||
world.add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete));
|
||||
|
||||
const parent = world.entity();
|
||||
const child = world.entity();
|
||||
world.add(child, pair(ChildOf, parent));
|
||||
|
||||
// This will delete both parent and child
|
||||
world.delete(parent);
|
||||
```
|
||||
:::
|
||||
|
||||
## Preregistration
|
||||
|
||||
By default, components being registered on runtime is useful for how dynamic it can be. But, sometimes being able to register components without having the world instance is useful.
|
||||
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local Position = jecs.component() :: jecs.Entity<Vector3>
|
||||
|
||||
jecs.world() -- Position gets registered here
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Position = jecs.component<Vector3>();
|
||||
|
||||
new World() // Position gets registered here
|
||||
```
|
||||
:::
|
||||
|
||||
However, if you try to set metadata, you will find that this doesn't work without the world instance. Instead, jecs offers a `meta` member function that can forward declare its metadata.
|
||||
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
jecs.meta(Position, jecs.Name, "Position")
|
||||
|
||||
jecs.world() -- Position gets registered here with its name "Position"
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
jecs.meta(Position, jecs.Name, "Position")
|
||||
|
||||
new World() // Position gets registered here with its name "Position"
|
||||
```
|
||||
:::
|
||||
|
||||
### Singletons
|
||||
|
||||
Singletons are components for which only a single instance
|
||||
exists on the world. They can be accessed on the
|
||||
world directly and do not require providing an entity.
|
||||
Singletons are useful for global game resources, such as
|
||||
game state, a handle to a physics engine or a network socket. An example:
|
||||
|
||||
::: code-group
|
||||
```luau [luau]
|
||||
local TimeOfDay = world:component() :: jecs.Entity<number>
|
||||
world:set(TimeOfDay, TimeOfDay, 0.5)
|
||||
local t = world:get(TimeOfDay, TimeOfDay)
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const TimeOfDay = world.component<number>();
|
||||
world.set(TimeOfDay, TimeOfDay, 0.5);
|
||||
const t = world.get(TimeOfDay, TimeOfDay);
|
||||
```
|
||||
:::
|
||||
|
||||
# Queries
|
||||
|
||||
Queries enable games to quickly find entities that satifies provided conditions.
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
for _ in world:query(Position, Velocity) do end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
for (const [_] of world.query(Position, Velocity)) {
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
In `jecs`, queries can do anything from returning entities that match a simple list of components, to matching against entity graphs.
|
||||
|
||||
This manual contains a full overview of the query features available in Jecs. Some of the features of Jecs queries are:
|
||||
|
||||
- Queries have support for relationships pairs which allow for matching against entity graphs without having to build complex data structures for it.
|
||||
- Queries support filters such as [`query:with(...)`](../api/query.md#with) if entities are required to have the components but you don’t actually care about components value. And [`query:without(...)`](../api/query.md#without) which selects entities without the components.
|
||||
- Queries can be drained or reset on when called, which lets you choose iterator behaviour.
|
||||
- Queries can be called with any ID, including entities created dynamically, this is useful for pairs.
|
||||
- Queries are already fast but can be futher inlined via [`query:archetypes()`](../api/query.md#archetypes) for maximum performance to eliminate function call overhead which is roughly 60-80% of the cost for iteration.
|
||||
|
||||
## Performance and Caching
|
||||
|
||||
Understanding the basic architecture of queries helps to make the right tradeoffs when using queries in games.
|
||||
The biggest impact on query performance is whether a query is cached or not.
|
||||
This section goes over what caching is, how it can be used and when it makes sense to use it.
|
||||
|
||||
### Caching: what is it?
|
||||
|
||||
Jecs is an archetype ECS, which means that entities with exactly the same components are
|
||||
grouped together in an "archetype". Archetypes are created on the fly
|
||||
whenever a new component combination is created in the ECS. For example:
|
||||
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
local e1 = world:entity()
|
||||
world:set(e1, Position, Vector3.new(10, 20, 30)) -- create archetype [Position]
|
||||
world:set(e1, Velocity, Vector3.new(1, 2, 3)) -- create archetype [Position, Velocity]
|
||||
|
||||
local e2 = world:entity()
|
||||
world:set(e2, Position, Vector3.new(10, 20, 30)) -- archetype [Position] already exists
|
||||
world:set(e2, Velocity, Vector3.new(1, 2, 3)) -- archetype [Position, Velocity] already exists
|
||||
world:set(e3, Mass, 100) -- create archetype [Position, Velocity, Mass]
|
||||
|
||||
-- e1 is now in archetype [Position, Velocity]
|
||||
-- e2 is now in archetype [Position, Velocity, Mass]
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const e1 = world.entity();
|
||||
world.set(e1, Position, new Vector3(10, 20, 30)); // create archetype [Position]
|
||||
world.set(e1, Velocity, new Vector3(1, 2, 3)); // create archetype [Position, Velocity]
|
||||
|
||||
const e2 = world.entity();
|
||||
world.set(e2, Position, new Vector3(10, 20, 30)); // archetype [Position] already exists
|
||||
world.set(e2, Velocity, new Vector3(1, 2, 3)); // archetype [Position, Velocity] already exists
|
||||
world.set(e3, Mass, 100); // create archetype [Position, Velocity, Mass]
|
||||
|
||||
// e1 is now in archetype [Position, Velocity]
|
||||
// e2 is now in archetype [Position, Velocity, Mass]
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Archetypes are important for queries. Since all entities in an archetype have the same components, and a query matches entities with specific components, a query can often match entire archetypes instead of individual entities. This is one of the main reasons why queries in an archetype ECS are fast.
|
||||
|
||||
The second reason that queries in an archetype ECS are fast is that they are cheap to cache. While an archetype is created for each unique component combination, games typically only use a finite set of component combinations which are created quickly after game assets are loaded.
|
||||
|
||||
This means that instead of searching for archetypes each time a query is evaluated, a query can instead cache the list of matching archetypes. This is a cheap cache to maintain: even though entities can move in and out of archetypes, the archetypes themselves are often stable.
|
||||
|
||||
If none of that made sense, the main thing to remember is that a cached query does not actually have to search for entities. Iterating a cached query just means iterating a list of prematched results, and this is really, really fast.
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
Jecs has both cached and uncached queries. If cached queries are so fast, why even bother with uncached queries? There are four main reasons:
|
||||
|
||||
- Cached queries are really fast to iterate, but take more time to create because the cache must be initialized first.
|
||||
- Cached queries add overhead to archetype creation/deletion, as these changes have to get propagated to caches.
|
||||
- While caching archetypes is fast, some query features require matching individual entities, which are not efficient to cache (and aren't cached).
|
||||
|
||||
As a rule of thumb, if you have a query that is evaluated each frame (as is typically the case with systems), they will benefit from being cached. If you need to create a query ad-hoc, an uncached query makes more sense.
|
||||
|
||||
Ad-hoc queries are often necessary when a game needs to find entities that match a condition that is only known at runtime, for example to find all child entities for a specific parent.
|
||||
|
||||
### Components
|
||||
|
||||
A component is any single ID that can be added to an entity. This includes tags and regular entities, which are IDs that do not have the builtin `Component` component. To match a query, an entity must have all the requested components. An example:
|
||||
|
||||
```luau
|
||||
local e1 = world:entity()
|
||||
world:add(e1, Position)
|
||||
|
||||
local e2 = world:entity()
|
||||
world:add(e2, Position)
|
||||
world:add(e2, Velocity)
|
||||
|
||||
local e3 = world:entity()
|
||||
world:add(e3, Position)
|
||||
world:add(e3, Velocity)
|
||||
world:add(e3, Mass)
|
||||
|
||||
```
|
||||
|
||||
Only entities `e2` and `e3` match the query Position, Velocity.
|
||||
|
||||
### Wildcards
|
||||
|
||||
Jecs currently only supports the `Any` type of wildcards which a single result for the first component that it matches.
|
||||
|
||||
When using the `Any` type wildcard it is undefined which component will be matched, as this can be influenced by other parts of the query. It is guaranteed that iterating the same query twice on the same dataset will produce the same result.
|
||||
|
||||
If you want to iterate multiple targets for the same relation on a pair, then use [`world:target`](../api/world.md#target)
|
||||
|
||||
Wildcards are particularly useful when used in combination with pairs (next section).
|
||||
|
||||
### Pairs
|
||||
|
||||
A pair is an ID that encodes two elements. Pairs, like components, can be added to entities and are the foundation for [`Relationships`](#relationships).
|
||||
|
||||
The elements of a pair are allowed to be wildcards. When a query pair returns an `Any` type wildcard, the query returns at most a single matching pair on an entity.
|
||||
|
||||
The following sections describe how to create queries for pairs in the different language bindings.
|
||||
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
local Likes = world:entity()
|
||||
local bob = world:entity()
|
||||
for _ in world:query(pair(Likes, bob)) do end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Likes = world.entity();
|
||||
const bob = world.entity();
|
||||
for (const [_] of world.query(pair(Likes, bob))) {
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
When a query pair contains a wildcard, the `world:target()` function can be used to determine the target of the pair element that matched the query:
|
||||
|
||||
:::code-group
|
||||
|
||||
```luau [luau]
|
||||
for id in world:query(pair(Likes, jecs.Wildcard)) do
|
||||
print(`entity {getName(id)} likes {getName(world, world:target(id, Likes))}`)
|
||||
end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
const Likes = world.entity();
|
||||
const bob = world.entity();
|
||||
for (const [_] of world.query(pair(Likes, jecs.Wildcard))) {
|
||||
print(`entity ${getName(id)} likes ${getName(world.target(id, Likes))}`);
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Filters
|
||||
|
||||
Filters are extensions to queries which allow you to select entities from a more complex pattern but you don't actually care about the component values.
|
||||
|
||||
The following filters are supported by queries:
|
||||
|
||||
| Identifier | Description |
|
||||
| ---------- | ----------------------------------- |
|
||||
| With | Must match with all terms. |
|
||||
| Without | Must not match with provided terms. |
|
||||
|
||||
## Relationships
|
||||
Relationships makes it possible to describe entity graphs natively in ECS.
|
||||
|
||||
Adding/removing relationships is similar to adding/removing regular components, with as difference that instead of a single component id, a relationship adds a pair of two things to an entity. In this pair, the first element represents the relationship (e.g. "Eats"), and the second element represents the relationship target (e.g. "Apples").
|
||||
|
||||
Relationships can be used to describe many things, from hierarchies to inventory systems to trade relationships between players in a game. The following sections go over how to use relationships, and what features they support.
|
||||
|
||||
### Definitions
|
||||
|
||||
Name | Description
|
||||
----------|------------
|
||||
Id | An id that can be added and removed
|
||||
Component | Id with a single element (same as an entity id)
|
||||
Relationship | Used to refer to first element of a pair
|
||||
Target | Used to refer to second element of a pair
|
||||
Source | Entity to which an id is added
|
||||
|
||||
### Relationship queries
|
||||
There are a number of ways a game can query for relationships. The following kinds of queries are available for all (unidirectional) relationships, and are all constant time:
|
||||
|
||||
Test if entity has a relationship pair
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:has(bob, pair(Eats, Apples))
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.has(bob, pair(Eats, Apples))
|
||||
```
|
||||
:::
|
||||
|
||||
Test if entity has a relationship wildcard
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:has(bob, pair(Eats, jecs.Wildcard))
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.has(bob, pair(Eats, jecs.Wildcard))
|
||||
```
|
||||
:::
|
||||
|
||||
Get parent for entity
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:parent(bob)
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.parent(bob)
|
||||
```
|
||||
:::
|
||||
|
||||
Find first target of a relationship for entity
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
world:target(bob, Eats)
|
||||
```
|
||||
```typescript [typescript]
|
||||
world.target(bob, Eats)
|
||||
```
|
||||
:::
|
||||
|
||||
Find all entities with a pair
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
for id in world:query(pair(Eats, Apples)) do
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
```typescript [typescript]
|
||||
for (const [id] of world.query(pair(Eats, Apples))) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
Find all entities with a pair wildcard
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
for id in world:query(pair(Eats, jecs.Wildcard)) do
|
||||
local food = world:target(id, Eats) -- Apples, ...
|
||||
end
|
||||
```
|
||||
```typescript [typescript]
|
||||
for (const [id] of world.query(pair(Eats, jecs.Wildcard))) {
|
||||
const food = world.target(id, Eats) // Apples, ...
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
Iterate all children for a parent
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
for child in world:query(pair(jecs.ChildOf, parent)) do
|
||||
-- ...
|
||||
end
|
||||
```
|
||||
```typescript [typescript]
|
||||
for (const [child] of world.query(pair(jecs.ChildOf, parent))) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### Relationship components
|
||||
|
||||
Relationship pairs, just like regular component, can be associated with data.
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
local Position = world:component()
|
||||
local Eats = world:component()
|
||||
local Apples = world:entity()
|
||||
local Begin = world:entity()
|
||||
local End = world:entity()
|
||||
|
||||
local e = world:entity()
|
||||
world:set(e, pair(Eats, Apples), { amount = 1 })
|
||||
|
||||
world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0))
|
||||
world:set(e, pair(End, Position), Vector3.new(10, 20, 30))
|
||||
|
||||
world:add(e, jecs.ChildOf, Position)
|
||||
|
||||
```
|
||||
```typescript [typescript]
|
||||
const Position = world.component()
|
||||
const Eats = world.component()
|
||||
const Apples = world.entity()
|
||||
const Begin = world.entity()
|
||||
const End = world.entity()
|
||||
|
||||
const e = world.entity()
|
||||
world.set(e, pair(Eats, Apples), { amount: 1 })
|
||||
|
||||
world.set(e, pair(Begin, Position), new Vector3(0, 0, 0))
|
||||
world.set(e, pair(End, Position), new Vector3(10, 20, 30))
|
||||
|
||||
world.add(e, jecs.ChildOf, Position)
|
||||
```
|
||||
:::
|
||||
|
||||
### Relationship wildcards
|
||||
|
||||
When querying for relationship pairs, it is often useful to be able to find all instances for a given relationship or target. To accomplish this, an game can use wildcard expressions.
|
||||
|
||||
Wildcards may used for the relationship or target part of a pair
|
||||
|
||||
```luau
|
||||
pair(Likes, jecs.Wildcard) -- Matches all Likes relationships
|
||||
pair(jecs.Wildcard, Alice) -- Matches all relationships with Alice as target
|
||||
```
|
||||
|
||||
### Relationship performance
|
||||
The ECS storage needs to know two things in order to store components for entities:
|
||||
- Which IDs are associated with an entity
|
||||
- Which types are associated with those ids
|
||||
Ids represent anything that can be added to an entity. An ID that is not associated with a type is called a tag. An ID associated with a type is a component. For regular components, the ID is a regular entity that has the builtin `Component` component.
|
||||
|
||||
### Storing relationships
|
||||
Relationships do not fundamentally change or extend the capabilities of the storage. Relationship pairs are two elements encoded into a single 53-bit ID, which means that on the storage level they are treated the same way as regular component IDs. What changes is the function that determines which type is associated with an id. For regular components this is simply a check on whether an entity has `Component`. To support relationships, new rules are added to determine the type of an id.
|
||||
|
||||
Because of this, adding/removing relationships to entities has the same performance as adding/removing regular components. This becomes more obvious when looking more closely at a function that adds a relationship pair.
|
||||
|
||||
### Fragmentation
|
||||
Fragmentation is a property of archetype-based ECS implementations where entities are spread out over more archetypes as the number of different component combinations increases. The overhead of fragmentation is visible in two areas:
|
||||
- Archetype creation
|
||||
- Queries (queries have to match & iterate more archetypes)
|
||||
Games that make extensive use of relationships might observe high levels of fragmentation, as relationships can introduce many different combinations of components. While the Jecs storage is optimized for supporting large amounts (hundreds of thousands) of archetypes, fragmentation is a factor to consider when using relationships.
|
||||
|
||||
Union relationships are planned along with other improvements to decrease the overhead of fragmentation introduced by relationships.
|
||||
|
||||
### Archetype Creation
|
||||
|
||||
When an ID added to an entity is deleted, all references to that ID are deleted from the storage. For example, when the component Position is deleted it is removed from all entities, and all archetypes with the Position component are deleted. While not unique to relationships, it is more common for relationships to trigger cleanup actions, as relationship pairs contain regular entities.
|
||||
|
||||
The opposite is also true. Because relationship pairs can contain regular entities which can be created on the fly, archetype creation is more common than in games that do not use relationships. While Jecs is optimized for fast archetypes creation, creating and cleaning up archetypes is inherently more expensive than creating/deleting an entity. Therefore archetypes creation is a factor to consider, especially for games that make extensive use of relationships.
|
||||
|
||||
### Indexing
|
||||
|
||||
To improve the speed of evaluating queries, Jecs has indices that store all archetypes for a given component ID. Whenever a new archetype is created, it is registered with the indices for the IDs the archetype has, including IDs for relationship pairs.
|
||||
|
||||
While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships.
|
||||
|
||||
This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html).
|
71
docs/learn/overview/first-jecs-project.md
Normal file
|
@ -0,0 +1,71 @@
|
|||
# First Jecs project
|
||||
|
||||
Now that you have installed Jecs, you can create your [World](https://ukendio.github.io/jecs/api/world.html).
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
local jecs = require(path/to/jecs)
|
||||
local world = jecs.World.new()
|
||||
```
|
||||
```typescript [typescript]
|
||||
import { World } from "@rbxts/jecs"
|
||||
const world = new World()
|
||||
```
|
||||
:::
|
||||
|
||||
Let's create a couple components.
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
local jecs = require(path/to/jecs)
|
||||
local world = jecs.World.new()
|
||||
|
||||
local Position = world:component()
|
||||
local Velocity = world:component()
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
import { World } from "@rbxts/jecs"
|
||||
const world = new World()
|
||||
|
||||
const Position = world.component()
|
||||
const Velocity = world.component()
|
||||
```
|
||||
:::
|
||||
|
||||
Systems can be as simple as a query in a function or a more contextualized construct. Let's make a system that moves an entity and decelerates over time.
|
||||
|
||||
:::code-group
|
||||
```luau [luau]
|
||||
local jecs = require(path/to/jecs)
|
||||
local world = jecs.World.new()
|
||||
|
||||
local Position = world:component()
|
||||
local Velocity = world:component()
|
||||
|
||||
for id, position, velocity in world:query(Position, Velocity) do
|
||||
world:set(id, Position, position + velocity)
|
||||
world:set(id, Velocity, velocity * 0.9)
|
||||
end
|
||||
```
|
||||
|
||||
```typescript [typescript]
|
||||
import { World } from "@rbxts/jecs"
|
||||
const world = new World()
|
||||
|
||||
const Position = world.component()
|
||||
const Velocity = world.component()
|
||||
|
||||
for (const [id, position, velocity] of world.query(Position, Velocity)) {
|
||||
world.set(id, Position, position.add(velocity))
|
||||
world.set(id, Velocity, velocity.mul(0.9))
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## Where To Get Help
|
||||
|
||||
If you are encountering problems, there are resources for you to get help:
|
||||
- [Roblox OSS Discord server](https://discord.gg/h2NV8PqhAD) has a [#jecs](https://discord.com/channels/385151591524597761/1248734074940559511) thread under the [#projects](https://discord.com/channels/385151591524597761/1019724676265676930) channel
|
||||
- [Open an issue](https://github.com/ukendio/jecs/issues) if you run into bugs or have feature requests
|
||||
- Dive into the nitty gritty in the [thesis paper](https://raw.githubusercontent.com/Ukendio/jecs/main/thesis/drafts/1/paper.pdf)
|
130
docs/learn/overview/get-started.md
Normal file
|
@ -0,0 +1,130 @@
|
|||
# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
### Installing Standalone
|
||||
|
||||
Navigate to the [releases page](https://github.com/Ukendio/jecs/releases) and download `jecs.rbxm` from the assets.
|
||||
|
||||

|
||||
|
||||
### Using Wally
|
||||
|
||||
Add the following to your wally configuration:
|
||||
|
||||
::: code-group
|
||||
|
||||
```toml [wally.toml]
|
||||
jecs = "ukendio/jecs@0.5.3"
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Using npm (roblox-ts)
|
||||
|
||||
Use one of the following commands on your root project directory:
|
||||
|
||||
::: code-group
|
||||
```bash [npm]
|
||||
npm i https://github.com/Ukendio/jecs.git
|
||||
```
|
||||
```bash [yarn]
|
||||
yarn add https://github.com/Ukendio/jecs.git
|
||||
```
|
||||
```bash [pnpm]
|
||||
pnpm add https://github.com/Ukendio/jecs.git
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Example Usage
|
||||
|
||||
::: code-group
|
||||
|
||||
```luau [Luau]
|
||||
local world = jecs.World.new()
|
||||
local pair = jecs.pair
|
||||
local Wildcard = jecs.Wildcard
|
||||
|
||||
local Name = world:component()
|
||||
|
||||
local function getName(e)
|
||||
return world:get(e, Name)
|
||||
end
|
||||
|
||||
local Eats = world:component()
|
||||
|
||||
-- Relationship objects
|
||||
local Apples = world:component()
|
||||
-- components are entities, so you can add components to components
|
||||
world:set(Apples, Name, "apples")
|
||||
local Oranges = world:component()
|
||||
world:set(Oranges, Name, "oranges")
|
||||
|
||||
local bob = world:entity()
|
||||
-- Pairs can be constructed from two entities
|
||||
|
||||
world:set(bob, pair(Eats, Apples), 10)
|
||||
world:set(bob, pair(Eats, Oranges), 5)
|
||||
world:set(bob, Name, "bob")
|
||||
|
||||
local alice = world:entity()
|
||||
world:set(alice, pair(Eats, Apples), 4)
|
||||
world:set(alice, Name, "alice")
|
||||
|
||||
for id, amount in world:query(pair(Eats, Wildcard)) do
|
||||
-- get the second target of the pair
|
||||
local food = world:target(id, Eats)
|
||||
print(string.format("%s eats %d %s", getName(id), amount, getName(food)))
|
||||
end
|
||||
|
||||
-- Output:
|
||||
-- bob eats 10 apples
|
||||
-- bob eats 5 pears
|
||||
-- alice eats 4 apples
|
||||
```
|
||||
|
||||
|
||||
```ts [Typescript]
|
||||
import { Wildcard, pair, World } from "@rbxts/jecs"
|
||||
|
||||
|
||||
const world = new World()
|
||||
const Name = world.component()
|
||||
function getName(e) {
|
||||
return world.get(e, Name)
|
||||
}
|
||||
|
||||
const Eats = world.component()
|
||||
|
||||
// Relationship objects
|
||||
const Apples = world.component()
|
||||
// components are entities, so you can add components to components
|
||||
world.set(Apples, Name, "apples")
|
||||
const Oranges = world.component()
|
||||
world.set(Oranges, Name, "oranges")
|
||||
|
||||
const bob = world.entity()
|
||||
// Pairs can be constructed from two entities
|
||||
|
||||
world.set(bob, pair(Eats, Apples), 10)
|
||||
world.set(bob, pair(Eats, Oranges), 5)
|
||||
world.set(bob, Name, "bob")
|
||||
|
||||
const alice = world.entity()
|
||||
world.set(alice, pair(Eats, Apples), 4)
|
||||
world.set(alice, Name, "alice")
|
||||
|
||||
for (const [id, amount] of world.query(pair(Eats, Wildcard))) {
|
||||
// get the second target of the pair
|
||||
const food = world:target(id, Eats)
|
||||
print(string.format("%s eats %d %s", getName(id), amount, getName(food)))
|
||||
}
|
||||
|
||||
// Output:
|
||||
// bob eats 10 apples
|
||||
// bob eats 5 pears
|
||||
// alice eats 4 apples
|
||||
|
||||
```
|
||||
|
BIN
docs/learn/overview/rbxm.png
Normal file
After Width: | Height: | Size: 34 KiB |
0
docs/learn/public/jecs_logo.svg
Executable file → Normal file
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
@ -1,69 +0,0 @@
|
|||
<html><head>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
|
||||
<style>
|
||||
body { font-family: monospace; text-align: center; }
|
||||
#funcTable table { margin: 0 auto; width: auto; max-width: 300px; font-size: 14px; border-collapse: collapse; }
|
||||
#funcTable th, #funcTable td { padding: 2px 6px; text-align: left; white-space: nowrap; }
|
||||
#funcTable th { background-color: #ddd; }
|
||||
#funcTable td:nth-child(2) { text-align: right; min-width: 50px; }
|
||||
.zero-hits { background-color: #fcc; font-weight: bold; color: red; }
|
||||
.nonzero-hits { color: green; font-weight: bold; }
|
||||
.low-hits { background-color: #ffe6b3; }
|
||||
.high-hits { background-color: #cfc; }
|
||||
.source-code-table { margin-left: 10px; }th, td { padding: 0px; font-size: 12px; }
|
||||
table.table { font-size: 14px; border-collapse: collapse; }
|
||||
table.table th, table.table td { padding: 1px; font-size: 12px; line-height: 1.2; }
|
||||
table.table tr { height: auto; }
|
||||
</style></head><body>
|
||||
<h1 class="text-center">ansi.luau Coverage</h1>
|
||||
<h2>Total Execution Hits: 1</h2>
|
||||
<h2>Function Coverage Overview: 11.11%</h2>
|
||||
<button class="btn btn-primary mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#funcTable">Toggle Function Coverage</button>
|
||||
<div class="collapse show" id="funcTable">
|
||||
<h2>Function Coverage:</h2><table class="table table-bordered"><thead><tr><th>Function</th><th>Hits</th></tr></thead><tbody>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;"><main></td><td style="padding: 1px; color: green; font-weight: bold;">1</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">white_underline:2</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">white:6</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">green:10</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">red:14</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">yellow:18</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">red_highlight:22</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">green_highlight:26</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
<tr><td style="padding: 1px; min-width: 18ch;">gray:30</td><td style="padding: 1px; color: red; font-weight: bold;">0</td></tr>
|
||||
</tbody></table></div>
|
||||
<h2>Source Code:</h2><table class="table table-bordered source-code-table "><thead><tr><th>Line</th><th>Hits</th><th>Code</th></tr></thead><tbody>
|
||||
<tr><td>1</td><td>1</td><td><span class=high-hits>return {</span></td></tr>
|
||||
<tr><td>2</td><td>1</td><td><span class=high-hits>white_underline = function(s: any)</span></td></tr>
|
||||
<tr><td>3</td><td>0</td><td><span class=zero-hits>return `\27[1;4m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>4</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>5</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
|
||||
<tr><td>6</td><td>1</td><td><span class=high-hits>white = function(s: any)</span></td></tr>
|
||||
<tr><td>7</td><td>0</td><td><span class=zero-hits>return `\27[37;1m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>8</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>9</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
|
||||
<tr><td>10</td><td>1</td><td><span class=high-hits>green = function(s: any)</span></td></tr>
|
||||
<tr><td>11</td><td>0</td><td><span class=zero-hits>return `\27[32;1m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>12</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>13</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
|
||||
<tr><td>14</td><td>1</td><td><span class=high-hits>red = function(s: any)</span></td></tr>
|
||||
<tr><td>15</td><td>0</td><td><span class=zero-hits>return `\27[31;1m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>16</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>17</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
|
||||
<tr><td>18</td><td>1</td><td><span class=high-hits>yellow = function(s: any)</span></td></tr>
|
||||
<tr><td>19</td><td>0</td><td><span class=zero-hits>return `\27[33;1m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>20</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>21</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
|
||||
<tr><td>22</td><td>1</td><td><span class=high-hits>red_highlight = function(s: any)</span></td></tr>
|
||||
<tr><td>23</td><td>0</td><td><span class=zero-hits>return `\27[41;1;30m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>24</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>25</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
|
||||
<tr><td>26</td><td>1</td><td><span class=high-hits>green_highlight = function(s: any)</span></td></tr>
|
||||
<tr><td>27</td><td>0</td><td><span class=zero-hits>return `\27[42;1;30m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>28</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>29</td><td><span class='text-muted'>N/A</span></td><td></td>></tr>
|
||||
<tr><td>30</td><td>1</td><td><span class=high-hits>gray = function(s: any)</span></td></tr>
|
||||
<tr><td>31</td><td>0</td><td><span class=zero-hits>return `\27[30;1m{s}\27[0m`</span></td></tr>
|
||||
<tr><td>32</td><td><span class='text-muted'>N/A</span></td><td>end,</td>></tr>
|
||||
<tr><td>33</td><td>0</td><td><span class=zero-hits>}</span></td></tr>
|
||||
</tbody></table></body></html>
|