Compare commits

...

11 commits

Author SHA1 Message Date
Denizhan Dakılır
bf3c3fa375
Merge 7b870e8f3c into b26fc39fce 2025-03-12 21:08:14 +01:00
Ukendio
b26fc39fce Add options to lifetime tracker
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-12 18:49:18 +01:00
Ukendio
0e4f40ced7 Improved emit for devtools 2025-03-12 17:12:25 +01:00
Ukendio
de8e263828 Add entity visualiser
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-03-12 16:30:19 +01:00
Ukendio
d15266b6d5 Devtools initial commit 2025-03-12 15:30:56 +01:00
Ukendio
18019679d5 Alias for world consttructor 2025-03-12 15:29:24 +01:00
Ukendio
925864dd2b Merge branch 'main' of https://github.com/Ukendio/jecs 2025-03-12 13:10:51 +01:00
Ukendio
ac79638599 Prevent invalidation during removal of pairs with deleted target 2025-03-12 13:10:17 +01:00
Ukendio
f7573e8824 Removal being invalidated during removal of deleted target
Prevent invalidation during removal of pairs with deleted targe
2025-03-12 13:08:19 +01:00
Denizhan Dakılır
7b870e8f3c Add comprehensive documentation for jecs library 2025-02-25 05:04:54 +03:00
Denizhan Dakılır
e668371cc1 Start work on bounty 2025-02-25 04:37:22 +03:00
20 changed files with 1689 additions and 71 deletions

4
.gitignore vendored
View file

@ -65,3 +65,7 @@ drafts/
# Luau tools
profile.*
# Patch files
*.patch

View file

@ -1,8 +1,9 @@
{
"aliases": {
"jecs": "jecs",
"testkit": "test/testkit",
"mirror": "mirror"
"testkit": "tools/testkit",
"mirror": "mirror",
"tools": "tools",
},
"languageMode": "strict"
}

View file

@ -62,3 +62,32 @@ Can be found under /benches/visual/query.luau
Inserting 8 components to an entity and updating them over 50 times.
![Insertions](assets/image-4.png)
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.

View file

@ -5,41 +5,60 @@ 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 newWorld = Matter.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 C, D = ecs:component(), ecs:component()
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 registry2 = ecr.registry()
return {
ParameterGenerator = function()
local matter_entities = {}
local jecs_entities = {}
local entities = {
matter = matter_entities,
jecs = jecs_entities,
}
local j = ecs:entity()
ecs:set(j, C1, true)
local m = mcs:entity()
mcs:set(m, E1, true)
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)
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 entities
return {
m = m,
j = j,
}
end,
Functions = {
Matter = function(_, entities)
for _, entity in entities.matter do
newWorld:despawn(entity)
end
Mirror = function(_, a)
mcs:delete(a.m)
end,
Jecs = function(_, entities)
for _, entity in entities.jecs do
ecs:delete(entity)
end
Jecs = function(_, a)
ecs:delete(a.j)
end,
},
}

View 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

View file

@ -16,6 +16,12 @@ jecs.Wildcard: Entity
```
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
```luau
jecs.Component: Entity
@ -28,6 +34,54 @@ jecs.ChildOf: Entity
```
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
```luau
jecs.Rest: Entity

130
docs/api/name.md Normal file
View 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.

View 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.

View file

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

View 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
View 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.

View file

