Add docs (#45)

* Initial commit

* Add section for standalone

* Fix docs

* Add pages to docs

* Remove redundant files
This commit is contained in:
Marcus 2024-05-27 03:39:20 +02:00 committed by GitHub
parent f1ba9c4a55
commit 0567856a59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 665 additions and 412 deletions

View file

@ -31,7 +31,7 @@ local Name = world:component()
local function parent(entity)
return world:target(entity, ChildOf)
end
local function name(entity)
local function getName(entity)
return world:get(entity, Name)
end

45
docs/api-types.md Normal file
View file

@ -0,0 +1,45 @@
# World
A World contains all ECS data
Games can have multiple worlds, although typically only one is necessary. These worlds are isolated from each other, meaning they donot share the same entities nor component IDs.
---
# Entity
An unique id.
Entities consist out of a number unique to the entity in the lower 32 bits, and a counter used to track entity liveliness in the upper 32 bits. When an id is recycled, its generation count is increased. This causes recycled ids to be very large (>4 billion), which is normal.
---
# QueryIter
A result from the `World:query` function.
Queries are used to iterate over entities that match against the set collection of components.
Calling it in a loop will allow iteration over the results.
```lua
for id, enemy, charge, model in world:query(Enemy, Charge, Model) do
-- Do something
end
```
### QueryIter.without
QueryIter.without(iter: QueryIter
...: [Entity](../api-types/Entity)): QueryIter
Create a new Query Iterator from the filter
#### Parameters
world The world.
... The collection of components to filter archetypes against.
#### Returns
The new query iterator.

View file

@ -0,0 +1,19 @@
# Getting Started
This section will provide a walk through setting up your development environment and a quick overview of the different features and concepts in Jecs with short examples.
## Installing Jecs
To use Jecs, you will need to add the library to your project's source folder.
## Installing as standalone
Head over to the [Releases](https://github.com/ukendio/jecs/releases/latest) page and install the rbxm file.
![jecs.rbxm](rbxm.png)
## Installing with Wally
Jecs is available as a package on [wally.run](https://wally.run/package/ukendio/jecs)
Add it to your project's Wally.toml like this:
```toml
[dependencies]
jecs = "0.1.0" # Make sure this is the latest version
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -6,10 +6,10 @@
type i53 = number
type i24 = number
type Ty = {i53}
type Ty = { i53 }
type ArchetypeId = number
type Column = {any}
type Column = { any }
type Archetype = {
id: number,
@ -21,20 +21,19 @@ type Archetype = {
},
types: Ty,
type: string | number,
entities: {number},
columns: {Column},
entities: { number },
columns: { Column },
records: {},
}
type Record = {
archetype: Archetype,
row: number,
dense: i24,
componentRecord: ArchetypeMap
componentRecord: ArchetypeMap,
}
type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}}
type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } }
type ArchetypeRecord = number
--[[
@ -48,16 +47,16 @@ TODO:
]]
type ArchetypeMap = {
cache: {[number]: ArchetypeRecord},
cache: { [number]: ArchetypeRecord },
first: ArchetypeMap,
second: ArchetypeMap,
parent: ArchetypeMap,
size: number
size: number,
}
type ComponentIndex = {[i24]: ArchetypeMap}
type ComponentIndex = { [i24]: ArchetypeMap }
type Archetypes = {[ArchetypeId]: Archetype}
type Archetypes = { [ArchetypeId]: Archetype }
type ArchetypeDiff = {
added: Ty,
@ -70,114 +69,102 @@ local ON_ADD = HI_COMPONENT_ID + 1
local ON_REMOVE = HI_COMPONENT_ID + 2
local ON_SET = HI_COMPONENT_ID + 3
local WILDCARD = HI_COMPONENT_ID + 4
local REST = HI_COMPONENT_ID + 5
local REST = HI_COMPONENT_ID + 5
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local function addFlags(isPair: boolean)
local typeFlags = 0x0
local function addFlags(isPair: boolean)
local typeFlags = 0x0
if isPair then
typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID.
end
if isPair then
typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID.
end
if false then
typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true
end
if false then
typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true
end
if false then
typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID.
end
typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true
end
if false then
typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true
end
if false then
typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID.
end
return typeFlags
return typeFlags
end
local function ECS_COMBINE(source: number, target: number): i53
local e = source * 2^28 + target * ECS_ID_FLAGS_MASK
return e
local e = source * 268435456 + target * ECS_ID_FLAGS_MASK
return e
end
local function ECS_IS_PAIR(e: number)
return (e % 2^4) // FLAGS_PAIR ~= 0
end
function separate(entity: number)
local _typeFlags = entity % 0x10
entity //= ECS_ID_FLAGS_MASK
return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags
local function ECS_IS_PAIR(e: number)
return (e % 2 ^ 4) // FLAGS_PAIR ~= 0
end
-- HIGH 24 bits LOW 24 bits
local function ECS_GENERATION(e: i53)
e //= 0x10
return e % ECS_GENERATION_MASK
end
-- SECOND
local function ECS_ENTITY_T_LO(e: i53)
e //= 0x10
return e // ECS_ENTITY_MASK
e = e // 0x10
return e % ECS_GENERATION_MASK
end
local function ECS_GENERATION_INC(e: i53)
local id, generation, flags = separate(e)
local flags = e // 0x10
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK
return ECS_COMBINE(id, generation + 1) + flags
return ECS_COMBINE(id, generation + 1) + flags
end
-- FIRST gets the high ID
local function ECS_ENTITY_T_HI(entity: i53): i24
entity //= 0x10
local first = entity % ECS_ENTITY_MASK
return first
local function ECS_ENTITY_T_HI(e: i53): i24
e = e // 0x10
return e % ECS_ENTITY_MASK
end
local function ECS_PAIR(pred: number, obj: number)
local first
-- SECOND
local function ECS_ENTITY_T_LO(e: i53)
e = e // 0x10
return e // ECS_ENTITY_MASK
end
local function ECS_PAIR(pred: i53, obj: i53): i53
local first
local second: number = WILDCARD
if pred == WILDCARD then
if pred == WILDCARD then
first = obj
elseif obj == WILDCARD then
first = pred
else
first = obj
first = obj
second = ECS_ENTITY_T_LO(pred)
end
return ECS_COMBINE(
ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true)
end
return ECS_COMBINE(ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true)
end
local function getAlive(entityIndex: EntityIndex, id: i24)
local function getAlive(entityIndex: EntityIndex, id: i24)
local entityId = entityIndex.dense[id]
local record = entityIndex.sparse[entityIndex.dense[id]]
if not record then
error(id.." is not alive")
end
return entityId
return entityId
end
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
local function ECS_PAIR_RELATION(entityIndex, e)
assert(ECS_IS_PAIR(e))
return getAlive(entityIndex, ECS_ENTITY_T_HI(e))
local function ECS_PAIR_RELATION(entityIndex, e)
return getAlive(entityIndex, ECS_ENTITY_T_HI(e))
end
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
local function ECS_PAIR_OBJECT(entityIndex, e)
assert(ECS_IS_PAIR(e))
return getAlive(entityIndex, ECS_ENTITY_T_LO(e))
local function ECS_PAIR_OBJECT(entityIndex, e)
return getAlive(entityIndex, ECS_ENTITY_T_LO(e))
end
local function nextEntityId(entityIndex, index: i24): i53
local id = ECS_COMBINE(index, 0)
entityIndex.sparse[id] = {
dense = index
} :: Record
dense = index,
} :: Record
entityIndex.dense[index] = id
return id
@ -224,7 +211,7 @@ local function transitionArchetype(
local e1 = sourceEntities[sourceRow]
local e2 = sourceEntities[movedAway]
if sourceRow ~= movedAway then
if sourceRow ~= movedAway then
sourceEntities[sourceRow] = e2
end
@ -252,7 +239,7 @@ local function newEntity(entityId: i53, record: Record, archetype: Archetype)
return record
end
local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype)
local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Record, to: Archetype)
local sourceRow = record.row
local from = record.archetype
local destinationRow = archetypeAppend(entityId, to)
@ -265,11 +252,16 @@ local function hash(arr): string | number
return table.concat(arr, "_")
end
local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap
local function ensureComponentRecord(
componentIndex: ComponentIndex,
archetypeId: number,
componentId: number,
i: number
): ArchetypeMap
local archetypesMap = componentIndex[componentId]
if not archetypesMap then
archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap
archetypesMap = { size = 0, cache = {}, first = {}, second = {} } :: ArchetypeMap
componentIndex[componentId] = archetypesMap
end
@ -279,15 +271,14 @@ local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId
return archetypesMap
end
local function ECS_ID_IS_WILDCARD(e)
local function ECS_ID_IS_WILDCARD(e)
assert(ECS_IS_PAIR(e))
local first = ECS_ENTITY_T_HI(e)
local second = ECS_ENTITY_T_LO(e)
return first == WILDCARD or second == WILDCARD
end
local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype
local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype
local ty = hash(types)
local id = world.nextArchetypeId + 1
@ -301,31 +292,29 @@ local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetyp
for i, componentId in types do
ensureComponentRecord(componentIndex, id, componentId, i)
records[componentId] = i
if ECS_IS_PAIR(componentId) then
if ECS_IS_PAIR(componentId) then
local relation = ECS_PAIR_RELATION(world.entityIndex, componentId)
local object = ECS_PAIR_OBJECT(world.entityIndex, componentId)
local idr_r = ECS_PAIR(relation, WILDCARD)
ensureComponentRecord(
componentIndex, id, idr_r, i)
ensureComponentRecord(componentIndex, id, idr_r, i)
records[idr_r] = i
local idr_t = ECS_PAIR(WILDCARD, object)
ensureComponentRecord(
componentIndex, id, idr_t, i)
ensureComponentRecord(componentIndex, id, idr_t, i)
records[idr_t] = i
end
columns[i] = {}
end
local archetype = {
columns = columns;
edges = {};
entities = {};
id = id;
records = records;
type = ty;
types = types;
columns = columns,
edges = {},
entities = {},
id = id,
records = records,
type = ty,
types = types,
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
@ -337,20 +326,20 @@ local World = {}
World.__index = World
function World.new()
local self = setmetatable({
archetypeIndex = {};
archetypes = {} :: Archetypes;
componentIndex = {} :: ComponentIndex;
archetypeIndex = {},
archetypes = {} :: Archetypes,
componentIndex = {} :: ComponentIndex,
entityIndex = {
dense = {},
sparse = {}
} :: EntityIndex;
sparse = {},
} :: EntityIndex,
hooks = {
[ON_ADD] = {};
};
nextArchetypeId = 0;
nextComponentId = 0;
nextEntityId = 0;
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
[ON_ADD] = {},
},
nextArchetypeId = 0,
nextComponentId = 0,
nextEntityId = 0,
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
}, World)
self.ROOT_ARCHETYPE = archetypeOf(self, {})
return self
@ -380,16 +369,16 @@ function World.target(world: World, entity: i53, relation: i24): i24?
local entityIndex = world.entityIndex
local record = entityIndex.sparse[entity]
local archetype = record.archetype
if not archetype then
if not archetype then
return nil
end
local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)]
if not componentRecord then
if not componentRecord then
return nil
end
local archetypeRecord = componentRecord.cache[archetype.id]
if not archetypeRecord then
if not archetypeRecord then
return nil
end
@ -397,37 +386,37 @@ function World.target(world: World, entity: i53, relation: i24): i24?
end
-- should reuse this logic in World.set instead of swap removing in transition archetype
local function destructColumns(columns, count, row)
if row == count then
for _, column in columns do
local function destructColumns(columns, count, row)
if row == count then
for _, column in columns do
column[count] = nil
end
else
for _, column in columns do
for _, column in columns do
column[row] = column[count]
column[count] = nil
end
end
end
local function archetypeDelete(world: World, id: i53)
local componentIndex = world.componentIndex
local function archetypeDelete(world: World, id: i53)
local componentIndex = world.componentIndex
local archetypesMap = componentIndex[id]
local archetypes = world.archetypes
if archetypesMap then
for archetypeId in archetypesMap.cache do
for _, entity in archetypes[archetypeId].entities do
if archetypesMap then
for archetypeId in archetypesMap.cache do
for _, entity in archetypes[archetypeId].entities do
world:remove(entity, id)
end
end
componentIndex[id] = nil
end
end
function World.delete(world: World, entityId: i53)
function World.delete(world: World, entityId: i53)
local record = world.entityIndex.sparse[entityId]
if not record then
if not record then
return
end
local entityIndex = world.entityIndex
@ -439,12 +428,12 @@ function World.delete(world: World, entityId: i53)
-- TODO: should traverse linked )component records to pairs including entityId
archetypeDelete(world, ECS_PAIR(entityId, WILDCARD))
archetypeDelete(world, ECS_PAIR(WILDCARD, entityId))
if archetype then
if archetype then
local entities = archetype.entities
local last = #entities
if row ~= last then
if row ~= last then
local entityToMove = entities[last]
dense[record.dense] = entityToMove
sparse[entityToMove] = record
@ -477,7 +466,7 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev)
end
local function findInsert(types: {i53}, toAdd: i53)
local function findInsert(types: { i53 }, toAdd: i53)
for i, id in types do
if id == toAdd then
return -1
@ -494,7 +483,7 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
-- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
local destinationType = table.clone(node.types)
local at = findInsert(types, componentId)
if at == -1 then
@ -532,7 +521,7 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
return add
end
function World.add(world: World, entityId: i53, componentId: i53)
function World.add(world: World, entityId: i53, componentId: i53)
local entityIndex = world.entityIndex
local record = entityIndex.sparse[entityId]
local from = record.archetype
@ -582,7 +571,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc
if not remove then
local to = table.clone(from.types)
local at = table.find(to, componentId)
if not at then
if not at then
return from
end
table.remove(to, at)
@ -607,7 +596,7 @@ end
-- Keeping the function as small as possible to enable inlining
local function get(record: Record, componentId: i24)
local archetype = record.archetype
if not archetype then
if not archetype then
return nil
end
@ -644,20 +633,20 @@ end
-- the less creation the better
local function actualNoOperation() end
local function noop(_self: Query, ...: i53): () -> (number, ...any)
local function noop(_self: Query, ...): () -> ()
return actualNoOperation :: any
end
local EmptyQuery = {
__iter = noop;
without = noop;
__iter = noop,
without = noop,
}
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
function World.query(world: World, ...): Query
-- breaking?
if (...) == nil then
error("Missing components")
@ -666,7 +655,7 @@ function World.query(world: World, ...: i53): Query
local compatibleArchetypes = {}
local length = 0
local components = {...}
local components = { ... }
local archetypes = world.archetypes
local queryLength = #components
@ -707,8 +696,8 @@ function World.query(world: World, ...: i53): Query
length += 1
compatibleArchetypes[length] = {
archetype = archetype,
indices = indices
archetype = archetype,
indices = indices,
}
end
@ -721,7 +710,7 @@ function World.query(world: World, ...: i53): Query
preparedQuery.__index = preparedQuery
function preparedQuery:without(...)
local withoutComponents = {...}
local withoutComponents = { ... }
for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i].archetype
local records = archetype.records
@ -828,16 +817,16 @@ function World.__iter(world: World): () -> (number?, unknown?)
local sparse = world.entityIndex.sparse
local last
return function()
return function()
local lastEntity, entityId = next(dense, last)
if not lastEntity then
if not lastEntity then
return
end
last = lastEntity
local record = sparse[entityId]
local archetype = record.archetype
if not archetype then
if not archetype then
-- Returns only the entity id as an entity without data should not return
-- data and allow the user to get an error if they don't handle the case.
return entityId
@ -851,17 +840,17 @@ function World.__iter(world: World): () -> (number?, unknown?)
-- We use types because the key should be the component ID not the column index
entityData[types[i]] = column[row]
end
return entityId, entityData
end
end
return table.freeze({
World = World;
World = World,
OnAdd = ON_ADD;
OnRemove = ON_REMOVE;
OnSet = ON_SET;
OnAdd = ON_ADD,
OnRemove = ON_REMOVE,
OnSet = ON_SET,
Wildcard = WILDCARD,
w = WILDCARD,
Rest = REST,

186
mkdocs.yml Normal file
View file

@ -0,0 +1,186 @@
site_name: Fusion
site_url: https://elttob.uk/Fusion/
repo_name: dphfox/Fusion
repo_url: https://github.com/dphfox/Fusion
extra:
version:
provider: mike
theme:
name: material
custom_dir: docs/assets/overrides
logo: assets/logo
favicon: assets/logo-dark.svg
palette:
- media: "(prefers-color-scheme: dark)"
scheme: fusiondoc-dark
toggle:
icon: octicons/sun-24
title: Switch to light theme
- media: "(prefers-color-scheme: light)"
scheme: fusiondoc-light
toggle:
icon: octicons/moon-24
title: Switch to dark theme
font:
text: Plus Jakarta Sans
code: JetBrains Mono
features:
- navigation.tabs
- navigation.top
- navigation.sections
- navigation.instant
- navigation.indexes
- search.suggest
- search.highlight
icon:
repo: octicons/mark-github-16
extra_css:
- assets/theme/fusiondoc.css
- assets/theme/colours.css
- assets/theme/code.css
- assets/theme/paragraph.css
- assets/theme/page.css
- assets/theme/admonition.css
- assets/theme/404.css
- assets/theme/api-reference.css
- assets/theme/dev-tools.css
extra_javascript:
- assets/scripts/smooth-scroll.js
nav:
- Home: index.md
- Tutorials:
- Get Started: tutorials/index.md
- Installing Fusion: tutorials/get-started/installing-fusion.md
- Developer Tools: tutorials/get-started/developer-tools.md
- Getting Help: tutorials/get-started/getting-help.md
- Fundamentals:
- Scopes: tutorials/fundamentals/scopes.md
- Values: tutorials/fundamentals/values.md
- Observers: tutorials/fundamentals/observers.md
- Computeds: tutorials/fundamentals/computeds.md
- Tables:
- ForValues: tutorials/tables/forvalues.md
- ForKeys: tutorials/tables/forkeys.md
- ForPairs: tutorials/tables/forpairs.md
- Animation:
- Tweens: tutorials/animation/tweens.md
- Springs: tutorials/animation/springs.md
- Roblox:
- Hydration: tutorials/roblox/hydration.md
- New Instances: tutorials/roblox/new-instances.md
- Parenting: tutorials/roblox/parenting.md
- Events: tutorials/roblox/events.md
- Change Events: tutorials/roblox/change-events.md
- Outputs: tutorials/roblox/outputs.md
- References: tutorials/roblox/references.md
- Best Practices:
- Components: tutorials/best-practices/components.md
- Instance Handling: tutorials/best-practices/instance-handling.md
- Callbacks: tutorials/best-practices/callbacks.md
- State: tutorials/best-practices/state.md
- Sharing Values: tutorials/best-practices/sharing-values.md
- Error Safety: tutorials/best-practices/error-safety.md
- Optimisation: tutorials/best-practices/optimisation.md
- Examples:
- Home: examples/index.md
- Cookbook:
- examples/cookbook/index.md
- Player List: examples/cookbook/player-list.md
- Animated Computed: examples/cookbook/animated-computed.md
- Fetch Data From Server: examples/cookbook/fetch-data-from-server.md
- Light & Dark Theme: examples/cookbook/light-and-dark-theme.md
- Button Component: examples/cookbook/button-component.md
- Loading Spinner: examples/cookbook/loading-spinner.md
- Drag & Drop: examples/cookbook/drag-and-drop.md
- API Reference:
- api-reference/index.md
- General:
- Errors: api-reference/general/errors.md
- Types:
- Contextual: api-reference/general/types/contextual.md
- Version: api-reference/general/types/version.md
- Members:
- Contextual: api-reference/general/members/contextual.md
- Safe: api-reference/general/members/safe.md
- version: api-reference/general/members/version.md
- Memory:
- Types:
- Scope: api-reference/memory/types/scope.md
- ScopedObject: api-reference/memory/types/scopedobject.md
- Task: api-reference/memory/types/task.md
- Members:
- deriveScope: api-reference/memory/members/derivescope.md
- doCleanup: api-reference/memory/members/docleanup.md
- scoped: api-reference/memory/members/scoped.md
- State:
- Types:
- UsedAs: api-reference/state/types/usedas.md
- Computed: api-reference/state/types/computed.md
- Dependency: api-reference/state/types/dependency.md
- Dependent: api-reference/state/types/dependent.md
- For: api-reference/state/types/for.md
- Observer: api-reference/state/types/observer.md
- StateObject: api-reference/state/types/stateobject.md
- Use: api-reference/state/types/use.md
- Value: api-reference/state/types/value.md
- Members:
- Computed: api-reference/state/members/computed.md
- ForKeys: api-reference/state/members/forkeys.md
- ForPairs: api-reference/state/members/forpairs.md
- ForValues: api-reference/state/members/forvalues.md
- Observer: api-reference/state/members/observer.md
- peek: api-reference/state/members/peek.md
- Value: api-reference/state/members/value.md
- Roblox:
- Types:
- Child: api-reference/roblox/types/child.md
- PropertyTable: api-reference/roblox/types/propertytable.md
- SpecialKey: api-reference/roblox/types/specialkey.md
- Members:
- Attribute: api-reference/roblox/members/attribute.md
- AttributeChange: api-reference/roblox/members/attributechange.md
- AttributeOut: api-reference/roblox/members/attributeout.md
- Children: api-reference/roblox/members/children.md
- Hydrate: api-reference/roblox/members/hydrate.md
- New: api-reference/roblox/members/new.md
- OnChange: api-reference/roblox/members/onchange.md
- OnEvent: api-reference/roblox/members/onevent.md
- Out: api-reference/roblox/members/out.md
- Ref: api-reference/roblox/members/ref.md
- Animation:
- Types:
- Animatable: api-reference/animation/types/animatable.md
- Spring: api-reference/animation/types/spring.md
- Tween: api-reference/animation/types/tween.md
- Members:
- Tween: api-reference/animation/members/tween.md
- Spring: api-reference/animation/members/spring.md
- Extras:
- Home: extras/index.md
- Backgrounds: extras/backgrounds.md
- Brand Guidelines: extras/brand-guidelines.md
markdown_extensions:
- admonition
- attr_list
- meta
- md_in_html
- pymdownx.superfences
- pymdownx.betterem
- pymdownx.details
- pymdownx.tabbed:
alternate_style: true
- pymdownx.inlinehilite
- toc:
permalink: true
- pymdownx.highlight:
guess_lang: false
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg

View file

@ -1,5 +1,5 @@
local testkit = require("../testkit")
local jecs = require("../lib/init")
local testkit = require("../testkit")
local __ = jecs.Wildcard
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
@ -11,329 +11,343 @@ local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
local ok, err: string? = pcall(fn, ...)
local ok, err: string? = pcall(fn, ...)
if not CHECK(not ok, 2) then
local i = string.find(err :: string, " ")
assert(i)
local msg = string.sub(err :: string, i+1)
CHECK(msg == s, 2)
end
if not CHECK(not ok, 2) then
local i = string.find(err :: string, " ")
assert(i)
local msg = string.sub(err :: string, i + 1)
CHECK(msg == s, 2)
end
end
local N = 10
TEST("world", function()
do CASE "should be iterable"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local eA = world:entity()
world:set(eA, A, true)
local eB = world:entity()
world:set(eB, B, true)
local eAB = world:entity()
world:set(eAB, A, true)
world:set(eAB, B, true)
TEST("world", function()
do
CASE("should be iterable")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local eA = world:entity()
world:set(eA, A, true)
local eB = world:entity()
world:set(eB, B, true)
local eAB = world:entity()
world:set(eAB, A, true)
world:set(eAB, B, true)
local count = 0
for id, data in world do
count += 1
if id == eA then
CHECK(data[A] == true)
CHECK(data[B] == nil)
elseif id == eB then
CHECK(data[A] == nil)
CHECK(data[B] == true)
elseif id == eAB then
CHECK(data[A] == true)
CHECK(data[B] == true)
end
end
local count = 0
for id, data in world do
count += 1
if id == eA then
CHECK(data[A] == true)
CHECK(data[B] == nil)
elseif id == eB then
CHECK(data[A] == nil)
CHECK(data[B] == true)
elseif id == eAB then
CHECK(data[A] == true)
CHECK(data[B] == true)
end
end
-- components are registered in the entity index as well
-- so this test has to add 2 to account for them
CHECK(count == 3 + 2)
end
-- components are registered in the entity index as well
-- so this test has to add 2 to account for them
CHECK(count == 3 + 2)
end
do CASE "should query all matching entities"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
do
CASE("should query all matching entities")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i > 5 then world:set(id, B, true) end
entities[i] = id
end
world:set(id, A, true)
if i > 5 then
world:set(id, B, true)
end
entities[i] = id
end
for id in world:query(A) do
table.remove(entities, CHECK(table.find(entities, id)))
end
for id in world:query(A) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
CHECK(#entities == 0)
end
end
do
CASE("should query all matching entities when irrelevant component is removed")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
do CASE "should query all matching entities when irrelevant component is removed"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
local entities = {}
for i = 1, N do
local id = world:entity()
-- specifically put them in disorder to track regression
-- https://github.com/Ukendio/jecs/pull/15
world:set(id, B, true)
world:set(id, A, true)
if i > 5 then
world:remove(id, B)
end
entities[i] = id
end
-- specifically put them in disorder to track regression
-- https://github.com/Ukendio/jecs/pull/15
world:set(id, B, true)
world:set(id, A, true)
if i > 5 then world:remove(id, B) end
entities[i] = id
end
local added = 0
for id in world:query(A) do
added += 1
table.remove(entities, CHECK(table.find(entities, id)))
end
local added = 0
for id in world:query(A) do
added += 1
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(added == N)
end
CHECK(added == N)
end
do
CASE("should query all entities without B")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
do CASE "should query all entities without B"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local entities = {}
for i = 1, N do
local id = world:entity()
local entities = {}
for i = 1, N do
local id = world:entity()
world:set(id, A, true)
if i < 5 then
entities[i] = id
else
world:set(id, B, true)
end
end
world:set(id, A, true)
if i < 5 then
entities[i] = id
else
world:set(id, B, true)
end
end
for id in world:query(A):without(B) do
table.remove(entities, CHECK(table.find(entities, id)))
end
for id in world:query(A):without(B) do
table.remove(entities, CHECK(table.find(entities, id)))
end
CHECK(#entities == 0)
end
CHECK(#entities == 0)
do
CASE("should allow setting components in arbitrary order")
local world = jecs.World.new()
end
local Health = world:entity()
local Poison = world:component()
do CASE "should allow setting components in arbitrary order"
local world = jecs.World.new()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
local Health = world:entity()
local Poison = world:component()
CHECK(world:get(id, Poison) == 5)
end
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
do
CASE("should allow deleting components")
local world = jecs.World.new()
CHECK(world:get(id, Poison) == 5)
end
local Health = world:entity()
local Poison = world:component()
do CASE "should allow deleting components"
local world = jecs.World.new()
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
local id1 = world:entity()
world:set(id1, Poison, 500)
world:set(id1, Health, 50)
local Health = world:entity()
local Poison = world:component()
world:delete(id)
local id = world:entity()
world:set(id, Poison, 5)
world:set(id, Health, 50)
local id1 = world:entity()
world:set(id1, Poison, 500)
world:set(id1, Health, 50)
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == nil)
CHECK(world:get(id1, Poison) == 500)
CHECK(world:get(id1, Health) == 50)
end
world:delete(id)
do
CASE("should allow remove that doesn't exist on entity")
local world = jecs.World.new()
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == nil)
CHECK(world:get(id1, Poison) == 500)
CHECK(world:get(id1, Health) == 50)
local Health = world:entity()
local Poison = world:component()
end
local id = world:entity()
world:set(id, Health, 50)
world:remove(id, Poison)
do CASE "should allow remove that doesn't exist on entity"
local world = jecs.World.new()
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
local Health = world:entity()
local Poison = world:component()
do
CASE("should increment generation")
local world = jecs.World.new()
local e = world:entity()
CHECK(ECS_ID(e) == 1 + jecs.Rest)
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
local id = world:entity()
world:set(id, Health, 50)
world:remove(id, Poison)
do
CASE("should get alive from index in the dense array")
local world = jecs.World.new()
local _e = world:entity()
local e2 = world:entity()
local e3 = world:entity()
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
CHECK(IS_PAIR(world:entity()) == false)
do CASE "should increment generation"
local world = jecs.World.new()
local e = world:entity()
CHECK(ECS_ID(e) == 1 + jecs.Rest)
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e)
CHECK(ECS_GENERATION(e) == 1) -- 1
end
local pair = ECS_PAIR(e2, e3)
CHECK(IS_PAIR(pair) == true)
CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2)
CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3)
end
do CASE "should get alive from index in the dense array"
local world = jecs.World.new()
local _e = world:entity()
local e2 = world:entity()
local e3 = world:entity()
do
CASE("should allow querying for relations")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
CHECK(IS_PAIR(world:entity()) == false)
world:set(bob, ECS_PAIR(Eats, Apples), true)
for e, bool in world:query(ECS_PAIR(Eats, Apples)) do
CHECK(e == bob)
CHECK(bool)
end
end
local pair = ECS_PAIR(e2, e3)
CHECK(IS_PAIR(pair) == true)
CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2)
CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3)
end
do
CASE("should allow wildcards in queries")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
do CASE "should allow querying for relations"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), true)
for e, bool in world:query(ECS_PAIR(Eats, Apples)) do
CHECK(e == bob)
CHECK(bool)
end
end
do CASE "should allow wildcards in queries"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
local w = jecs.Wildcard
for e, data in world:query(ECS_PAIR(Eats, w)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
for e, data in world:query(ECS_PAIR(w, Apples)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
end
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
do CASE "should match against multiple pairs"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local Oranges =world:entity()
local bob = world:entity()
local alice = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
local w = jecs.Wildcard
local count = 0
for e, data in world:query(ECS_PAIR(Eats, w)) do
count += 1
if e == bob then
CHECK(data == "bob eats apples")
else
CHECK(data == "alice eats oranges")
end
end
local w = jecs.Wildcard
for e, data in world:query(ECS_PAIR(Eats, w)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
for e, data in world:query(ECS_PAIR(w, Apples)) do
CHECK(e == bob)
CHECK(data == "bob eats apples")
end
end
CHECK(count == 2)
count = 0
do
CASE("should match against multiple pairs")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
for e, data in world:query(ECS_PAIR(w, Apples)) do
count += 1
CHECK(data == "bob eats apples")
end
CHECK(count == 1)
end
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
do CASE "should only relate alive entities"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
world:set(bob, Apples, "apples")
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
local w = jecs.Wildcard
local count = 0
for e, data in world:query(ECS_PAIR(Eats, w)) do
count += 1
if e == bob then
CHECK(data == "bob eats apples")
else
CHECK(data == "alice eats oranges")
end
end
world:delete(Apples)
local Wildcard = jecs.Wildcard
local count = 0
for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do
count += 1
end
CHECK(count == 2)
count = 0
world:delete(ECS_PAIR(Eats, Apples))
CHECK(count == 0)
CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil)
end
for e, data in world:query(ECS_PAIR(w, Apples)) do
count += 1
CHECK(data == "bob eats apples")
end
CHECK(count == 1)
end
do CASE "should error when setting invalid pair"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
do
CASE("should only relate alive entities")
world:delete(Apples)
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local Oranges = world:entity()
local bob = world:entity()
local alice = world:entity()
CHECK_NO_ERR("Apples should be dead", function()
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
end)
end
world:set(bob, Apples, "apples")
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
do CASE "should find target for ChildOf"
local world = jecs.World.new()
world:delete(Apples)
local Wildcard = jecs.Wildcard
local ChildOf = world:component()
local Name = world:component()
local count = 0
for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do
count += 1
end
local function parent(entity)
return world:target(entity, ChildOf)
end
world:delete(ECS_PAIR(Eats, Apples))
local bob = world:entity()
local alice = world:entity()
local sara = world:entity()
world:add(bob, ECS_PAIR(ChildOf, alice))
world:set(bob, Name, "bob")
world:add(sara, ECS_PAIR(ChildOf, alice))
world:set(sara, Name, "sara")
CHECK(parent(bob) == alice) -- O(1)
CHECK(count == 0)
CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil)
end
local count = 0
for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do
print(name)
count += 1
end
CHECK(count == 2)
end
do
CASE("should error when setting invalid pair")
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:delete(Apples)
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
end
do
CASE("should find target for ChildOf")
local world = jecs.World.new()
local ChildOf = world:component()
local Name = world:component()
local function parent(entity)
return world:target(entity, ChildOf)
end
local bob = world:entity()
local alice = world:entity()
local sara = world:entity()
world:add(bob, ECS_PAIR(ChildOf, alice))
world:set(bob, Name, "bob")
world:add(sara, ECS_PAIR(ChildOf, alice))
world:set(sara, Name, "sara")
CHECK(parent(bob) == alice) -- O(1)
local count = 0
for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do
print(name)
count += 1
end
CHECK(count == 2)
end
end)
FINISH()
FINISH()