Target should return the entity with its generation
Some checks are pending
22
assets/.github/ISSUE_TEMPLATE/BUG-REPORT.md
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: File a bug report for any behavior that you believe is unintentional or problematic
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Describe the bug
|
||||||
|
Put a clear and concise description of what the bug is. This should be short and to the point, not to exceed more than a paragraph. Put the details inside your reproduction steps.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
Make an easy-to-follow guide on how to reproduce it. Does it happen all the time? Will specific features affect reproduction? All these questions should be answered for a good issue.
|
||||||
|
|
||||||
|
This is a good place to put rbxl files or scripts that help explain your reproduction steps.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What you expect to happen
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
What actually happens
|
14
assets/.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
name: Documentation
|
||||||
|
about: Open an issue to add, change, or otherwise modify any part of the documentation.
|
||||||
|
title: "[DOCS]"
|
||||||
|
labels: documentation
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Which Sections Does This Issue Cover?
|
||||||
|
[Put sections (e.g. Query Concepts), page links, etc as necessary]
|
||||||
|
|
||||||
|
## What Needs To Change?
|
||||||
|
What specifically needs to change and what suggestions do you have to change it?
|
27
assets/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: File a feature request for something you believe should be added to Jecs
|
||||||
|
title: "[FEATURE]"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Describe your Feature
|
||||||
|
|
||||||
|
You should explain your feature here, and the motivation for why you want it.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Explain how you would implement your feature here. Provide relevant API examples and such here (if applicable).
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
What other alternative implementations or otherwise relevant information is important to why you decided to go with this specific implementation?
|
||||||
|
|
||||||
|
## Considerations
|
||||||
|
|
||||||
|
Some questions that need to be answered include the following:
|
||||||
|
- Will old code break in response to this feature?
|
||||||
|
- What are the performance impacts with this feature (if any)?
|
||||||
|
- How is it useful to include?
|
15
assets/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
## Brief Description of your Changes.
|
||||||
|
|
||||||
|
Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
|
||||||
|
|
||||||
|
## Impact of your Changes
|
||||||
|
|
||||||
|
What implications will this have on the project? Will there be altered behavior or performance with this change?
|
||||||
|
|
||||||
|
## Tests Performed
|
||||||
|
|
||||||
|
What have you done to ensure this change has the least possible impact on the project?
|
||||||
|
|
||||||
|
## Additional Comments
|
||||||
|
|
||||||
|
Anything else you feel is relevant.
|
19
assets/.github/workflows/analysis.yaml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
name: analysis
|
||||||
|
|
||||||
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
name: Run Luau Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Luau
|
||||||
|
uses: encodedvenom/install-luau@v2.1
|
||||||
|
|
||||||
|
- name: Analyze
|
||||||
|
run: |
|
||||||
|
output=$(luau-analyze src || true) # Suppress errors for now.
|
11
assets/.github/workflows/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
64
assets/.github/workflows/deploy-docs.yaml
vendored
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# Sample workflow for building and deploying a VitePress site to GitHub Pages
|
||||||
|
#
|
||||||
|
name: deploy-docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
|
||||||
|
# using the `master` branch as the default branch.
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build job
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Not needed if lastUpdated is not enabled
|
||||||
|
# - uses: pnpm/action-setup@v3 # Uncomment this if you're using pnpm
|
||||||
|
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm # or pnpm / yarn
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci # or pnpm install / yarn install / bun install
|
||||||
|
- name: Build with VitePress
|
||||||
|
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: docs/.vitepress/dist
|
||||||
|
|
||||||
|
# Deployment job
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Deploy
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
17
assets/.github/workflows/publish-npm.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: publish-npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
- uses: JS-DevTools/npm-publish@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.NPM_AUTH_TOKEN }}
|
71
assets/.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: wally install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: rojo build --output build.rbxm default.project.json
|
||||||
|
|
||||||
|
- name: Upload Build Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: build.rbxm
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
needs: [build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Jecs Build
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: build
|
||||||
|
|
||||||
|
- name: Rename Build
|
||||||
|
run: mv build/build.rbxm jecs.rbxm
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
name: Jecs ${{ github.ref_name }}
|
||||||
|
files: |
|
||||||
|
jecs.rbxm
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish
|
||||||
|
needs: [release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rokit
|
||||||
|
uses: CompeyDev/setup-rokit@v0.1.2
|
||||||
|
|
||||||
|
- name: Wally Login
|
||||||
|
run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: wally publish
|
31
assets/.github/workflows/unit-testing.yaml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
name: unit-testing
|
||||||
|
|
||||||
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
name: Run Luau Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 2
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Project
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Luau
|
||||||
|
uses: encodedvenom/install-luau@v4.3
|
||||||
|
with:
|
||||||
|
version: "latest"
|
||||||
|
verbose: "true"
|
||||||
|
|
||||||
|
- name: Run Unit Tests
|
||||||
|
id: run_tests
|
||||||
|
run: |
|
||||||
|
output=$(luau test/tests.luau)
|
||||||
|
echo "$output"
|
||||||
|
if [[ "$output" == *"0 fails"* ]]; then
|
||||||
|
echo "Unit Tests Passed"
|
||||||
|
else
|
||||||
|
echo "Error: One or More Unit Tests Failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
20
jecs.luau
|
@ -233,12 +233,12 @@ local function entity_index_is_alive(entity_index: ecs_entity_index_t, entity: i
|
||||||
return entity_index_try_get(entity_index, entity) ~= nil
|
return entity_index_try_get(entity_index, entity) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53
|
local function entity_index_get_alive(index: ecs_entity_index_t, entity: i53): i53?
|
||||||
local r = entity_index_try_get_any(index, entity)
|
local r = entity_index_try_get_any(index, entity)
|
||||||
if r then
|
if r then
|
||||||
return index.dense_array[r.dense]
|
return index.dense_array[r.dense]
|
||||||
end
|
end
|
||||||
return 0
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ecs_get_alive(world, entity)
|
local function ecs_get_alive(world, entity)
|
||||||
|
@ -295,6 +295,10 @@ local function ecs_pair_second(world: ecs_world_t, e: i53)
|
||||||
return ecs_get_alive(world, obj)
|
return ecs_get_alive(world, obj)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function ecs_component_record(world: ecs_world_t, component: i53)
|
||||||
|
return world.component_index[component]
|
||||||
|
end
|
||||||
|
|
||||||
local function query_match(query: ecs_query_data_t,
|
local function query_match(query: ecs_query_data_t,
|
||||||
archetype: ecs_archetype_t)
|
archetype: ecs_archetype_t)
|
||||||
local records = archetype.records
|
local records = archetype.records
|
||||||
|
@ -534,7 +538,8 @@ local function world_target(world: ecs_world_t, entity: i53, relation: i24, inde
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return ECS_PAIR_SECOND(nth)
|
return entity_index_get_alive(world.entity_index,
|
||||||
|
ECS_PAIR_SECOND(nth))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ECS_ID_IS_WILDCARD(e: i53): boolean
|
local function ECS_ID_IS_WILDCARD(e: i53): boolean
|
||||||
|
@ -555,10 +560,10 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t
|
||||||
local is_pair = ECS_IS_PAIR(id)
|
local is_pair = ECS_IS_PAIR(id)
|
||||||
if is_pair then
|
if is_pair then
|
||||||
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id))
|
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id))
|
||||||
assert(relation ~= 0 and entity_index_is_alive(
|
assert(relation and entity_index_is_alive(
|
||||||
entity_index, relation), ECS_INTERNAL_ERROR)
|
entity_index, relation), ECS_INTERNAL_ERROR)
|
||||||
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id))
|
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id))
|
||||||
assert(target ~= 0 and entity_index_is_alive(
|
assert(target and entity_index_is_alive(
|
||||||
entity_index, target), ECS_INTERNAL_ERROR)
|
entity_index, target), ECS_INTERNAL_ERROR)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -662,9 +667,7 @@ local function archetype_create(world: ecs_world_t, id_types: { i24 }, ty, prev:
|
||||||
|
|
||||||
if ECS_IS_PAIR(component_id) then
|
if ECS_IS_PAIR(component_id) then
|
||||||
local relation = ECS_PAIR_FIRST(component_id)
|
local relation = ECS_PAIR_FIRST(component_id)
|
||||||
relation = entity_index_get_alive(entity_index, relation)
|
|
||||||
local object = ECS_PAIR_SECOND(component_id)
|
local object = ECS_PAIR_SECOND(component_id)
|
||||||
object = entity_index_get_alive(entity_index, object)
|
|
||||||
local r = ECS_PAIR(relation, EcsWildcard)
|
local r = ECS_PAIR(relation, EcsWildcard)
|
||||||
local idr_r = id_record_ensure(world, r)
|
local idr_r = id_record_ensure(world, r)
|
||||||
archetype_append_to_records(idr_r, archetype, r, i)
|
archetype_append_to_records(idr_r, archetype, r, i)
|
||||||
|
@ -1259,7 +1262,8 @@ local function world_delete(world: ecs_world_t, entity: i53)
|
||||||
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 = entity_index_get_alive(
|
||||||
|
entity_index, ECS_PAIR_SECOND(id))
|
||||||
if object ~= delete then
|
if object ~= delete then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
11
test/examples/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
This folder contains code examples for the Luau/Typescript APIs.
|
||||||
|
|
||||||
|
## Run with Luau
|
||||||
|
To run the examples with Luau, run the following commands from the root of the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd examples/luau
|
||||||
|
luau path/to/file.luau
|
||||||
|
```
|
43
test/examples/luau/entities/basics.luau
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
local Position = world:component()
|
||||||
|
local Walking = world:component()
|
||||||
|
local Name = world:component()
|
||||||
|
|
||||||
|
-- Create an entity with name Bob
|
||||||
|
local bob = world:entity()
|
||||||
|
|
||||||
|
-- The set operation finds or creates a component, and sets it.
|
||||||
|
world:set(bob, Position, Vector3.new(10, 20, 30))
|
||||||
|
-- Name the entity Bob
|
||||||
|
world:set(bob, Name, "Bob")
|
||||||
|
-- The add operation adds a component without setting a value. This is
|
||||||
|
-- useful for tags, or when adding a component with its default value.
|
||||||
|
world:add(bob, Walking)
|
||||||
|
|
||||||
|
-- Get the value for the Position component
|
||||||
|
local pos = world:get(bob, Position)
|
||||||
|
print(`\{{pos.X}, {pos.Y}, {pos.Z}\}`)
|
||||||
|
|
||||||
|
-- Overwrite the value of the Position component
|
||||||
|
world:set(bob, Position, Vector3.new(40, 50, 60))
|
||||||
|
|
||||||
|
local alice = world:entity()
|
||||||
|
-- Create another named entity
|
||||||
|
world:set(alice, Name, "Alice")
|
||||||
|
world:set(alice, Position, Vector3.new(10, 20, 30))
|
||||||
|
world:add(alice, Walking)
|
||||||
|
|
||||||
|
-- Remove tag
|
||||||
|
world:remove(alice, Walking)
|
||||||
|
|
||||||
|
-- Iterate all entities with Position
|
||||||
|
for entity, p in world:query(Position) do
|
||||||
|
print(`{entity}: \{{p.X}, {p.Y}, {p.Z}\}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- {10, 20, 30}
|
||||||
|
-- Alice: {10, 20, 30}
|
||||||
|
-- Bob: {40, 50, 60}
|
112
test/examples/luau/entities/hierarchy.luau
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local pair = jecs.pair
|
||||||
|
local ChildOf = jecs.ChildOf
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
local Name = world:component()
|
||||||
|
local Position = world:component()
|
||||||
|
local Star = world:component()
|
||||||
|
local Planet = world:component()
|
||||||
|
local Moon = world:component()
|
||||||
|
|
||||||
|
local Vector3
|
||||||
|
do
|
||||||
|
Vector3 = {}
|
||||||
|
Vector3.__index = Vector3
|
||||||
|
|
||||||
|
function Vector3.new(x, y, z)
|
||||||
|
x = x or 0
|
||||||
|
y = y or 0
|
||||||
|
z = z or 0
|
||||||
|
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__add(left, right)
|
||||||
|
return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__mul(left, right)
|
||||||
|
if typeof(right) == "number" then
|
||||||
|
return Vector3.new(left.X * right, left.Y * right, left.Z * right)
|
||||||
|
end
|
||||||
|
return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z)
|
||||||
|
end
|
||||||
|
|
||||||
|
Vector3.one = Vector3.new(1, 1, 1)
|
||||||
|
Vector3.zero = Vector3.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function path(entity)
|
||||||
|
local str = world:get(entity, Name)
|
||||||
|
local parent
|
||||||
|
while true do
|
||||||
|
parent = world:parent(entity)
|
||||||
|
if not parent then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
entity = parent
|
||||||
|
str = world:get(parent, Name) .. "/" .. str
|
||||||
|
end
|
||||||
|
return str
|
||||||
|
end
|
||||||
|
|
||||||
|
local function iterate(entity, parent)
|
||||||
|
local p = world:get(entity, Position)
|
||||||
|
local actual = p + parent
|
||||||
|
print(path(entity))
|
||||||
|
print(`\{{actual.X}, {actual.Y}, {actual.Z}}`)
|
||||||
|
|
||||||
|
for child in world:query(pair(ChildOf, entity)) do
|
||||||
|
--print(world:get(child, Name))
|
||||||
|
iterate(child, actual)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local sun = world:entity()
|
||||||
|
world:add(sun, Star)
|
||||||
|
world:set(sun, Position, Vector3.one)
|
||||||
|
world:set(sun, Name, "Sun")
|
||||||
|
do
|
||||||
|
local earth = world:entity()
|
||||||
|
world:set(earth, Name, "Earth")
|
||||||
|
world:add(earth, pair(ChildOf, sun))
|
||||||
|
world:add(earth, Planet)
|
||||||
|
world:set(earth, Position, Vector3.one * 3)
|
||||||
|
|
||||||
|
do
|
||||||
|
local moon = world:entity()
|
||||||
|
world:set(moon, Name, "Moon")
|
||||||
|
world:add(moon, pair(ChildOf, earth))
|
||||||
|
world:add(moon, Moon)
|
||||||
|
world:set(moon, Position, Vector3.one * 0.1)
|
||||||
|
|
||||||
|
print(`Child of Earth? {world:has(moon, pair(ChildOf, earth))}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
local venus = world:entity()
|
||||||
|
world:set(venus, Name, "Venus")
|
||||||
|
world:add(venus, pair(ChildOf, sun))
|
||||||
|
world:add(venus, Planet)
|
||||||
|
world:set(venus, Position, Vector3.one * 2)
|
||||||
|
|
||||||
|
local mercury = world:entity()
|
||||||
|
world:set(mercury, Name, "Mercury")
|
||||||
|
world:add(mercury, pair(ChildOf, sun))
|
||||||
|
world:add(mercury, Planet)
|
||||||
|
world:set(mercury, Position, Vector3.one)
|
||||||
|
|
||||||
|
iterate(sun, Vector3.zero)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- Child of Earth? true
|
||||||
|
-- Sun
|
||||||
|
-- {1, 1, 1}
|
||||||
|
-- Sun/Mercury
|
||||||
|
-- {2, 2, 2}
|
||||||
|
-- Sun/Venus
|
||||||
|
-- {3, 3, 3}
|
||||||
|
-- Sun/Earth
|
||||||
|
-- {4, 4, 4}
|
||||||
|
-- Sun/Earth/Moon
|
||||||
|
-- {4.1, 4.1, 4.1}
|
21
test/examples/luau/hooks/cleanup.luau
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
local Model = world:component()
|
||||||
|
|
||||||
|
-- It is important to define hooks for the component before the component is ever used
|
||||||
|
-- otherwise the hooks will never invoke!
|
||||||
|
world:set(Model, jecs.OnRemove, function(entity)
|
||||||
|
-- OnRemove is invoked before the component and its value is removed
|
||||||
|
-- which provides a stable reference to the entity at deletion.
|
||||||
|
-- This means that it is safe to retrieve the data inside of a hook
|
||||||
|
local model = world:get(entity, Model)
|
||||||
|
model:Destroy()
|
||||||
|
end)
|
||||||
|
|
||||||
|
world:set(Model, jecs.OnSet, function(entity, model)
|
||||||
|
-- OnSet is invoked after the data has been assigned.
|
||||||
|
-- It also returns the data for faster access.
|
||||||
|
-- There may be some logic to do some side effects on reassignments
|
||||||
|
model:SetAttribute("entityId", entity)
|
||||||
|
end)
|
63
test/examples/luau/queries/basics.luau
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
local Position = world:component()
|
||||||
|
local Velocity = world:component()
|
||||||
|
local Name = world:component()
|
||||||
|
|
||||||
|
local Vector3
|
||||||
|
do
|
||||||
|
Vector3 = {}
|
||||||
|
Vector3.__index = Vector3
|
||||||
|
|
||||||
|
function Vector3.new(x, y, z)
|
||||||
|
x = x or 0
|
||||||
|
y = y or 0
|
||||||
|
z = z or 0
|
||||||
|
return setmetatable({ X = x, Y = y, Z = z }, Vector3)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__add(left, right)
|
||||||
|
return Vector3.new(left.X + right.X, left.Y + right.Y, left.Z + right.Z)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Vector3.__mul(left, right)
|
||||||
|
if typeof(right) == "number" then
|
||||||
|
return Vector3.new(left.X * right, left.Y * right, left.Z * right)
|
||||||
|
end
|
||||||
|
return Vector3.new(left.X * right.X, left.Y * right.Y, left.Z * right.Z)
|
||||||
|
end
|
||||||
|
|
||||||
|
Vector3.one = Vector3.new(1, 1, 1)
|
||||||
|
Vector3.zero = Vector3.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Create a few test entities for a Position, Velocity query
|
||||||
|
local e1 = world:entity()
|
||||||
|
world:set(e1, Name, "e1")
|
||||||
|
world:set(e1, Position, Vector3.new(10, 20, 30))
|
||||||
|
world:set(e1, Velocity, Vector3.new(1, 2, 3))
|
||||||
|
|
||||||
|
local e2 = world:entity()
|
||||||
|
world:set(e2, Name, "e2")
|
||||||
|
world:set(e2, Position, Vector3.new(10, 20, 30))
|
||||||
|
world:set(e2, Velocity, Vector3.new(4, 5, 6))
|
||||||
|
|
||||||
|
-- This entity will not match as it does not have Position, Velocity
|
||||||
|
local e3 = world:entity()
|
||||||
|
world:set(e3, Name, "e3")
|
||||||
|
world:set(e3, Position, Vector3.new(10, 20, 30))
|
||||||
|
|
||||||
|
-- Create an uncached query for Position, Velocity.
|
||||||
|
for entity, p, v in world:query(Position, Velocity) do
|
||||||
|
-- Iterate entities matching the query
|
||||||
|
p.X += v.X
|
||||||
|
p.Y += v.Y
|
||||||
|
p.Z += v.Z
|
||||||
|
|
||||||
|
print(`{world:get(entity, Name)}: \{{p.X}, {p.Y}, {p.Z}}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- e2: {14, 25, 36}
|
||||||
|
-- e1: {11, 22, 33}
|
61
test/examples/luau/queries/changetracking.luau
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local Name = world:component()
|
||||||
|
|
||||||
|
local function named(ctr, name)
|
||||||
|
local e = ctr(world)
|
||||||
|
world:set(e, Name, name)
|
||||||
|
return e
|
||||||
|
end
|
||||||
|
local function name(e)
|
||||||
|
return world:get(e, Name)
|
||||||
|
end
|
||||||
|
|
||||||
|
local Position = named(world.component, "Position") :: jecs.Entity<Vector3>
|
||||||
|
local Previous = jecs.Rest
|
||||||
|
local PreviousPosition = jecs.pair(Previous, Position)
|
||||||
|
|
||||||
|
local added = world
|
||||||
|
:query(Position)
|
||||||
|
:without(PreviousPosition)
|
||||||
|
:cached()
|
||||||
|
local changed = world
|
||||||
|
:query(Position, PreviousPosition)
|
||||||
|
:cached()
|
||||||
|
local removed = world
|
||||||
|
:query(PreviousPosition)
|
||||||
|
:without(Position)
|
||||||
|
:cached()
|
||||||
|
|
||||||
|
local e1 = named(world.entity, "e1")
|
||||||
|
world:set(e1, Position, Vector3.new(10, 20, 30))
|
||||||
|
|
||||||
|
local e2 = named(world.entity, "e2")
|
||||||
|
world:set(e2, Position, Vector3.new(10, 20, 30))
|
||||||
|
|
||||||
|
for e, p in added:iter() do
|
||||||
|
print(`Added {e}: \{{p.X}, {p.Y}, {p.Z}}`)
|
||||||
|
world:set(e, PreviousPosition, p)
|
||||||
|
end
|
||||||
|
|
||||||
|
world:set(e1, Position, "")
|
||||||
|
|
||||||
|
for e, new, old in changed:iter() do
|
||||||
|
if new ~= old then
|
||||||
|
print(`{name(new)}'s Position changed from \{{old.X}, {old.Y}, {old.Z}\} to \{{new.X}, {new.Y}, {new.Z}\}`)
|
||||||
|
world:set(e, PreviousPosition, new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
world:remove(e2, Position)
|
||||||
|
|
||||||
|
for e in removed:iter() do
|
||||||
|
print(`Position was removed from {name(e)}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- Added 265: {10, 20, 30}
|
||||||
|
-- Added 264: {10, 20, 30}
|
||||||
|
-- e1's Position changed from {10, 20, 30} to {999, 999, 1998}
|
||||||
|
-- Position was removed from e2
|
125
test/examples/luau/queries/spatial_grids.luau
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local pair = jecs.pair
|
||||||
|
local ChildOf = jecs.ChildOf
|
||||||
|
local __ = jecs.Wildcard
|
||||||
|
local Name = jecs.Name
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
|
type Id<T = nil> = number & { __T: T }
|
||||||
|
local Voxel = world:component() :: Id
|
||||||
|
local Position = world:component() :: Id<Vector3>
|
||||||
|
local Perception = world:component() :: Id<{
|
||||||
|
range: number,
|
||||||
|
fov: number,
|
||||||
|
dir: Vector3,
|
||||||
|
}>
|
||||||
|
local PrimaryPart = world:component() :: Id<Part>
|
||||||
|
|
||||||
|
local local_player = game:GetService("Players").LocalPlayer
|
||||||
|
|
||||||
|
local function distance(a: Vector3, b: Vector3)
|
||||||
|
return (b - a).Magnitude
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_in_fov(a: Vector3, b: Vector3, forward_dir: Vector3, fov_angle: number)
|
||||||
|
local to_target = b - a
|
||||||
|
|
||||||
|
local forward_xz = Vector3.new(forward_dir.X, 0, forward_dir.Z).Unit
|
||||||
|
local to_target_xz = Vector3.new(to_target.X, 0, to_target.Z).Unit
|
||||||
|
|
||||||
|
local angle_to_target = math.deg(math.atan2(to_target_xz.Z, to_target_xz.X))
|
||||||
|
local forward_angle = math.deg(math.atan2(forward_xz.Z, forward_xz.X))
|
||||||
|
|
||||||
|
local angle_difference = math.abs(forward_angle - angle_to_target)
|
||||||
|
|
||||||
|
if angle_difference > 180 then
|
||||||
|
angle_difference = 360 - angle_difference
|
||||||
|
end
|
||||||
|
|
||||||
|
return angle_difference <= (fov_angle / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local map = {}
|
||||||
|
local grid = 50
|
||||||
|
|
||||||
|
local function add_to_voxel(source: number, position: Vector3, prev_voxel_id: number?)
|
||||||
|
local hash = position // grid
|
||||||
|
local voxel_id = map[hash]
|
||||||
|
if not voxel_id then
|
||||||
|
voxel_id = world:entity()
|
||||||
|
world:add(voxel_id, Voxel)
|
||||||
|
world:set(voxel_id, Position, hash)
|
||||||
|
map[hash] = voxel_id
|
||||||
|
end
|
||||||
|
if prev_voxel_id ~= nil then
|
||||||
|
world:remove(source, pair(ChildOf, prev_voxel_id))
|
||||||
|
end
|
||||||
|
world:add(source, pair(ChildOf, voxel_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reconcile_client_owned_assembly_to_voxel(dt: number)
|
||||||
|
for e, part, position in world:query(PrimaryPart, Position) do
|
||||||
|
local p = part.Position
|
||||||
|
if p ~= position then
|
||||||
|
world:set(e, Position, p)
|
||||||
|
local voxel_id = world:target(e, ChildOf, 0)
|
||||||
|
if map[p // grid] == voxel_id then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
add_to_voxel(e, p, voxel_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_camera_direction(dt: number)
|
||||||
|
for _, perception in world:query(Perception) do
|
||||||
|
perception.dir = workspace.CurrentCamera.CFrame.LookVector
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function perceive_enemies(dt: number)
|
||||||
|
local it = world:query(Perception, Position, PrimaryPart)
|
||||||
|
-- There is only going to be one entity matching the query
|
||||||
|
local e, self_perception, self_position, self_primary_part = it()
|
||||||
|
|
||||||
|
local voxel_id = map[self_primary_part.Position // grid]
|
||||||
|
local nearby_entities_query = world:query(Position, pair(ChildOf, voxel_id))
|
||||||
|
|
||||||
|
for enemy, target_position in nearby_entities_query do
|
||||||
|
if distance(self_position, target_position) > self_perception.range then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_in_fov(self_position, target_position, self_perception.dir, self_perception.fov) then
|
||||||
|
local p = target_position
|
||||||
|
print(`Entity {world:get(e, Name)} can see target {world:get(enemy, Name)} at ({p.X}, {p.Y}, {p.Z})`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local player = world:entity()
|
||||||
|
world:set(player, Perception, {
|
||||||
|
range = 100,
|
||||||
|
fov = 90,
|
||||||
|
dir = Vector3.new(1, 0, 0),
|
||||||
|
})
|
||||||
|
world:set(player, Name, "LocalPlayer")
|
||||||
|
local primary_part = (local_player.Character :: Model).PrimaryPart :: Part
|
||||||
|
world:set(player, PrimaryPart, primary_part)
|
||||||
|
world:set(player, Position, Vector3.zero)
|
||||||
|
|
||||||
|
local enemy = world:entity()
|
||||||
|
world:set(enemy, Name, "Enemy $1")
|
||||||
|
world:set(enemy, Position, Vector3.new(50, 0, 20))
|
||||||
|
|
||||||
|
add_to_voxel(player, primary_part.Position)
|
||||||
|
add_to_voxel(enemy, world)
|
||||||
|
|
||||||
|
local dt = 1 / 60
|
||||||
|
reconcile_client_owned_assembly_to_voxel(dt)
|
||||||
|
update_camera_direction(dt)
|
||||||
|
perceive_enemies(dt)
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- LocalPlayer can see target Enemy $1
|
37
test/examples/luau/queries/wildcards.luau
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
local jecs = require("@jecs")
|
||||||
|
local pair = jecs.pair
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local Name = world:component()
|
||||||
|
|
||||||
|
local function named(ctr, name)
|
||||||
|
local e = ctr(world)
|
||||||
|
world:set(e, Name, name)
|
||||||
|
return e
|
||||||
|
end
|
||||||
|
local function name(e)
|
||||||
|
return world:get(e, Name)
|
||||||
|
end
|
||||||
|
|
||||||
|
local Eats = world:component()
|
||||||
|
local Apples = named(world.entity, "Apples")
|
||||||
|
local Oranges = named(world.entity, "Oranges")
|
||||||
|
|
||||||
|
local bob = named(world.entity, "Bob")
|
||||||
|
world:set(bob, pair(Eats, Apples), 10)
|
||||||
|
|
||||||
|
local alice = named(world.entity, "Alice")
|
||||||
|
world:set(alice, pair(Eats, Oranges), 5)
|
||||||
|
|
||||||
|
-- Aliasing the wildcard to symbols improves readability and ease of writing
|
||||||
|
local __ = jecs.Wildcard
|
||||||
|
|
||||||
|
-- Create a query that matches edible components
|
||||||
|
for entity, amount in world:query(pair(Eats, __)) do
|
||||||
|
-- Iterate the query
|
||||||
|
local food = world:target(entity, Eats)
|
||||||
|
print(`{name(entity)} eats {amount} {name(food)}`)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Output:
|
||||||
|
-- Alice eats 5 Oranges
|
||||||
|
-- Bob eats 10 Apples
|
|
@ -117,6 +117,22 @@ local function name(world, e)
|
||||||
return world:get(e, jecs.Name)
|
return world:get(e, jecs.Name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
TEST("#adding a recycled target", function()
|
||||||
|
local world = world_new()
|
||||||
|
local R = world:component()
|
||||||
|
|
||||||
|
local e = world:entity()
|
||||||
|
local T = world:entity()
|
||||||
|
world:add(e, pair(R, T))
|
||||||
|
world:delete(T)
|
||||||
|
CHECK(not world:has(e, pair(R, T)))
|
||||||
|
local T2 = world:entity()
|
||||||
|
world:add(e, pair(R, T2))
|
||||||
|
CHECK(world:target(e, R) ~= T)
|
||||||
|
CHECK(world:target(e, R) ~= 0)
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
TEST("#repro2", function()
|
TEST("#repro2", function()
|
||||||
local world = world_new()
|
local world = world_new()
|
||||||
local Lifetime = world:component() :: jecs.Id<number>
|
local Lifetime = world:component() :: jecs.Id<number>
|
||||||
|
|
BIN
test/thesis/images/archetype_graph.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
test/thesis/images/chrome_IdcpbCveiD.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
test/thesis/images/chrome_f5DTavXIka.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
test/thesis/images/chrome_giChmd5W4Z.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
test/thesis/images/insertion.png
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
test/thesis/images/queries.png
Normal file
After Width: | Height: | Size: 201 KiB |
BIN
test/thesis/images/random_access.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
test/thesis/images/removed.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
test/thesis/images/sparseset.png
Normal file
After Width: | Height: | Size: 9.5 KiB |