@ -46,7 +46,7 @@ export type Record = {
}
type IdRecord = {
columns: { number },
cache: { number },
counts: { number },
flags: number,
size: number,
@ -480,7 +480,7 @@ local function world_target(world: World, entity: i53, relation: i24, index: num
nth = nth + count + 1
end
local tr = idr.columns[archetype_id]
local tr = idr.cache[archetype_id]
nth = archetype.types[nth + tr]
@ -537,7 +537,7 @@ local function id_record_ensure(world: World, id: number): IdRecord
idr = {
size = 0,
columns = {},
cache = {},
counts = {},
flags = flags,
hooks = {
@ -562,7 +562,7 @@ local function archetype_append_to_records(
local archetype_id = archetype.id
local archetype_records = archetype.records
local archetype_counts = archetype.counts
local idr_columns = idr.columns
local idr_columns = idr.cache
local idr_counts = idr.counts
local tr = idr_columns[archetype_id]
if not tr then
@ -1063,7 +1063,7 @@ local function archetype_destroy(world: World, archetype: Archetype)
for id in records do
local idr = component_index[id]
idr.columns[archetype_id] = nil :: any
idr.cache[archetype_id] = nil :: any
idr.counts[archetype_id] = nil
idr.size -= 1
records[id] = nil :: any
@ -1122,7 +1122,7 @@ do
if idr then
local flags = idr.flags
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 entities = idr_archetype.entities
@ -1134,7 +1134,7 @@ do
archetype_destroy(world, idr_archetype)
end
else
for archetype_id in idr.columns do
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities
local n = #entities
@ -1147,55 +1147,66 @@ do
end
end
local sparse_array = entity_index.sparse_array
local dense_array = entity_index.dense_array
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_types = idr_t_archetype.types
for _, child in idr_t_archetype.entities do
table.insert(children, child)
end
local n = #children
local entities = idr_t_archetype.entities
local removal_queued = false
for _, id in idr_t_types do
if not ECS_IS_PAIR(id) then
continue
end
local object = ecs_pair_second(world, id)
if object == delete then
if object ~= delete then
continue
end
local id_record = component_index[id]
local flags = id_record.flags
local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE)
if flags_delete_mask ~= 0 then
for i = n, 1, -1 do
world_delete(world, children[i])
for i = #entities, 1, -1 do
local child = entities[i]
world_delete(world, child)
end
break
else
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)
if not ids then
ids = {}
end
local r = sparse_array[ECS_ENTITY_T_LO(child)]
if not empty then
entity_move(entity_index, child, r, to)
ids[id] = true
removal_queued = true
end
end
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
archetype_destroy(world, idr_t_archetype)
for archetype_id in archetype_ids do
archetype_destroy(world, archetypes[archetype_id])
end
end
@ -2096,7 +2107,7 @@ local function world_query(world: World, ...)
return q
end
for archetype_id in idr.columns do
for archetype_id in idr.cache do
local compatibleArchetype = archetypes[archetype_id]
if #compatibleArchetype.entities == 0 then
continue
@ -2130,9 +2141,9 @@ local function world_each(world: World, id): () -> ()
return NOOP
end
local idr_columns = idr.columns
local idr_cache = idr.cache
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]
if not archetype then
return NOOP
@ -2144,7 +2155,7 @@ local function world_each(world: World, id): () -> ()
return function(): any
local entity = entities[row]
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
return
end
@ -2477,6 +2488,7 @@ export type World = {
return {
World = World :: { new: () -> World },
world = World.new :: () -> World,
OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>,
OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>,
@ -2500,6 +2512,8 @@ return {
ECS_GENERATION = ECS_GENERATION,
ECS_ID_IS_WILDCARD = ECS_ID_IS_WILDCARD,
ECS_ID_DELETE = ECS_ID_DELETE,
IS_PAIR = ECS_IS_PAIR,
pair_first = ecs_pair_first,
pair_second = ecs_pair_second,

View file

@ -51,8 +51,64 @@ extra_css:
extra_javascript:
- 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:
- 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:
- Get Started: tutorials/index.md
- Installing Fusion: tutorials/get-started/installing-fusion.md

25
test/devtools_test.luau Normal file
View 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()

View file

@ -26,6 +26,50 @@ local N = 2 ^ 8
type World = jecs.World
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 record(e): jecs.Record
return entity_index_try_get_any(world.entity_index, e) :: any
@ -67,10 +111,61 @@ local function debug_world_inspect(world: World)
}
end
local dwi = debug_world_inspect
local function name(world, e)
return world:get(e, jecs.Name)
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()
local archetype_traverse_add = jecs.archetype_traverse_add
local archetype_traverse_remove = jecs.archetype_traverse_remove
@ -1199,6 +1294,7 @@ TEST("world:delete", function()
world:delete(e)
end)
local d = debug_world_inspect(world)
for i, friend in friends do
CHECK(not world:has(friend, pair(FriendsWith, e)))
CHECK(world:has(friend, Health))

33
tools/ansi.luau Normal file
View 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,
}

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