mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Compare commits
11 commits
4e934cb51f
...
bf3c3fa375
Author | SHA1 | Date | |
---|---|---|---|
|
bf3c3fa375 | ||
|
b26fc39fce | ||
|
0e4f40ced7 | ||
|
de8e263828 | ||
|
d15266b6d5 | ||
|
18019679d5 | ||
|
925864dd2b | ||
|
ac79638599 | ||
|
f7573e8824 | ||
|
7b870e8f3c | ||
|
e668371cc1 |
20 changed files with 1689 additions and 71 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -65,3 +65,7 @@ drafts/
|
||||||
|
|
||||||
# Luau tools
|
# Luau tools
|
||||||
profile.*
|
profile.*
|
||||||
|
|
||||||
|
# Patch files
|
||||||
|
|
||||||
|
*.patch
|
||||||
|
|
5
.luaurc
5
.luaurc
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"jecs": "jecs",
|
"jecs": "jecs",
|
||||||
"testkit": "test/testkit",
|
"testkit": "tools/testkit",
|
||||||
"mirror": "mirror"
|
"mirror": "mirror",
|
||||||
|
"tools": "tools",
|
||||||
},
|
},
|
||||||
"languageMode": "strict"
|
"languageMode": "strict"
|
||||||
}
|
}
|
||||||
|
|
29
README.md
29
README.md
|
@ -62,3 +62,32 @@ Can be found under /benches/visual/query.luau
|
||||||
Inserting 8 components to an entity and updating them over 50 times.
|
Inserting 8 components to an entity and updating them over 50 times.
|
||||||

|

|
||||||
Can be found under /benches/visual/insertions.luau
|
Can be found under /benches/visual/insertions.luau
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Using Wally
|
||||||
|
|
||||||
|
Add jecs to your `wally.toml` file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
jecs = "ukendio/jecs@0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `wally install` in your project directory.
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
Download the latest release from the [releases page](https://github.com/ukendio/jecs/releases).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For complete documentation, visit our [documentation site](https://ukendio.github.io/jecs/).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome. Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
|
@ -5,41 +5,60 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||||
local jecs = require(ReplicatedStorage.Lib)
|
local jecs = require(ReplicatedStorage.Lib)
|
||||||
|
local pair = jecs.pair
|
||||||
local newWorld = Matter.World.new()
|
local newWorld = Matter.World.new()
|
||||||
local ecs = jecs.World.new()
|
local ecs = jecs.World.new()
|
||||||
|
local mirror = require(ReplicatedStorage.mirror)
|
||||||
|
local mcs = mirror.World.new()
|
||||||
|
|
||||||
local A, B = Matter.component(), Matter.component()
|
local C1 = ecs:component()
|
||||||
local C, D = ecs:component(), 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 registry2 = ecr.registry()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ParameterGenerator = function()
|
ParameterGenerator = function()
|
||||||
local matter_entities = {}
|
local j = ecs:entity()
|
||||||
local jecs_entities = {}
|
ecs:set(j, C1, true)
|
||||||
local entities = {
|
local m = mcs:entity()
|
||||||
matter = matter_entities,
|
mcs:set(m, E1, true)
|
||||||
jecs = jecs_entities,
|
|
||||||
}
|
|
||||||
for i = 1, 1000 do
|
for i = 1, 1000 do
|
||||||
table.insert(matter_entities, newWorld:spawn(A(), B()))
|
local friend1 = ecs:entity()
|
||||||
local e = ecs:entity()
|
local friend2 = mcs:entity()
|
||||||
ecs:set(e, C, {})
|
|
||||||
ecs:set(e, D, {})
|
ecs:add(friend1, pair(C2, j))
|
||||||
table.insert(jecs_entities, e)
|
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
|
end
|
||||||
return entities
|
return {
|
||||||
|
m = m,
|
||||||
|
j = j,
|
||||||
|
}
|
||||||
end,
|
end,
|
||||||
|
|
||||||
Functions = {
|
Functions = {
|
||||||
Matter = function(_, entities)
|
Mirror = function(_, a)
|
||||||
for _, entity in entities.matter do
|
mcs:delete(a.m)
|
||||||
newWorld:despawn(entity)
|
|
||||||
end
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
Jecs = function(_, entities)
|
Jecs = function(_, a)
|
||||||
for _, entity in entities.jecs do
|
ecs:delete(a.j)
|
||||||
ecs:delete(entity)
|
|
||||||
end
|
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
48
demo/src/ReplicatedStorage/track.luau
Normal file
48
demo/src/ReplicatedStorage/track.luau
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
local events = {}
|
||||||
|
|
||||||
|
local function trackers_invoke(event, component, entity, ...)
|
||||||
|
local trackers = events[event][component]
|
||||||
|
if not trackers then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, tracker in trackers do
|
||||||
|
tracker(entity, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function trackers_init(event, component, fn)
|
||||||
|
local ob = events[event]
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect = function(component, fn)
|
||||||
|
local trackers = ob[component]
|
||||||
|
if not trackers then
|
||||||
|
trackers = {}
|
||||||
|
ob[component] = trackers
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(trackers, fn)
|
||||||
|
end,
|
||||||
|
invoke = function(component, ...)
|
||||||
|
trackers_invoke(event, component, ...)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
return function(component, fn)
|
||||||
|
local trackers = ob[component]
|
||||||
|
if not trackers then
|
||||||
|
trackers = {}
|
||||||
|
ob[component] = trackers
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(trackers, fn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local trackers = {
|
||||||
|
emplace = trackers_init("emplace"),
|
||||||
|
add = trackers_init("added"),
|
||||||
|
remove = trackers_init("removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackers
|
|
@ -16,6 +16,12 @@ jecs.Wildcard: Entity
|
||||||
```
|
```
|
||||||
Builtin component type. This ID is used for wildcard queries.
|
Builtin component type. This ID is used for wildcard queries.
|
||||||
|
|
||||||
|
## w
|
||||||
|
```luau
|
||||||
|
jecs.w: Entity
|
||||||
|
```
|
||||||
|
An alias for `jecs.Wildcard`. This ID is used for wildcard queries, providing a shorter syntax in query operations.
|
||||||
|
|
||||||
## Component
|
## Component
|
||||||
```luau
|
```luau
|
||||||
jecs.Component: Entity
|
jecs.Component: Entity
|
||||||
|
@ -28,6 +34,54 @@ jecs.ChildOf: Entity
|
||||||
```
|
```
|
||||||
Builtin component type. This ID is for creating parent-child hierarchies.
|
Builtin component type. This ID is for creating parent-child hierarchies.
|
||||||
|
|
||||||
|
## Name
|
||||||
|
```luau
|
||||||
|
jecs.Name: Entity
|
||||||
|
```
|
||||||
|
Builtin component type. This ID is used to associate a string name with an entity, typically for debugging or display purposes.
|
||||||
|
|
||||||
|
## OnAdd
|
||||||
|
```luau
|
||||||
|
jecs.OnAdd: Entity<(entity: Entity) -> ()>
|
||||||
|
```
|
||||||
|
Builtin component hook. When set on a component, the provided function is called whenever that component is added to an entity.
|
||||||
|
|
||||||
|
## OnRemove
|
||||||
|
```luau
|
||||||
|
jecs.OnRemove: Entity<(entity: Entity) -> ()>
|
||||||
|
```
|
||||||
|
Builtin component hook. When set on a component, the provided function is called whenever that component is removed from an entity.
|
||||||
|
|
||||||
|
## OnSet
|
||||||
|
```luau
|
||||||
|
jecs.OnSet: Entity<(entity: Entity, data: any) -> ()>
|
||||||
|
```
|
||||||
|
Builtin component hook. When set on a component, the provided function is called whenever that component's value is set or changed on an entity.
|
||||||
|
|
||||||
|
## OnDelete
|
||||||
|
```luau
|
||||||
|
jecs.OnDelete: Entity
|
||||||
|
```
|
||||||
|
Builtin component trait. Used with `pair()` to define behavior when a component or entity is deleted. Must be paired with an action component like `jecs.Delete` or `jecs.Remove`.
|
||||||
|
|
||||||
|
## OnDeleteTarget
|
||||||
|
```luau
|
||||||
|
jecs.OnDeleteTarget: Entity
|
||||||
|
```
|
||||||
|
Builtin component trait. Used with `pair()` to define behavior when a relationship target is deleted. Must be paired with an action component like `jecs.Delete` or `jecs.Remove`.
|
||||||
|
|
||||||
|
## Delete
|
||||||
|
```luau
|
||||||
|
jecs.Delete: Entity
|
||||||
|
```
|
||||||
|
Builtin action component used with `OnDelete` or `OnDeleteTarget` to specify that entities should be deleted in the cleanup process.
|
||||||
|
|
||||||
|
## Remove
|
||||||
|
```luau
|
||||||
|
jecs.Remove: Entity
|
||||||
|
```
|
||||||
|
Builtin action component used with `OnDelete` or `OnDeleteTarget` to specify that components should be removed in the cleanup process.
|
||||||
|
|
||||||
## Rest
|
## Rest
|
||||||
```luau
|
```luau
|
||||||
jecs.Rest: Entity
|
jecs.Rest: Entity
|
||||||
|
|
130
docs/api/name.md
Normal file
130
docs/api/name.md
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
# Name Component
|
||||||
|
|
||||||
|
The `Name` component allows you to associate a string identifier with an entity.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
```luau
|
||||||
|
jecs.Name: Entity<string>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Name` component is a built-in component in jecs that stores a string value. It has no special behavior beyond storing a name.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Setting a Name
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
-- Create an entity
|
||||||
|
local entity = world:entity()
|
||||||
|
|
||||||
|
-- Assign a name to the entity
|
||||||
|
world:set(entity, jecs.Name, "Player")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting an Entity's Name
|
||||||
|
|
||||||
|
```luau
|
||||||
|
-- Retrieve an entity's name
|
||||||
|
local name = world:get(entity, jecs.Name)
|
||||||
|
print("Entity name:", name) -- Outputs: "Entity name: Player"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding Entities by Name
|
||||||
|
|
||||||
|
```luau
|
||||||
|
-- Iterate over all entities with the Name component
|
||||||
|
for entity, name in world:query(jecs.Name) do
|
||||||
|
if name == "Player" then
|
||||||
|
-- Found the entity named "Player"
|
||||||
|
-- Do something with entity...
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Names with Hierarchies
|
||||||
|
|
||||||
|
Names are particularly useful when working with entity hierarchies:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local pair = jecs.pair
|
||||||
|
|
||||||
|
-- Create a parent entity with a name
|
||||||
|
local parent = world:entity()
|
||||||
|
world:set(parent, jecs.Name, "Parent")
|
||||||
|
|
||||||
|
-- Create child entities with names
|
||||||
|
local child1 = world:entity()
|
||||||
|
world:set(child1, jecs.Name, "Child1")
|
||||||
|
world:add(child1, pair(jecs.ChildOf, parent))
|
||||||
|
|
||||||
|
local child2 = world:entity()
|
||||||
|
world:set(child2, jecs.Name, "Child2")
|
||||||
|
world:add(child2, pair(jecs.ChildOf, parent))
|
||||||
|
|
||||||
|
-- Print the hierarchy
|
||||||
|
print("Parent:", world:get(parent, jecs.Name))
|
||||||
|
for child in world:query(pair(jecs.ChildOf, parent)) do
|
||||||
|
print(" Child:", world:get(child, jecs.Name))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- Parent: Parent
|
||||||
|
-- Child: Child1
|
||||||
|
-- Child: Child2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Function
|
||||||
|
|
||||||
|
For convenience, you can create a simple helper function to get an entity's name:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local function getName(world, entity)
|
||||||
|
return world:get(entity, jecs.Name) or tostring(entity)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Usage
|
||||||
|
local name = getName(world, entity)
|
||||||
|
```
|
||||||
|
|
||||||
|
This function will return the entity's name if it has one, or fall back to the entity ID as a string.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use Consistently**: Apply names to key entities systematically, especially for important game objects.
|
||||||
|
|
||||||
|
2. **Be Descriptive**: Use meaningful names that describe the entity's role or purpose.
|
||||||
|
|
||||||
|
3. **Handle Missing Names**: Always check if an entity has a name before trying to use it, or use a helper function like the one above.
|
||||||
|
|
||||||
|
4. **Namespacing**: Consider using prefixes or namespaces for different systems (e.g., "UI:MainMenu", "Enemy:Boss").
|
||||||
|
|
||||||
|
5. **Performance**: Remember that adding a Name component to every entity adds memory overhead. Use judiciously in performance-critical applications.
|
||||||
|
|
||||||
|
## Example: Entity Debugging System
|
||||||
|
|
||||||
|
Here's a simple debugging system that uses the Name component:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local function debugEntity(world, entity)
|
||||||
|
local name = world:get(entity, jecs.Name) or "Unnamed"
|
||||||
|
print("Entity", entity, "(" .. name .. ")")
|
||||||
|
|
||||||
|
-- Print all components
|
||||||
|
for componentId in world:query(jecs.pair(jecs.Component, jecs.Wildcard)) do
|
||||||
|
if world:has(entity, componentId) then
|
||||||
|
local componentName = world:get(componentId, jecs.Name) or "Unknown"
|
||||||
|
local value = world:get(entity, componentId)
|
||||||
|
print(" Component:", componentName, "=", value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Usage
|
||||||
|
debugEntity(world, player)
|
||||||
|
```
|
||||||
|
|
||||||
|
This debugging function prints an entity's name along with all its components, making it easier to inspect entities during development.
|
215
docs/api/world-entity-management.md
Normal file
215
docs/api/world-entity-management.md
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
# Entity Management
|
||||||
|
|
||||||
|
This section covers methods for managing entities in the jecs World.
|
||||||
|
|
||||||
|
## delete
|
||||||
|
|
||||||
|
Deletes an entity (and its components/relationships) from the world entirely.
|
||||||
|
|
||||||
|
```luau
|
||||||
|
function World:delete(entity: Entity): void
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| entity | Entity | The entity to delete from the world |
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
|
||||||
|
1. Invokes any `OnRemove` hooks for all components on the entity
|
||||||
|
2. Triggers any cleanup actions based on `OnDelete` and `OnDeleteTarget` relationships
|
||||||
|
3. Removes the entity from all archetypes
|
||||||
|
4. Recycles the entity ID for future use
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```luau [luau]
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
-- Create an entity
|
||||||
|
local entity = world:entity()
|
||||||
|
world:set(entity, Position, {x = 0, y = 0})
|
||||||
|
|
||||||
|
-- Delete the entity
|
||||||
|
world:delete(entity)
|
||||||
|
|
||||||
|
-- The entity no longer exists in the world
|
||||||
|
assert(not world:contains(entity))
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript [typescript]
|
||||||
|
import { World } from "@rbxts/jecs";
|
||||||
|
|
||||||
|
const world = new World();
|
||||||
|
|
||||||
|
// Create an entity
|
||||||
|
const entity = world.entity();
|
||||||
|
world.set(entity, Position, {x: 0, y: 0});
|
||||||
|
|
||||||
|
// Delete the entity
|
||||||
|
world.delete(entity);
|
||||||
|
|
||||||
|
// The entity no longer exists in the world
|
||||||
|
assert(!world.contains(entity));
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Child Entity Deletion
|
||||||
|
|
||||||
|
When an entity has children (via the `ChildOf` relationship), deleting the parent can also delete the children if the appropriate cleanup policy is set:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
-- Set up parent-child relationship
|
||||||
|
local ChildOf = world:component()
|
||||||
|
world:add(ChildOf, jecs.pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||||
|
|
||||||
|
local parent = world:entity()
|
||||||
|
local child = world:entity()
|
||||||
|
|
||||||
|
-- Make child a child of parent
|
||||||
|
world:add(child, jecs.pair(ChildOf, parent))
|
||||||
|
|
||||||
|
-- Deleting parent will also delete child
|
||||||
|
world:delete(parent)
|
||||||
|
assert(not world:contains(child))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Deletion
|
||||||
|
|
||||||
|
The `delete` method can also be used to delete a component definition:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local Temporary = world:component()
|
||||||
|
world:add(Temporary, jecs.pair(jecs.OnDelete, jecs.Remove))
|
||||||
|
|
||||||
|
-- Add Temporary to entities
|
||||||
|
local e1 = world:entity()
|
||||||
|
local e2 = world:entity()
|
||||||
|
world:add(e1, Temporary)
|
||||||
|
world:add(e2, Temporary)
|
||||||
|
|
||||||
|
-- Delete the component definition
|
||||||
|
world:delete(Temporary)
|
||||||
|
|
||||||
|
-- Temporary is removed from all entities
|
||||||
|
assert(not world:has(e1, Temporary))
|
||||||
|
assert(not world:has(e2, Temporary))
|
||||||
|
```
|
||||||
|
|
||||||
|
## remove
|
||||||
|
|
||||||
|
Removes a component from a given entity.
|
||||||
|
|
||||||
|
```luau
|
||||||
|
function World:remove(entity: Entity, component: Id): void
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| entity | Entity | The entity to modify |
|
||||||
|
| component | Id | The component or relationship to remove |
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
|
||||||
|
1. Invokes any `OnRemove` hook for the component being removed
|
||||||
|
2. Removes the component from the entity
|
||||||
|
3. May cause the entity to transition to another archetype
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
::: code-group
|
||||||
|
|
||||||
|
```luau [luau]
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
-- Create components
|
||||||
|
local Health = world:component()
|
||||||
|
local Shield = world:component()
|
||||||
|
|
||||||
|
-- Create an entity with both components
|
||||||
|
local entity = world:entity()
|
||||||
|
world:set(entity, Health, 100)
|
||||||
|
world:set(entity, Shield, 50)
|
||||||
|
|
||||||
|
-- Remove just the Shield component
|
||||||
|
world:remove(entity, Shield)
|
||||||
|
|
||||||
|
-- Entity still exists but no longer has Shield
|
||||||
|
assert(world:contains(entity))
|
||||||
|
assert(world:has(entity, Health))
|
||||||
|
assert(not world:has(entity, Shield))
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript [typescript]
|
||||||
|
import { World } from "@rbxts/jecs";
|
||||||
|
|
||||||
|
const world = new World();
|
||||||
|
|
||||||
|
// Create components
|
||||||
|
const Health = world.component();
|
||||||
|
const Shield = world.component();
|
||||||
|
|
||||||
|
// Create an entity with both components
|
||||||
|
const entity = world.entity();
|
||||||
|
world.set(entity, Health, 100);
|
||||||
|
world.set(entity, Shield, 50);
|
||||||
|
|
||||||
|
// Remove just the Shield component
|
||||||
|
world.remove(entity, Shield);
|
||||||
|
|
||||||
|
// Entity still exists but no longer has Shield
|
||||||
|
assert(world.contains(entity));
|
||||||
|
assert(world.has(entity, Health));
|
||||||
|
assert(!world.has(entity, Shield));
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Removing Relationships
|
||||||
|
|
||||||
|
The `remove` method can also be used to remove relationship pairs:
|
||||||
|
|
||||||
|
```luau
|
||||||
|
local ChildOf = world:component()
|
||||||
|
local parent = world:entity()
|
||||||
|
local child = world:entity()
|
||||||
|
|
||||||
|
-- Establish parent-child relationship
|
||||||
|
world:add(child, jecs.pair(ChildOf, parent))
|
||||||
|
|
||||||
|
-- Remove the relationship
|
||||||
|
world:remove(child, jecs.pair(ChildOf, parent))
|
||||||
|
|
||||||
|
-- Child is no longer related to parent
|
||||||
|
assert(not world:has(child, jecs.pair(ChildOf, parent)))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Differences Between delete and remove
|
||||||
|
|
||||||
|
| Method | What It Affects | Entity Existence | Cleanup Policies |
|
||||||
|
|--------|-----------------|------------------|------------------|
|
||||||
|
| delete | Entity and all its components | Entity no longer exists | Triggers all cleanup policies |
|
||||||
|
| remove | Only the specified component | Entity still exists | Only triggers component-specific OnRemove hook |
|
||||||
|
|
||||||
|
### When to Use Each Method
|
||||||
|
|
||||||
|
- Use `delete` when you want to completely remove an entity from the world
|
||||||
|
- Use `remove` when you want to keep the entity but remove specific components
|
||||||
|
- Use `delete` on a component definition to remove that component from all entities
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
Both operations may cause archetype transitions, which have performance implications:
|
||||||
|
|
||||||
|
- `delete` typically has more overhead because it has to remove all components
|
||||||
|
- `remove` is generally faster for individual component removal
|
||||||
|
- Both methods are optimized in jecs to be as efficient as possible
|
||||||
|
|
||||||
|
For performance-critical code dealing with many entities, consider batching operations to minimize archetype transitions.
|
|
@ -1,3 +1,84 @@
|
||||||
## TODO
|
# Contributing to jecs
|
||||||
|
|
||||||
This is a TODO stub.
|
This document provides guidelines for contributing to the project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Clone your fork
|
||||||
|
3. Set up the development environment
|
||||||
|
4. Create a branch for your changes
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone your fork
|
||||||
|
git clone https://github.com/your-username/jecs.git
|
||||||
|
cd jecs
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
wally install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Making Changes
|
||||||
|
|
||||||
|
1. Create a branch:
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make your changes
|
||||||
|
|
||||||
|
3. Add tests if applicable
|
||||||
|
|
||||||
|
4. Run tests:
|
||||||
|
```bash
|
||||||
|
lune run test
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Commit your changes:
|
||||||
|
```bash
|
||||||
|
git commit -m "Add feature: description"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Submitting Changes
|
||||||
|
|
||||||
|
1. Push your changes:
|
||||||
|
```bash
|
||||||
|
git push origin feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a Pull Request
|
||||||
|
|
||||||
|
3. Explain the changes and reference related issues
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Follow the existing code style
|
||||||
|
- Use meaningful variable and function names
|
||||||
|
- Write clear comments for complex logic
|
||||||
|
- Keep functions focused on a single responsibility
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Add tests for new features
|
||||||
|
- Ensure all tests pass before submitting
|
||||||
|
- Consider edge cases
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Update documentation for changed functionality
|
||||||
|
- Document new features or APIs
|
||||||
|
- Use clear and concise language
|
||||||
|
|
||||||
|
## Issue Reporting
|
||||||
|
|
||||||
|
1. Check if the issue already exists in the [Issues](https://github.com/ukendio/jecs/issues) section
|
||||||
|
2. Create a new issue with a descriptive title and detailed information
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
- Be respectful in communications
|
||||||
|
- Provide constructive feedback
|
||||||
|
- Accept constructive criticism
|
||||||
|
- Focus on what is best for the community
|
94
docs/learn/faq/index.md
Normal file
94
docs/learn/faq/index.md
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
This section addresses common questions about jecs.
|
||||||
|
|
||||||
|
## General Questions
|
||||||
|
|
||||||
|
### What is jecs?
|
||||||
|
|
||||||
|
jecs is a high-performance Entity Component System (ECS) library for Luau/Roblox.
|
||||||
|
|
||||||
|
### How does jecs compare to other ECS libraries?
|
||||||
|
|
||||||
|
jecs uses an archetype-based storage system (SoA) which is more cache-friendly than traditional approaches. For a detailed comparison with Matter, see the [Migration from Matter](../migration-from-matter.md) guide.
|
||||||
|
|
||||||
|
### Can I use jecs outside of Roblox?
|
||||||
|
|
||||||
|
Yes, jecs can be used in any Lua/Luau environment as it has zero dependencies.
|
||||||
|
|
||||||
|
## Technical Questions
|
||||||
|
|
||||||
|
### How many entities can jecs handle?
|
||||||
|
|
||||||
|
jecs can handle up to 800,000 entities at 60 frames per second.
|
||||||
|
|
||||||
|
### What are archetypes?
|
||||||
|
|
||||||
|
Archetypes are groups of entities that have the exact same set of components. When you add or remove components from an entity, it moves to a different archetype.
|
||||||
|
|
||||||
|
### How do I optimize performance with jecs?
|
||||||
|
|
||||||
|
- Group entity operations to minimize archetype transitions
|
||||||
|
- Use cached queries for frequently accessed data
|
||||||
|
- Define components that are frequently queried together
|
||||||
|
- Use tags instead of empty tables
|
||||||
|
- Be mindful of archetype transitions
|
||||||
|
|
||||||
|
### What are relationship pairs?
|
||||||
|
|
||||||
|
Relationship pairs allow you to create connections between entities using the `pair()` function.
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### My entity disappeared after adding a component
|
||||||
|
|
||||||
|
Check if you have cleanup policies set up with `OnDelete` or `OnDeleteTarget`.
|
||||||
|
|
||||||
|
### Queries are returning unexpected results
|
||||||
|
|
||||||
|
Make sure you're querying for the exact components you need. Use modifiers like `.with()` or `.without()` to refine queries.
|
||||||
|
|
||||||
|
### Performance degrades over time
|
||||||
|
|
||||||
|
This could be due to:
|
||||||
|
- Memory leaks from not properly deleting entities
|
||||||
|
- Excessive archetype transitions
|
||||||
|
- Too many entities or components
|
||||||
|
- Inefficient queries
|
||||||
|
|
||||||
|
### How do I debug entity relationships?
|
||||||
|
|
||||||
|
Use the `Name` component to give entities meaningful names:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Print all parent-child relationships
|
||||||
|
for entity, target in world:query(jecs.pair(jecs.ChildOf, jecs.Wildcard)) do
|
||||||
|
print(world:get(entity, jecs.Name), "is a child of", world:get(target, jecs.Name))
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### When should I use component lifecycle hooks?
|
||||||
|
|
||||||
|
Use lifecycle hooks (`OnAdd`, `OnRemove`, `OnSet`) for:
|
||||||
|
- Initializing resources
|
||||||
|
- Cleaning up resources
|
||||||
|
- Reacting to data changes
|
||||||
|
|
||||||
|
### How should I structure my ECS code?
|
||||||
|
|
||||||
|
- Separate data (components) from behavior (systems)
|
||||||
|
- Keep components small and focused
|
||||||
|
- Design systems for specific component combinations
|
||||||
|
- Use relationships to model connections
|
||||||
|
- Document cleanup policies
|
||||||
|
|
||||||
|
### Should I use multiple worlds?
|
||||||
|
|
||||||
|
Multiple worlds are useful for:
|
||||||
|
- Separating client and server logic
|
||||||
|
- Creating isolated test environments
|
||||||
|
- Managing different game states
|
||||||
|
|
||||||
|
Component IDs may conflict between worlds if not registered in the same order.
|
218
docs/learn/migration-from-matter.md
Normal file
218
docs/learn/migration-from-matter.md
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
# Migrating from Matter to jecs
|
||||||
|
|
||||||
|
This guide is intended to help developers migrate from the Matter ECS library to jecs.
|
||||||
|
|
||||||
|
## Key Differences
|
||||||
|
|
||||||
|
### Architectural Differences
|
||||||
|
|
||||||
|
- **Storage Implementation**: jecs uses an archetype-based storage system (SoA - Structure of Arrays). Matter uses a simpler component-based storage approach.
|
||||||
|
- **Performance**: jecs is designed with a focus on performance, particularly for large-scale systems with hundreds of thousands of entities.
|
||||||
|
- **Relationship Model**: jecs treats entity relationships as first-class citizens. Matter doesn't have built-in relationship features.
|
||||||
|
- **Memory Usage**: jecs typically uses less memory for the same number of entities and components due to its optimized storage structure.
|
||||||
|
|
||||||
|
### API Differences
|
||||||
|
|
||||||
|
#### World Creation and Management
|
||||||
|
|
||||||
|
**Matter:**
|
||||||
|
```lua
|
||||||
|
local Matter = require("Matter")
|
||||||
|
local world = Matter.World.new()
|
||||||
|
```
|
||||||
|
|
||||||
|
**jecs:**
|
||||||
|
```lua
|
||||||
|
local jecs = require("jecs")
|
||||||
|
local world = jecs.World.new()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Component Definition
|
||||||
|
|
||||||
|
**Matter:**
|
||||||
|
```lua
|
||||||
|
local Position = Matter.component("Position")
|
||||||
|
local Velocity = Matter.component("Velocity")
|
||||||
|
```
|
||||||
|
|
||||||
|
**jecs:**
|
||||||
|
```lua
|
||||||
|
local Position = world:component()
|
||||||
|
local Velocity = world:component()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Entity Creation
|
||||||
|
|
||||||
|
**Matter:**
|
||||||
|
```lua
|
||||||
|
local entity = world:spawn(
|
||||||
|
Position({x = 0, y = 0}),
|
||||||
|
Velocity({x = 10, y = 5})
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**jecs:**
|
||||||
|
```lua
|
||||||
|
local entity = world:entity()
|
||||||
|
world:set(entity, Position, {x = 0, y = 0})
|
||||||
|
world:set(entity, Velocity, {x = 10, y = 5})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Queries
|
||||||
|
|
||||||
|
**Matter:**
|
||||||
|
```lua
|
||||||
|
for id, position, velocity in world:query(Position, Velocity) do
|
||||||
|
-- Update position based on velocity
|
||||||
|
position.x = position.x + velocity.x * dt
|
||||||
|
position.y = position.y + velocity.y * dt
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**jecs:**
|
||||||
|
```lua
|
||||||
|
for entity, position, velocity in world:query(Position, Velocity) do
|
||||||
|
-- Update position based on velocity
|
||||||
|
position.x = position.x + velocity.x * dt
|
||||||
|
position.y = position.y + velocity.y * dt
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### System Definition
|
||||||
|
|
||||||
|
**Matter:**
|
||||||
|
```lua
|
||||||
|
local function movementSystem(world)
|
||||||
|
for id, position, velocity in world:query(Position, Velocity) do
|
||||||
|
position.x = position.x + velocity.x * dt
|
||||||
|
position.y = position.y + velocity.y * dt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**jecs:**
|
||||||
|
```lua
|
||||||
|
local function movementSystem(world, dt)
|
||||||
|
for entity, position, velocity in world:query(Position, Velocity) do
|
||||||
|
position.x = position.x + velocity.x * dt
|
||||||
|
position.y = position.y + velocity.y * dt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Entity Removal
|
||||||
|
|
||||||
|
**Matter:**
|
||||||
|
```lua
|
||||||
|
world:despawn(entity)
|
||||||
|
```
|
||||||
|
|
||||||
|
**jecs:**
|
||||||
|
```lua
|
||||||
|
world:delete(entity)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Component Removal
|
||||||
|
|
||||||
|
**Matter:**
|
||||||
|
```lua
|
||||||
|
world:remove(entity, Position)
|
||||||
|
```
|
||||||
|
|
||||||
|
**jecs:**
|
||||||
|
```lua
|
||||||
|
world:remove(entity, Position)
|
||||||
|
```
|
||||||
|
|
||||||
|
### jecs-Specific Features
|
||||||
|
|
||||||
|
#### Relationships
|
||||||
|
|
||||||
|
jecs has built-in support for entity relationships:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local ChildOf = world:component()
|
||||||
|
local Name = world:component()
|
||||||
|
|
||||||
|
local parent = world:entity()
|
||||||
|
world:set(parent, Name, "Parent")
|
||||||
|
|
||||||
|
local child = world:entity()
|
||||||
|
world:add(child, jecs.pair(ChildOf, parent))
|
||||||
|
world:set(child, Name, "Child")
|
||||||
|
|
||||||
|
-- Query for all children of parent
|
||||||
|
for e in world:query(jecs.pair(ChildOf, parent)) do
|
||||||
|
local name = world:get(e, Name)
|
||||||
|
print(name, "is a child of Parent")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Wildcards
|
||||||
|
|
||||||
|
jecs supports wildcard queries for relationships:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Query for all parent-child relationships
|
||||||
|
for entity, target in world:query(jecs.pair(ChildOf, jecs.Wildcard)) do
|
||||||
|
print(world:get(entity, Name), "is a child of", world:get(target, Name))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Alternative using the shorter 'w' alias
|
||||||
|
for entity, target in world:query(jecs.pair(ChildOf, jecs.w)) do
|
||||||
|
print(world:get(entity, Name), "is a child of", world:get(target, Name))
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Observer Hooks
|
||||||
|
|
||||||
|
jecs provides component lifecycle hooks:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
world:set(Position, jecs.OnAdd, function(entity)
|
||||||
|
print("Position added to entity", entity)
|
||||||
|
end)
|
||||||
|
|
||||||
|
world:set(Position, jecs.OnRemove, function(entity)
|
||||||
|
print("Position removed from entity", entity)
|
||||||
|
end)
|
||||||
|
|
||||||
|
world:set(Position, jecs.OnSet, function(entity, value)
|
||||||
|
print("Position set on entity", entity, "with value", value)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Automatic Cleanup with OnDelete and OnDeleteTarget
|
||||||
|
|
||||||
|
jecs offers automated cleanup of related entities:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- When a parent is deleted, all its children will be deleted too
|
||||||
|
world:add(ChildOf, jecs.pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||||
|
|
||||||
|
-- When Health component is deleted, remove the entity's Shield component
|
||||||
|
world:add(Health, jecs.pair(jecs.OnDelete, jecs.Remove))
|
||||||
|
world:add(Shield, jecs.pair(jecs.OnDelete, jecs.Remove))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
When migrating from Matter to jecs, consider these performance tips:
|
||||||
|
|
||||||
|
1. **Batch Entity Operations**: Group entity operations when possible to minimize archetype transitions.
|
||||||
|
2. **Use Cached Queries**: For frequently used queries, create cached versions.
|
||||||
|
3. **Consider Component Layout**: Components frequently queried together should be defined together.
|
||||||
|
4. **Use Tags When Appropriate**: For components with no data, use tags instead of empty tables.
|
||||||
|
5. **Be Aware of Archetype Transitions**: Adding/removing components causes archetype transitions, which have performance implications for large-scale operations.
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
1. **Start with World Creation**: Replace Matter's world creation with jecs.
|
||||||
|
2. **Migrate Component Definitions**: Update component definitions to use jecs's approach.
|
||||||
|
3. **Update Entity Creation**: Modify entity spawning code to use jecs's entity and set methods.
|
||||||
|
4. **Adapt Queries**: Update queries, noting that iteration patterns are similar.
|
||||||
|
5. **Implement Relationships**: Take advantage of jecs's relationship features where applicable.
|
||||||
|
6. **Add Observer Hooks**: Implement lifecycle hooks to replace custom event handling in Matter.
|
||||||
|
7. **Optimize**: Refine your implementation using jecs-specific features for better performance.
|
||||||
|
|
||||||
|
By following this guide, you should be able to migrate your Matter ECS application to jecs while taking advantage of jecs's enhanced performance and features.
|
246
docs/learn/observers.md
Normal file
246
docs/learn/observers.md
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
# Observer APIs
|
||||||
|
|
||||||
|
jecs provides observer hooks that allow you to respond to component lifecycle events and implement cleanup policies.
|
||||||
|
|
||||||
|
## Component Lifecycle Hooks
|
||||||
|
|
||||||
|
Component lifecycle hooks let you execute code when components are added, removed, or modified.
|
||||||
|
|
||||||
|
### OnAdd
|
||||||
|
|
||||||
|
The `OnAdd` hook is called when a component is added to an entity.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Define a component
|
||||||
|
local Transform = world:component()
|
||||||
|
|
||||||
|
-- Set an OnAdd hook for the Transform component
|
||||||
|
world:set(Transform, jecs.OnAdd, function(entity)
|
||||||
|
print("Transform component added to entity", entity)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- The hook will be called when Transform is added to any entity
|
||||||
|
local entity = world:entity()
|
||||||
|
world:add(entity, Transform) -- OnAdd hook is triggered
|
||||||
|
```
|
||||||
|
|
||||||
|
TypeScript signature:
|
||||||
|
```typescript
|
||||||
|
type OnAddHook = (entity: Entity) => void;
|
||||||
|
```
|
||||||
|
|
||||||
|
### OnRemove
|
||||||
|
|
||||||
|
The `OnRemove` hook is called when a component is removed from an entity.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Set an OnRemove hook for the Transform component
|
||||||
|
world:set(Transform, jecs.OnRemove, function(entity)
|
||||||
|
print("Transform component removed from entity", entity)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- The hook will be called when Transform is removed from any entity
|
||||||
|
world:remove(entity, Transform) -- OnRemove hook is triggered
|
||||||
|
```
|
||||||
|
|
||||||
|
TypeScript signature:
|
||||||
|
```typescript
|
||||||
|
type OnRemoveHook = (entity: Entity) => void;
|
||||||
|
```
|
||||||
|
|
||||||
|
### OnSet
|
||||||
|
|
||||||
|
The `OnSet` hook is called when a component's value is set or changed on an entity.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Set an OnSet hook for the Transform component
|
||||||
|
world:set(Transform, jecs.OnSet, function(entity, value)
|
||||||
|
print("Transform component set on entity", entity, "with value", value)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- The hook will be called when Transform's value is set on any entity
|
||||||
|
world:set(entity, Transform, { position = {x = 10, y = 20} }) -- OnSet hook is triggered
|
||||||
|
```
|
||||||
|
|
||||||
|
TypeScript signature:
|
||||||
|
```typescript
|
||||||
|
type OnSetHook<T> = (entity: Entity, value: T) => void;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatic Cleanup Policies
|
||||||
|
|
||||||
|
jecs provides automatic cleanup policies through the `OnDelete` and `OnDeleteTarget` hooks paired with action components.
|
||||||
|
|
||||||
|
### OnDelete
|
||||||
|
|
||||||
|
The `OnDelete` trait specifies what happens when a component or entity is deleted. It must be paired with an action component like `Delete` or `Remove`.
|
||||||
|
|
||||||
|
#### (OnDelete, Delete)
|
||||||
|
|
||||||
|
When paired with `Delete`, this policy deletes entities that have the component when the component itself is deleted.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Define a component
|
||||||
|
local Health = world:component()
|
||||||
|
|
||||||
|
-- Add the (OnDelete, Delete) cleanup policy to Health
|
||||||
|
world:add(Health, jecs.pair(jecs.OnDelete, jecs.Delete))
|
||||||
|
|
||||||
|
-- Create entities
|
||||||
|
local entity1 = world:entity()
|
||||||
|
local entity2 = world:entity()
|
||||||
|
|
||||||
|
-- Add Health component to entities
|
||||||
|
world:add(entity1, Health)
|
||||||
|
world:add(entity2, Health)
|
||||||
|
|
||||||
|
-- When Health component is deleted, all entities with Health will be deleted
|
||||||
|
world:delete(Health) -- Deletes entity1 and entity2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### (OnDelete, Remove)
|
||||||
|
|
||||||
|
When paired with `Remove`, this policy removes a component from entities when the component itself is deleted.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Define components
|
||||||
|
local Poison = world:component()
|
||||||
|
local PoisonEffect = world:component()
|
||||||
|
|
||||||
|
-- Add the (OnDelete, Remove) cleanup policy to Poison
|
||||||
|
world:add(Poison, jecs.pair(jecs.OnDelete, jecs.Remove))
|
||||||
|
world:add(PoisonEffect, jecs.pair(jecs.OnDelete, jecs.Remove))
|
||||||
|
|
||||||
|
-- Create entity
|
||||||
|
local entity = world:entity()
|
||||||
|
world:add(entity, Poison)
|
||||||
|
world:add(entity, PoisonEffect)
|
||||||
|
|
||||||
|
-- When Poison component is deleted, PoisonEffect will be removed from all entities
|
||||||
|
world:delete(Poison) -- Removes PoisonEffect from entity
|
||||||
|
```
|
||||||
|
|
||||||
|
### OnDeleteTarget
|
||||||
|
|
||||||
|
The `OnDeleteTarget` trait specifies what happens when a relationship target is deleted. It must be paired with an action component like `Delete` or `Remove`.
|
||||||
|
|
||||||
|
#### (OnDeleteTarget, Delete)
|
||||||
|
|
||||||
|
When paired with `Delete`, this policy deletes entities that have a relationship with the deleted target.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Define a ChildOf component for parent-child relationships
|
||||||
|
local ChildOf = world:component()
|
||||||
|
|
||||||
|
-- Add the (OnDeleteTarget, Delete) cleanup policy
|
||||||
|
world:add(ChildOf, jecs.pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||||
|
|
||||||
|
-- Create parent and child entities
|
||||||
|
local parent = world:entity()
|
||||||
|
local child1 = world:entity()
|
||||||
|
local child2 = world:entity()
|
||||||
|
|
||||||
|
-- Establish parent-child relationships
|
||||||
|
world:add(child1, jecs.pair(ChildOf, parent))
|
||||||
|
world:add(child2, jecs.pair(ChildOf, parent))
|
||||||
|
|
||||||
|
-- When the parent is deleted, all its children will be deleted too
|
||||||
|
world:delete(parent) -- Deletes child1 and child2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### (OnDeleteTarget, Remove)
|
||||||
|
|
||||||
|
When paired with `Remove`, this policy removes a relationship component when its target is deleted.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Define a Likes component for relationships
|
||||||
|
local Likes = world:component()
|
||||||
|
|
||||||
|
-- Add the (OnDeleteTarget, Remove) cleanup policy
|
||||||
|
world:add(Likes, jecs.pair(jecs.OnDeleteTarget, jecs.Remove))
|
||||||
|
|
||||||
|
-- Create entities
|
||||||
|
local bob = world:entity()
|
||||||
|
local alice = world:entity()
|
||||||
|
local charlie = world:entity()
|
||||||
|
|
||||||
|
-- Establish relationships
|
||||||
|
world:add(bob, jecs.pair(Likes, alice))
|
||||||
|
world:add(charlie, jecs.pair(Likes, alice))
|
||||||
|
|
||||||
|
-- When alice is deleted, all Likes relationships targeting her will be removed
|
||||||
|
world:delete(alice) -- Removes Likes relationship from bob and charlie
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use hooks for reactive logic**: Component hooks are perfect for synchronizing game state with visual representations or external systems.
|
||||||
|
|
||||||
|
2. **Keep hook logic simple**: Hooks should be lightweight and focused on a single concern.
|
||||||
|
|
||||||
|
3. **Consider cleanup policies carefully**: The right cleanup policies can prevent memory leaks and simplify your codebase by automating entity management.
|
||||||
|
|
||||||
|
4. **Avoid infinite loops**: Be careful not to create circular dependencies with your hooks and cleanup policies.
|
||||||
|
|
||||||
|
5. **Document your policies**: When using cleanup policies, document them clearly so other developers understand the entity lifecycle in your application.
|
||||||
|
|
||||||
|
6. **Use OnAdd for initialization**: The OnAdd hook is ideal for initializing component-specific resources.
|
||||||
|
|
||||||
|
7. **Use OnRemove for cleanup**: The OnRemove hook ensures resources are properly released when components are removed.
|
||||||
|
|
||||||
|
8. **Use OnSet for synchronization**: The OnSet hook helps keep different aspects of your game in sync when component values change.
|
||||||
|
|
||||||
|
## Example: Complete Entity Lifecycle
|
||||||
|
|
||||||
|
This example shows how to use all observer hooks together to manage an entity's lifecycle:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local pair = jecs.pair
|
||||||
|
|
||||||
|
-- Define components
|
||||||
|
local Model = world:component()
|
||||||
|
local Transform = world:component()
|
||||||
|
local ChildOf = world:component()
|
||||||
|
|
||||||
|
-- Set up component hooks
|
||||||
|
world:set(Model, jecs.OnAdd, function(entity)
|
||||||
|
print("Model added to entity", entity)
|
||||||
|
-- Create visual representation
|
||||||
|
end)
|
||||||
|
|
||||||
|
world:set(Model, jecs.OnRemove, function(entity)
|
||||||
|
print("Model removed from entity", entity)
|
||||||
|
-- Destroy visual representation
|
||||||
|
end)
|
||||||
|
|
||||||
|
world:set(Model, jecs.OnSet, function(entity, model)
|
||||||
|
print("Model set on entity", entity, "with value", model)
|
||||||
|
-- Update visual representation
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Set up cleanup policies
|
||||||
|
world:add(ChildOf, pair(jecs.OnDeleteTarget, jecs.Delete))
|
||||||
|
world:add(Transform, pair(jecs.OnDelete, jecs.Remove))
|
||||||
|
|
||||||
|
-- Create entities
|
||||||
|
local parent = world:entity()
|
||||||
|
local child = world:entity()
|
||||||
|
|
||||||
|
-- Set up relationships and components
|
||||||
|
world:add(child, pair(ChildOf, parent))
|
||||||
|
world:set(child, Model, "cube")
|
||||||
|
world:set(child, Transform, {position = {x = 0, y = 0, z = 0}})
|
||||||
|
|
||||||
|
-- Updating a component triggers the OnSet hook
|
||||||
|
world:set(child, Model, "sphere")
|
||||||
|
|
||||||
|
-- Removing a component triggers the OnRemove hook
|
||||||
|
world:remove(child, Model)
|
||||||
|
|
||||||
|
-- Deleting the parent triggers the OnDeleteTarget cleanup policy
|
||||||
|
-- which automatically deletes the child
|
||||||
|
world:delete(parent)
|
||||||
|
```
|
||||||
|
|
||||||
|
By effectively using observer hooks and cleanup policies, you can create more maintainable and robust ECS-based applications with less manual resource management.
|
104
jecs.luau
104
jecs.luau
|
@ -46,7 +46,7 @@ export type Record = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdRecord = {
|
type IdRecord = {
|
||||||
columns: { number },
|
cache: { number },
|
||||||
counts: { number },
|
counts: { number },
|
||||||
flags: number,
|
flags: number,
|
||||||
size: number,
|
size: number,
|
||||||
|
@ -480,7 +480,7 @@ local function world_target(world: World, entity: i53, relation: i24, index: num
|
||||||
nth = nth + count + 1
|
nth = nth + count + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local tr = idr.columns[archetype_id]
|
local tr = idr.cache[archetype_id]
|
||||||
|
|
||||||
nth = archetype.types[nth + tr]
|
nth = archetype.types[nth + tr]
|
||||||
|
|
||||||
|
@ -537,7 +537,7 @@ local function id_record_ensure(world: World, id: number): IdRecord
|
||||||
|
|
||||||
idr = {
|
idr = {
|
||||||
size = 0,
|
size = 0,
|
||||||
columns = {},
|
cache = {},
|
||||||
counts = {},
|
counts = {},
|
||||||
flags = flags,
|
flags = flags,
|
||||||
hooks = {
|
hooks = {
|
||||||
|
@ -562,7 +562,7 @@ local function archetype_append_to_records(
|
||||||
local archetype_id = archetype.id
|
local archetype_id = archetype.id
|
||||||
local archetype_records = archetype.records
|
local archetype_records = archetype.records
|
||||||
local archetype_counts = archetype.counts
|
local archetype_counts = archetype.counts
|
||||||
local idr_columns = idr.columns
|
local idr_columns = idr.cache
|
||||||
local idr_counts = idr.counts
|
local idr_counts = idr.counts
|
||||||
local tr = idr_columns[archetype_id]
|
local tr = idr_columns[archetype_id]
|
||||||
if not tr then
|
if not tr then
|
||||||
|
@ -1063,7 +1063,7 @@ local function archetype_destroy(world: World, archetype: Archetype)
|
||||||
|
|
||||||
for id in records do
|
for id in records do
|
||||||
local idr = component_index[id]
|
local idr = component_index[id]
|
||||||
idr.columns[archetype_id] = nil :: any
|
idr.cache[archetype_id] = nil :: any
|
||||||
idr.counts[archetype_id] = nil
|
idr.counts[archetype_id] = nil
|
||||||
idr.size -= 1
|
idr.size -= 1
|
||||||
records[id] = nil :: any
|
records[id] = nil :: any
|
||||||
|
@ -1122,7 +1122,7 @@ do
|
||||||
if idr then
|
if idr then
|
||||||
local flags = idr.flags
|
local flags = idr.flags
|
||||||
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
|
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
|
||||||
for archetype_id in idr.columns do
|
for archetype_id in idr.cache do
|
||||||
local idr_archetype = archetypes[archetype_id]
|
local idr_archetype = archetypes[archetype_id]
|
||||||
|
|
||||||
local entities = idr_archetype.entities
|
local entities = idr_archetype.entities
|
||||||
|
@ -1134,7 +1134,7 @@ do
|
||||||
archetype_destroy(world, idr_archetype)
|
archetype_destroy(world, idr_archetype)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
for archetype_id in idr.columns do
|
for archetype_id in idr.cache do
|
||||||
local idr_archetype = archetypes[archetype_id]
|
local idr_archetype = archetypes[archetype_id]
|
||||||
local entities = idr_archetype.entities
|
local entities = idr_archetype.entities
|
||||||
local n = #entities
|
local n = #entities
|
||||||
|
@ -1147,55 +1147,66 @@ do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local sparse_array = entity_index.sparse_array
|
|
||||||
local dense_array = entity_index.dense_array
|
local dense_array = entity_index.dense_array
|
||||||
|
|
||||||
if idr_t then
|
if idr_t then
|
||||||
for archetype_id in idr_t.columns do
|
local children
|
||||||
local children = {}
|
local ids
|
||||||
|
local count = 0
|
||||||
|
local archetype_ids = idr_t.cache
|
||||||
|
for archetype_id in archetype_ids do
|
||||||
local idr_t_archetype = archetypes[archetype_id]
|
local idr_t_archetype = archetypes[archetype_id]
|
||||||
|
|
||||||
local idr_t_types = idr_t_archetype.types
|
local idr_t_types = idr_t_archetype.types
|
||||||
|
local entities = idr_t_archetype.entities
|
||||||
for _, child in idr_t_archetype.entities do
|
local removal_queued = false
|
||||||
table.insert(children, child)
|
|
||||||
end
|
|
||||||
|
|
||||||
local n = #children
|
|
||||||
|
|
||||||
for _, id in idr_t_types do
|
for _, id in idr_t_types do
|
||||||
if not ECS_IS_PAIR(id) then
|
if not ECS_IS_PAIR(id) then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
local object = ecs_pair_second(world, id)
|
local object = ecs_pair_second(world, id)
|
||||||
if object == delete then
|
if object ~= delete then
|
||||||
local id_record = component_index[id]
|
continue
|
||||||
local flags = id_record.flags
|
end
|
||||||
local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE)
|
local id_record = component_index[id]
|
||||||
if flags_delete_mask ~= 0 then
|
local flags = id_record.flags
|
||||||
for i = n, 1, -1 do
|
local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE)
|
||||||
world_delete(world, children[i])
|
if flags_delete_mask ~= 0 then
|
||||||
end
|
for i = #entities, 1, -1 do
|
||||||
break
|
local child = entities[i]
|
||||||
else
|
world_delete(world, child)
|
||||||
local on_remove = id_record.hooks.on_remove
|
|
||||||
local to = archetype_traverse_remove(world, id, idr_t_archetype)
|
|
||||||
local empty = #to.types == 0
|
|
||||||
for i = n, 1, -1 do
|
|
||||||
local child = children[i]
|
|
||||||
if on_remove then
|
|
||||||
on_remove(child)
|
|
||||||
end
|
|
||||||
local r = sparse_array[ECS_ENTITY_T_LO(child)]
|
|
||||||
if not empty then
|
|
||||||
entity_move(entity_index, child, r, to)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
break
|
||||||
|
else
|
||||||
|
if not ids then
|
||||||
|
ids = {}
|
||||||
|
end
|
||||||
|
ids[id] = true
|
||||||
|
removal_queued = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
archetype_destroy(world, idr_t_archetype)
|
if not removal_queued then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
if not children then
|
||||||
|
children = {}
|
||||||
|
end
|
||||||
|
local n = #entities
|
||||||
|
table.move(entities, 1, n, count + 1, children)
|
||||||
|
count += n
|
||||||
|
end
|
||||||
|
|
||||||
|
if ids then
|
||||||
|
for id in ids do
|
||||||
|
for _, child in children do
|
||||||
|
world_remove(world, child, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for archetype_id in archetype_ids do
|
||||||
|
archetype_destroy(world, archetypes[archetype_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2096,7 +2107,7 @@ local function world_query(world: World, ...)
|
||||||
return q
|
return q
|
||||||
end
|
end
|
||||||
|
|
||||||
for archetype_id in idr.columns do
|
for archetype_id in idr.cache do
|
||||||
local compatibleArchetype = archetypes[archetype_id]
|
local compatibleArchetype = archetypes[archetype_id]
|
||||||
if #compatibleArchetype.entities == 0 then
|
if #compatibleArchetype.entities == 0 then
|
||||||
continue
|
continue
|
||||||
|
@ -2130,9 +2141,9 @@ local function world_each(world: World, id): () -> ()
|
||||||
return NOOP
|
return NOOP
|
||||||
end
|
end
|
||||||
|
|
||||||
local idr_columns = idr.columns
|
local idr_cache = idr.cache
|
||||||
local archetypes = world.archetypes
|
local archetypes = world.archetypes
|
||||||
local archetype_id = next(idr_columns, nil) :: number
|
local archetype_id = next(idr_cache, nil) :: number
|
||||||
local archetype = archetypes[archetype_id]
|
local archetype = archetypes[archetype_id]
|
||||||
if not archetype then
|
if not archetype then
|
||||||
return NOOP
|
return NOOP
|
||||||
|
@ -2144,7 +2155,7 @@ local function world_each(world: World, id): () -> ()
|
||||||
return function(): any
|
return function(): any
|
||||||
local entity = entities[row]
|
local entity = entities[row]
|
||||||
while not entity do
|
while not entity do
|
||||||
archetype_id = next(idr_columns, archetype_id) :: number
|
archetype_id = next(idr_cache, archetype_id) :: number
|
||||||
if not archetype_id then
|
if not archetype_id then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
@ -2477,6 +2488,7 @@ export type World = {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
World = World :: { new: () -> World },
|
World = World :: { new: () -> World },
|
||||||
|
world = World.new :: () -> World,
|
||||||
|
|
||||||
OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>,
|
OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>,
|
||||||
OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>,
|
OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>,
|
||||||
|
@ -2500,6 +2512,8 @@ return {
|
||||||
ECS_GENERATION = ECS_GENERATION,
|
ECS_GENERATION = ECS_GENERATION,
|
||||||
ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD,
|
ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD,
|
||||||
|
|
||||||
|
ECS_ID_DELETE = ECS_ID_DELETE,
|
||||||
|
|
||||||
IS_PAIR = ECS_IS_PAIR,
|
IS_PAIR = ECS_IS_PAIR,
|
||||||
pair_first = ecs_pair_first,
|
pair_first = ecs_pair_first,
|
||||||
pair_second = ecs_pair_second,
|
pair_second = ecs_pair_second,
|
||||||
|
|
56
mkdocs.yml
56
mkdocs.yml
|
@ -51,8 +51,64 @@ extra_css:
|
||||||
extra_javascript:
|
extra_javascript:
|
||||||
- assets/scripts/smooth-scroll.js
|
- assets/scripts/smooth-scroll.js
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
- mike:
|
||||||
|
version_selector: true
|
||||||
|
canonical_version: latest
|
||||||
|
- mkdocstrings:
|
||||||
|
default_handler: python
|
||||||
|
handlers:
|
||||||
|
python:
|
||||||
|
import:
|
||||||
|
- https://installer.github.io/docs/python-objects.inv
|
||||||
|
- https://docs.python.org/3/objects.inv
|
||||||
|
selection:
|
||||||
|
members: true
|
||||||
|
rendering:
|
||||||
|
show_source: true
|
||||||
|
show_submodules: true
|
||||||
|
paths: [src]
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
|
- Learn:
|
||||||
|
- learn/index.md
|
||||||
|
- Getting Started:
|
||||||
|
- Installation: learn/getting-started/installation.md
|
||||||
|
- Quick start: learn/getting-started/quick-start.md
|
||||||
|
- Concepts:
|
||||||
|
- Entities & Components: learn/concepts/entities-components.md
|
||||||
|
- Queries: learn/concepts/queries.md
|
||||||
|
- Component Traits: learn/concepts/component-traits.md
|
||||||
|
- Command line: learn/concepts/command-line.md
|
||||||
|
- Addons: learn/concepts/addons.md
|
||||||
|
- Guides:
|
||||||
|
- learn/guides/index.md
|
||||||
|
- Hello, World: learn/guides/hello-world.md
|
||||||
|
- Reading Queries: learn/guides/reading-queries.md
|
||||||
|
- Pair Rules: learn/guides/pair-rules.md
|
||||||
|
- Tagging: learn/guides/tagging.md
|
||||||
|
- Removing Components: learn/guides/removing-components.md
|
||||||
|
- Bulk Operations: learn/guides/bulk.md
|
||||||
|
- Bulk Queries: learn/guides/bulk-queries.md
|
||||||
|
- Debugging: learn/guides/debugging.md
|
||||||
|
- Naming: learn/guides/naming.md
|
||||||
|
- Building Applications: learn/guides/building-applications.md
|
||||||
|
- Troubleshoot: learn/guides/troubleshoot.md
|
||||||
|
- Migration from Matter: learn/migration-from-matter.md
|
||||||
|
- Observer APIs: learn/observers.md
|
||||||
|
- FAQ: learn/faq/index.md
|
||||||
|
- API:
|
||||||
|
- api/jecs.md
|
||||||
|
- World: api/world.md
|
||||||
|
- Query: api/query.md
|
||||||
|
- Entity Management: api/world-entity-management.md
|
||||||
|
- Name Component: api/name.md
|
||||||
|
- Contributing:
|
||||||
|
- contributing/index.md
|
||||||
|
- Roadmap: contributing/roadmap.md
|
||||||
|
- How to Contribute: learn/faq/contributing.md
|
||||||
- Tutorials:
|
- Tutorials:
|
||||||
- Get Started: tutorials/index.md
|
- Get Started: tutorials/index.md
|
||||||
- Installing Fusion: tutorials/get-started/installing-fusion.md
|
- Installing Fusion: tutorials/get-started/installing-fusion.md
|
||||||
|
|
25
test/devtools_test.luau
Normal file
25
test/devtools_test.luau
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local pair = jecs.pair
|
||||||
|
local ChildOf = jecs.ChildOf
|
||||||
|
local lifetime_tracker_add = require("@tools/lifetime_tracker")
|
||||||
|
local pe = require("@tools/entity_visualiser").prettify
|
||||||
|
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
|
||||||
|
local FriendsWith = world:component()
|
||||||
|
local _1 = world:print_snapshot()
|
||||||
|
local e1 = world:entity()
|
||||||
|
local e2 = world:entity()
|
||||||
|
world:delete(e2)
|
||||||
|
|
||||||
|
local _2 = world:print_snapshot()
|
||||||
|
local e3 = world:entity()
|
||||||
|
world:add(e3, pair(ChildOf, e1))
|
||||||
|
local e4 = world:entity()
|
||||||
|
world:add(e4, pair(FriendsWith, e3))
|
||||||
|
local _3 = world:print_snapshot()
|
||||||
|
world:delete(e1)
|
||||||
|
world:delete(e3)
|
||||||
|
local _4 = world:print_snapshot()
|
||||||
|
world:print_entity_index()
|
||||||
|
world:entity()
|
||||||
|
world:entity()
|
||||||
|
local _5 = world:print_snapshot()
|
|
@ -26,6 +26,50 @@ local N = 2 ^ 8
|
||||||
type World = jecs.World
|
type World = jecs.World
|
||||||
type Entity<T=nil> = jecs.Entity<T>
|
type Entity<T=nil> = jecs.Entity<T>
|
||||||
|
|
||||||
|
local c = {
|
||||||
|
white_underline = function(s: any)
|
||||||
|
return `\27[1;4m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
white = function(s: any)
|
||||||
|
return `\27[37;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
green = function(s: any)
|
||||||
|
return `\27[32;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
red = function(s: any)
|
||||||
|
return `\27[31;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
yellow = function(s: any)
|
||||||
|
return `\27[33;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
red_highlight = function(s: any)
|
||||||
|
return `\27[41;1;30m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
green_highlight = function(s: any)
|
||||||
|
return `\27[42;1;30m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
gray = function(s: any)
|
||||||
|
return `\27[30;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function pe(e)
|
||||||
|
local gen = ECS_GENERATION(e)
|
||||||
|
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function pp(e)
|
||||||
|
local gen = ECS_GENERATION(e)
|
||||||
|
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{jecs.ECS_ENTITY_T_HI(e)}`)
|
||||||
|
end
|
||||||
|
|
||||||
local function debug_world_inspect(world: World)
|
local function debug_world_inspect(world: World)
|
||||||
local function record(e): jecs.Record
|
local function record(e): jecs.Record
|
||||||
return entity_index_try_get_any(world.entity_index, e) :: any
|
return entity_index_try_get_any(world.entity_index, e) :: any
|
||||||
|
@ -67,10 +111,61 @@ local function debug_world_inspect(world: World)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local dwi = debug_world_inspect
|
||||||
|
|
||||||
local function name(world, e)
|
local function name(world, e)
|
||||||
return world:get(e, jecs.Name)
|
return world:get(e, jecs.Name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
TEST("#repro", function()
|
||||||
|
local world = world_new()
|
||||||
|
|
||||||
|
local function getTargets(relation)
|
||||||
|
local tgts = {}
|
||||||
|
local pairwildcard = pair(relation, jecs.Wildcard)
|
||||||
|
for _, archetype in world:query(pairwildcard):archetypes() do
|
||||||
|
local tr = archetype.records[pairwildcard]
|
||||||
|
local count = archetype.counts[pairwildcard]
|
||||||
|
local types = archetype.types
|
||||||
|
for _, entity in archetype.entities do
|
||||||
|
for i = 0, count - 1 do
|
||||||
|
local tgt = jecs.pair_second(world, types[i + tr])
|
||||||
|
table.insert(tgts, tgt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return tgts
|
||||||
|
end
|
||||||
|
|
||||||
|
local Attacks = world:component()
|
||||||
|
local Eats = world:component()
|
||||||
|
|
||||||
|
local function setAttacksAndEats(entity1, entity2)
|
||||||
|
world:add(entity1, pair(Attacks, entity2))
|
||||||
|
world:add(entity1, pair(Eats, entity2))
|
||||||
|
end
|
||||||
|
|
||||||
|
local e1 = world:entity()
|
||||||
|
local e2 = world:entity()
|
||||||
|
local e3 = world:entity()
|
||||||
|
print(e1, e2, e3)
|
||||||
|
setAttacksAndEats(e3, e1)
|
||||||
|
setAttacksAndEats(e3, e2)
|
||||||
|
setAttacksAndEats(e1, e2)
|
||||||
|
print("---------------- delete e2 ---------------")
|
||||||
|
local d = dwi(world)
|
||||||
|
for archetype_id in world.component_index[pair(jecs.Wildcard, e2)].cache do
|
||||||
|
local archetype = world.archetypes[archetype_id].type
|
||||||
|
testkit.print(archetype)
|
||||||
|
end
|
||||||
|
world:delete(e2)
|
||||||
|
print("-----------------------------")
|
||||||
|
testkit.print(d.tbl(e1).types)
|
||||||
|
-- testkit.print(d.tbl(e3).types)
|
||||||
|
-- testkit.print(getTargets(Attacks))
|
||||||
|
-- testkit.print(getTargets(Eats))
|
||||||
|
end)
|
||||||
|
|
||||||
TEST("archetype", function()
|
TEST("archetype", function()
|
||||||
local archetype_traverse_add = jecs.archetype_traverse_add
|
local archetype_traverse_add = jecs.archetype_traverse_add
|
||||||
local archetype_traverse_remove = jecs.archetype_traverse_remove
|
local archetype_traverse_remove = jecs.archetype_traverse_remove
|
||||||
|
@ -1199,6 +1294,7 @@ TEST("world:delete", function()
|
||||||
world:delete(e)
|
world:delete(e)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local d = debug_world_inspect(world)
|
||||||
for i, friend in friends do
|
for i, friend in friends do
|
||||||
CHECK(not world:has(friend, pair(FriendsWith, e)))
|
CHECK(not world:has(friend, pair(FriendsWith, e)))
|
||||||
CHECK(world:has(friend, Health))
|
CHECK(world:has(friend, Health))
|
||||||
|
|
33
tools/ansi.luau
Normal file
33
tools/ansi.luau
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
return {
|
||||||
|
white_underline = function(s: any)
|
||||||
|
return `\27[1;4m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
white = function(s: any)
|
||||||
|
return `\27[37;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
green = function(s: any)
|
||||||
|
return `\27[32;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
red = function(s: any)
|
||||||
|
return `\27[31;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
yellow = function(s: any)
|
||||||
|
return `\27[33;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
red_highlight = function(s: any)
|
||||||
|
return `\27[41;1;30m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
green_highlight = function(s: any)
|
||||||
|
return `\27[42;1;30m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
|
||||||
|
gray = function(s: any)
|
||||||
|
return `\27[30;1m{s}\27[0m`
|
||||||
|
end,
|
||||||
|
}
|
43
tools/entity_visualiser.luau
Normal file
43
tools/entity_visualiser.luau
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local ECS_GENERATION = jecs.ECS_GENERATION
|
||||||
|
local ECS_ID = jecs.ECS_ID
|
||||||
|
local ansi = require("@tools/ansi")
|
||||||
|
|
||||||
|
local function pe(e: any)
|
||||||
|
local gen = ECS_GENERATION(e)
|
||||||
|
return ansi.green(`e{ECS_ID(e)}`)..ansi.yellow(`v{gen}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function name(world: jecs.World, id: any)
|
||||||
|
return world:get(id, jecs.Name) or `${id}`
|
||||||
|
end
|
||||||
|
|
||||||
|
local function components(world: jecs.World, entity: any)
|
||||||
|
local r = jecs.entity_index_try_get(world.entity_index, entity)
|
||||||
|
if not r then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local archetype = r.archetype
|
||||||
|
local row = r.row
|
||||||
|
print(`Entity {pe(entity)}`)
|
||||||
|
print("-----------------------------------------------------")
|
||||||
|
for i, column in archetype.columns do
|
||||||
|
local component = archetype.types[i]
|
||||||
|
local n
|
||||||
|
if jecs.IS_PAIR(component) then
|
||||||
|
n = `({name(world, jecs.pair_first(world, component))}, {name(world, jecs.pair_second(world, component))})`
|
||||||
|
else
|
||||||
|
n = name(world, component)
|
||||||
|
end
|
||||||
|
local data = column[row] or "TAG"
|
||||||
|
print(`| {n} | {data} |`)
|
||||||
|
end
|
||||||
|
print("-----------------------------------------------------")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
components = components,
|
||||||
|
prettify = pe,
|
||||||
|
}
|
212
tools/lifetime_tracker.luau
Normal file
212
tools/lifetime_tracker.luau
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local ECS_GENERATION = jecs.ECS_GENERATION
|
||||||
|
local ECS_ID = jecs.ECS_ID
|
||||||
|
local __ = jecs.Wildcard
|
||||||
|
local pair = jecs.pair
|
||||||
|
|
||||||
|
local prettify = require("@tools/entity_visualiser").prettify
|
||||||
|
|
||||||
|
local pe = prettify
|
||||||
|
local ansi = require("@tools/ansi")
|
||||||
|
|
||||||
|
function print_centered_entity(entity, width: number)
|
||||||
|
local entity_str = tostring(entity)
|
||||||
|
local entity_length = #entity_str
|
||||||
|
|
||||||
|
local padding_total = width - 2 - entity_length
|
||||||
|
|
||||||
|
local padding_left = math.floor(padding_total / 2)
|
||||||
|
local padding_right = padding_total - padding_left
|
||||||
|
|
||||||
|
local centered_str = string.rep(" ", padding_left) .. entity_str .. string.rep(" ", padding_right)
|
||||||
|
|
||||||
|
print("|" .. centered_str .. "|")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function name(world, e)
|
||||||
|
return world:get(world, e, jecs.Name) or pe(e)
|
||||||
|
end
|
||||||
|
local padding_enabled = false
|
||||||
|
local function pad()
|
||||||
|
if padding_enabled then
|
||||||
|
print("")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function lifetime_tracker_add(world: jecs.World, opt)
|
||||||
|
local entity_index = world.entity_index
|
||||||
|
local dense_array = entity_index.dense_array
|
||||||
|
local component_index = world.component_index
|
||||||
|
|
||||||
|
local ENTITY_RANGE = (jecs.Rest :: any) + 1
|
||||||
|
|
||||||
|
local w = setmetatable({}, { __index = world })
|
||||||
|
|
||||||
|
padding_enabled = opt.padding_enabled
|
||||||
|
|
||||||
|
local world_entity = world.entity
|
||||||
|
w.entity = function(self)
|
||||||
|
local will_recycle = entity_index.max_id ~= entity_index.alive_count
|
||||||
|
local e = world_entity(world)
|
||||||
|
if will_recycle then
|
||||||
|
print(`*recycled {pe(e)}`)
|
||||||
|
else
|
||||||
|
print(`*created {pe(e)}`)
|
||||||
|
end
|
||||||
|
pad()
|
||||||
|
return e
|
||||||
|
end
|
||||||
|
w.print_entity_index = function(self)
|
||||||
|
local max_id = entity_index.max_id
|
||||||
|
local alive_count = entity_index.alive_count
|
||||||
|
local alive = table.move(dense_array, 1+jecs.Rest::any, alive_count, 1, {})
|
||||||
|
local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})
|
||||||
|
|
||||||
|
local sep = "|--------|"
|
||||||
|
if #alive > 0 then
|
||||||
|
print("|-alive--|")
|
||||||
|
for i = 1, #alive do
|
||||||
|
local e = pe(alive[i])
|
||||||
|
print_centered_entity(e, 32)
|
||||||
|
print(sep)
|
||||||
|
end
|
||||||
|
print("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
if #dead > 0 then
|
||||||
|
print("|--dead--|")
|
||||||
|
for i = 1, #dead do
|
||||||
|
print_centered_entity(pe(dead[i]), 32)
|
||||||
|
print(sep)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pad()
|
||||||
|
end
|
||||||
|
local timelines = {}
|
||||||
|
w.print_snapshot = function(self)
|
||||||
|
local timeline = #timelines + 1
|
||||||
|
local entity_column_width = 10
|
||||||
|
local status_column_width = 8
|
||||||
|
|
||||||
|
local header = string.format("| %-" .. entity_column_width .. "s |", "Entity")
|
||||||
|
for i = 1, timeline do
|
||||||
|
header = header .. string.format(" %-" .. status_column_width .. "s |", string.format("T%d", i))
|
||||||
|
end
|
||||||
|
|
||||||
|
local max_id = entity_index.max_id
|
||||||
|
local alive_count = entity_index.alive_count
|
||||||
|
local alive = table.move(dense_array, 1+jecs.Rest::any, alive_count, 1, {})
|
||||||
|
local dead = table.move(dense_array, alive_count + 1, max_id, 1, {})
|
||||||
|
|
||||||
|
local data = {}
|
||||||
|
print("-------------------------------------------------------------------")
|
||||||
|
print(header)
|
||||||
|
|
||||||
|
-- Store the snapshot data for this timeline
|
||||||
|
for i = ENTITY_RANGE, max_id do
|
||||||
|
if dense_array[i] then
|
||||||
|
local entity = dense_array[i]
|
||||||
|
local id = ECS_ID(entity)
|
||||||
|
local status = "alive"
|
||||||
|
if id > alive_count then
|
||||||
|
status = "dead"
|
||||||
|
end
|
||||||
|
data[id] = status
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(timelines, data)
|
||||||
|
|
||||||
|
-- Create a table to hold entity data for sorting
|
||||||
|
local entities = {}
|
||||||
|
for i = ENTITY_RANGE, max_id do
|
||||||
|
if dense_array[i] then
|
||||||
|
local entity = dense_array[i]
|
||||||
|
local id = ECS_ID(entity)
|
||||||
|
-- Push entity and id into the new `entities` table
|
||||||
|
table.insert(entities, {entity = entity, id = id})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sort the entities by ECS_ID
|
||||||
|
table.sort(entities, function(a, b)
|
||||||
|
return a.id < b.id
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Print the sorted rows
|
||||||
|
for _, entity_data in ipairs(entities) do
|
||||||
|
local entity = entity_data.entity
|
||||||
|
local id = entity_data.id
|
||||||
|
local status = "alive"
|
||||||
|
if id > alive_count then
|
||||||
|
status = "dead"
|
||||||
|
end
|
||||||
|
local row = string.format("| %-" .. entity_column_width .. "s |", pe(entity))
|
||||||
|
for j = 1, timeline do
|
||||||
|
local timeline_data = timelines[j]
|
||||||
|
local entity_data = timeline_data[id]
|
||||||
|
if entity_data then
|
||||||
|
row = row .. string.format(" %-" .. status_column_width .. "s |", entity_data)
|
||||||
|
else
|
||||||
|
row = row .. string.format(" %-" .. status_column_width .. "s |", "-")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
print(row)
|
||||||
|
end
|
||||||
|
print("-------------------------------------------------------------------")
|
||||||
|
pad()
|
||||||
|
end
|
||||||
|
local world_add = world.add
|
||||||
|
local relations = {}
|
||||||
|
w.add = function(self, entity: any, component: any)
|
||||||
|
world_add(world, entity, component)
|
||||||
|
if jecs.IS_PAIR(component) then
|
||||||
|
local relation = jecs.pair_first(world, component)
|
||||||
|
local target = jecs.pair_second(world, component)
|
||||||
|
print(`*added ({pe(relation)}, {pe(target)}) to {pe(entity)}`)
|
||||||
|
pad()
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
local world_delete = world.delete
|
||||||
|
w.delete = function(self, e)
|
||||||
|
world_delete(world, e)
|
||||||
|
|
||||||
|
local idr_t = component_index[pair(__, e)]
|
||||||
|
if idr_t then
|
||||||
|
for archetype_id in idr_t.cache do
|
||||||
|
local archetype = world.archetypes[archetype_id]
|
||||||
|
for _, id in archetype.types do
|
||||||
|
if not jecs.IS_PAIR(id) then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
local object = jecs.pair_second(world, id)
|
||||||
|
if object ~= e then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
local id_record = component_index[id]
|
||||||
|
local flags = id_record.flags
|
||||||
|
local flags_delete_mask: number = bit32.band(flags, jecs.ECS_ID_DELETE)
|
||||||
|
if flags_delete_mask ~= 0 then
|
||||||
|
for _, entity in archetype.entities do
|
||||||
|
print(`*deleted dependant {pe(entity)} of {pe(e)}`)
|
||||||
|
pad()
|
||||||
|
end
|
||||||
|
break
|
||||||
|
else
|
||||||
|
for _, entity in archetype.entities do
|
||||||
|
print(`*removed dependency ({pe(jecs.pair_first(world, id))}, {pe(object)}) from {pe(entity)}`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
print(`*deleted {pe(e)}`)
|
||||||
|
pad()
|
||||||
|
end
|
||||||
|
return w
|
||||||
|
end
|
||||||
|
|
||||||
|
return lifetime_tracker_add
|
Loading…
Reference in a new issue