Merge branch 'main' into Add-queries-with-tags

This commit is contained in:
Marcus 2024-07-26 16:18:20 +02:00 committed by GitHub
commit cc33c4d038
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 460 additions and 237 deletions

4
.gitignore vendored
View file

@ -53,7 +53,7 @@ WallyPatches
# Misc # Misc
roblox.toml roblox.toml
sourcemap.json sourcemap.json
drafts/*.lua drafts/
# Cached Vitepress (docs) # Cached Vitepress (docs)
@ -61,4 +61,4 @@ drafts/*.lua
/docs/.vitepress/dist /docs/.vitepress/dist
.vitepress/cache .vitepress/cache
.vitepress/dist .vitepress/dist

View file

@ -1,59 +0,0 @@
# API
## World
### World.new() -> `World`
Creates a new world.
Example:
::: code-group
```luau [luau]
local world = jecs.World.new()
```
```ts [typescript]
import { World } from "@rbxts/jecs";
const world = new World();
```
:::
### world:entity() -> `Entity<T>`
Creates a new entity.
Example:
::: code-group
```luau [luau]
local entity = world:entity()
```
```ts [typescript]
const entity = world.entity();
```
:::
### world:component() -> `Entity<T>`
Creates a new static component. Keep in mind that components are also entities.
Example:
::: code-group
```luau [luau]
local Health = world:component()
```
```ts [typescript]
const Health = world.component<number>();
```
:::
::: info
You should use this when creating static components.
For example, a generic Health entity should be created using this.
:::

132
docs/api/query.md Normal file
View file

@ -0,0 +1,132 @@
# Query
A World contains entities which have components. The World is queryable and can be used to get entities with a specific set of components.
## Functions
### new()
```luau
function World.new(): World
```
Creates a new world.
Example:
::: code-group
```luau [luau]
local world = jecs.World.new()
```
```ts [typescript]
import { World } from "@rbxts/jecs";
const world = new World();
```
:::
## entity()
```luau
function World:entity(): Entity -- The new entit.
```
Creates a new entity.
Example:
::: code-group
```luau [luau]
local entity = world:entity()
```
```ts [typescript]
const entity = world.entity();
```
::
:
### component()
```luau
function World:component<T>(): Entity<T> -- The new componen.
```
Creates a new component.
Example:
::: code-group
```luau [luau]
local Health = world:component() :: jecs.Entity<number>
```
```ts [typescript]
const Health = world.component<number>();
```
:::
::: info
You should use this when creating components.
For example, a Health type should be created using this.
:::
### get()
```luau
function World:get(
entity: Entity, -- The entity
...: Entity<T> -- The types to fetch
): ... -- Returns the component data in the same order they were passed in
```
Returns the data for each provided type for the corresponding entity.
:::
### add()
```luau
function World:add(
entity: Entity, -- The entity
id: Entity<T> -- The component ID to add
): ()
```
Adds a component ID to the entity.
This operation adds a single (component) id to an entity.
::: info
This function is idempotent, meaning if the entity already has the id, this operation will have no side effects.
:::
### set()
```luau
function World:set(
entity: Entity, -- The entity
id: Entity<T>, -- The component ID to set
data: T -- The data of the component's type
): ()
```
Adds or changes the entity's component.
### query()
```luau
function World:query(
...: Entity<T> -- The component IDs to query with. Entities that satifies the conditions will be returned
): Query<...Entity<T>> -- Returns the Query which gets the entity and their corresponding data when iterated
```
Creates a [`query`](query) with the given component IDs.
Example:
::: code-group
```luau [luau]
for id, position, velocity in world:query(Position, Velocity) do
-- Do something
end
```
```ts [typescript]
for (const [id, position, velocity] of world.query(Position, Velocity) {
// Do something
}
```
:::

70
docs/api/world.md Normal file
View file

@ -0,0 +1,70 @@
# World
A World contains entities which have components. The World is queryable and can be used to get entities with a specific set of components.
## Functions
### new()
```lua
function World.new(): World
```
Creates a new world.
Example:
::: code-group
```luau [luau]
local world = jecs.World.new()
```
```ts [typescript]
import { World } from "@rbxts/jecs";
const world = new World();
```
:::
## entity()
```luau
function World:entity(): Entity
```
Creates a new entity.
Example:
::: code-group
```luau [luau]
local entity = world:entity()
```
```ts [typescript]
const entity = world.entity();
```
:::
### component()`
```luau
function World:component<T>(): Entity<T>
```
Creates a new component.
Example:
::: code-group
```luau [luau]
local Health = world:component() :: jecs.Entity<number>
```
```ts [typescript]
const Health = world.component<number>();
```
:::
::: info
You should use this when creating components.
For example, a Health type should be created using this.
:::

View file

@ -1,3 +1 @@
## TODO
This is a TODO stub.

View file

@ -13,8 +13,8 @@ hero:
text: Get Started text: Get Started
link: /overview/get-started.md link: /overview/get-started.md
- theme: alt - theme: alt
text: API Examples text: API References
link: /api.md link: /api/
features: features:
- title: Stupidly Fast - title: Stupidly Fast
@ -26,4 +26,4 @@ features:
- title: Zero-Dependencies - title: Zero-Dependencies
icon: 📦 icon: 📦
details: Jecs doesn't rely on anything other than itself. details: Jecs doesn't rely on anything other than itself.
--- ---

View file

@ -1,4 +1,3 @@
--!optimize 2 --!optimize 2
--!native --!native
--!strict --!strict
@ -17,6 +16,7 @@ type ArchetypeEdge = {
remove: Archetype, remove: Archetype,
} }
type Archetype = { type Archetype = {
id: number, id: number,
edges: { [i53]: ArchetypeEdge }, edges: { [i53]: ArchetypeEdge },
@ -533,6 +533,9 @@ local function world_remove(world: World, entityId: i53, componentId: i53)
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
local record = entityIndex.sparse[entityId] local record = entityIndex.sparse[entityId]
local sourceArchetype = record.archetype local sourceArchetype = record.archetype
if not sourceArchetype then
return
end
local destinationArchetype = archetype_traverse_remove(world, componentId, sourceArchetype) local destinationArchetype = archetype_traverse_remove(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then if sourceArchetype and not (sourceArchetype == destinationArchetype) then
@ -669,6 +672,32 @@ do
end end
end end
local world_has: (world: World, entityId: number, ...i53) -> boolean
do
function world_has(world, entity_id, ...)
local id = entity_id
local record = world.entityIndex.sparse[id]
if not record then
return false
end
local archetype = record.archetype
if not archetype then
return false
end
local tr = archetype.records
for i = 1, select("#", ...) do
if not tr[select(i, ...)] then
return false
end
end
return true
end
end
type Item = () -> (number, ...any) type Item = () -> (number, ...any)
export type Query = typeof({ export type Query = typeof({
__iter = function(): Item __iter = function(): Item
@ -678,8 +707,8 @@ export type Query = typeof({
end, end,
}) & { }) & {
next: Item, next: Item,
replace: (Query, ...any) -> (), without: (Query) -> Query,
without: (Query) -> Query replace: (Query, (...any) -> (...any)) -> ()
} }
type CompatibleArchetype = { archetype: Archetype, indices: { number } } type CompatibleArchetype = { archetype: Archetype, indices: { number } }
@ -688,135 +717,146 @@ local world_query: (World, ...i53) -> Query
do do
local noop: Item = function() local noop: Item = function()
return nil :: any return nil :: any
end end
local EmptyQuery: Query = { local EmptyQuery: Query = {
__iter = function(): Item __iter = function(): Item
return noop return noop
end, end,
next = noop :: Item, next = noop :: Item,
replace = noop :: (Query, ...any) -> (), replace = noop :: (Query, ...any) -> (),
without = function(self: Query, ...) without = function(self: Query, ...)
return self return self
end end
} }
setmetatable(EmptyQuery, EmptyQuery) setmetatable(EmptyQuery, EmptyQuery)
local indices: { { number } }
local compatibleArchetypes: { Archetype }
local length
local components: { number }
local queryLength: number
local lastArchetype: number local lastArchetype: number
local archetype: Archetype local archetype: Archetype
local queryOutput: { any }
local queryLength: number
local entities: { number }
local i: number
local queryOutput: { any } local compatible_archetypes: { Archetype }
local column_indices: { { number} }
local ids: { number }
local entities: {} local function world_query_next(): any
local i: number
local function world_query_next()
local entityId = entities[i] local entityId = entities[i]
while entityId == nil do while entityId == nil do
lastArchetype += 1 lastArchetype += 1
archetype = compatibleArchetypes[lastArchetype] archetype = compatible_archetypes[lastArchetype]
if not archetype then if not archetype then
return return nil
end end
entities = archetype.entities
entities = archetype.entities
i = #entities i = #entities
entityId = entities[i] entityId = entities[i]
end end
local row = i local row = i
i-=1 i-=1
local columns = archetype.columns local columns = archetype.columns
local tr = indices[lastArchetype] local tr = column_indices[lastArchetype]
if queryLength == 1 then if queryLength == 1 then
return entityId, columns[tr[1]][row] return entityId, columns[tr[1]][row]
elseif queryLength == 2 then elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row]
elseif queryLength == 3 then elseif queryLength == 3 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
elseif queryLength == 4 then elseif queryLength == 4 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
elseif queryLength == 5 then elseif queryLength == 5 then
return entityId,columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], return entityId,
columns[tr[5]][row] columns[tr[1]][row],
elseif queryLength == 6 then columns[tr[2]][row],
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[3]][row],
columns[tr[5]][row], columns[tr[4]][row],
columns[tr[6]][row] columns[tr[5]][row]
elseif queryLength == 7 then elseif queryLength == 6 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], return entityId,
columns[tr[5]][row], columns[tr[1]][row],
columns[tr[6]][row], columns[tr[2]][row],
columns[tr[7]][row] columns[tr[3]][row],
elseif queryLength == 8 then columns[tr[4]][row],
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[5]][row],
columns[tr[5]][row], columns[tr[6]][row]
columns[tr[6]][row], elseif queryLength == 7 then
columns[tr[7]][row], return entityId,
columns[tr[8]][row] columns[tr[1]][row],
end columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
elseif queryLength == 8 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row],
columns[tr[8]][row]
end
for i in components do for j in ids do
queryOutput[i] = columns[tr[i]][row] queryOutput[j] = columns[tr[j]][row]
end end
return entityId, unpack(queryOutput, 1, queryLength) return entityId, unpack(queryOutput, 1, queryLength)
end end
local function world_query_without(self, ...): Query local function world_query_iter()
return world_query_next
end
local function world_query_without(self, ...)
local withoutComponents = { ... } local withoutComponents = { ... }
for i = #compatibleArchetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatibleArchetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local shouldRemove = false local shouldRemove = false
for _, componentId in withoutComponents do for _, componentId in withoutComponents do
if records[componentId] then if records[componentId] then
shouldRemove = true shouldRemove = true
break break
end end
end end
if shouldRemove then if shouldRemove then
table.remove(compatibleArchetypes, i) table.remove(compatible_archetypes, i)
table.remove(column_indices, i)
end end
end end
if #compatibleArchetypes == 0 then
return EmptyQuery
end
return self
end
local function world_query_iter()
lastArchetype = 1 lastArchetype = 1
archetype = compatibleArchetypes[1] archetype = compatible_archetypes[lastArchetype]
entities = archetype.entities
i = #entities
return world_query_next if not archetype then
return EmptyQuery
end
return self
end end
local function world_query_replace_values(row, columns, ...) local function world_query_replace_values(row, columns, ...)
for i, column in columns do for i, column in columns do
column[row] = select(i, ...) column[row] = select(i, ...)
end end
end end
local function world_query_replace(_, fn: any) local function world_query_replace(_, fn: (...any) -> (...any))
for i, archetype in compatibleArchetypes do for i, archetype in compatible_archetypes do
local tr = indices[i] local tr = column_indices[i]
local columns = archetype.columns local columns = archetype.columns
for row in archetype.entities do for row in archetype.entities do
@ -881,81 +921,88 @@ do
return query return query
end end
function world_query(world: World, ...: number): Query function world_query(world: World, ...: any): Query
-- breaking? -- breaking?
if (...) == nil then if (...) == nil then
error("Missing components") error("Missing components")
end end
indices = {} local indices = {}
compatibleArchetypes = {} local compatibleArchetypes = {}
length = 0 local length = 0
components = { ... }
local archetypes: { Archetype } = world.archetypes :: any local components = { ... } :: any
local firstArchetypeMap: ArchetypeMap local archetypes = world.archetypes
local componentIndex = world.componentIndex
for _, componentId in components do local firstArchetypeMap: ArchetypeMap
local map: ArchetypeMap = componentIndex[componentId] :: any local componentIndex = world.componentIndex
if not map then
return EmptyQuery
end
if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size > map.size then for _, componentId in components do
firstArchetypeMap = map local map = componentIndex[componentId]
end if not map then
end return EmptyQuery
end
for id in firstArchetypeMap.cache do if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
local compatibleArchetype = archetypes[id] firstArchetypeMap = map
local archetypeRecords = compatibleArchetype.records end
end
local records: { number } = {} for id in firstArchetypeMap.cache do
local skip = false local compatibleArchetype = archetypes[id]
local archetypeRecords = compatibleArchetype.records
for i, componentId in components do local records = {}
local index = archetypeRecords[componentId] local skip = false
if not index then
skip = true
break
end
-- index should be index.offset
records[i] = index
end
if skip then for i, componentId in components do
continue local index = archetypeRecords[componentId]
end if not index then
skip = true
break
end
-- index should be index.offset
records[i] = index
end
length += 1 if skip then
compatibleArchetypes[length] = compatibleArchetype continue
indices[length] = records end
end
lastArchetype = 1 length += 1
archetype = compatibleArchetypes[lastArchetype] compatibleArchetypes[length] = compatibleArchetype
indices[length] = records
end
if not archetype then compatible_archetypes = compatibleArchetypes
return EmptyQuery column_indices = indices
end ids = components
queryOutput = {} lastArchetype = 1
queryLength = #components archetype = compatible_archetypes[lastArchetype]
entities = archetype.entities if not archetype then
i = #entities return EmptyQuery
end
local it = { queryOutput = {}
__iter = world_query_iter, queryLength = #ids
next = world_query_next,
without = world_query_without,
with = world_query_with,
replace = world_query_replace,
}
return setmetatable(it, it) :: any entities = archetype.entities
end i = #entities
local it = {
__iter = world_query_iter,
next = world_query_next,
with = world_query_with,
without = world_query_without,
replace = world_query_replace,
} :: any
setmetatable(it, it)
return it
end
end end
type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53)
@ -998,11 +1045,13 @@ export type WorldShim = typeof(setmetatable(
--- Removes a component from the given entity --- Removes a component from the given entity
remove: (WorldShim, id: Entity, component: Entity) -> (), remove: (WorldShim, id: Entity, component: Entity) -> (),
--- Retrieves the value of up to 4 components. These values may be nil. --- Retrieves the value of up to 4 components. These values may be nil.
get: (<A>(WorldShim, id: any, Entity<A>) -> A) get: (<A>(WorldShim, id: Entity, Entity<A>) -> A)
& (<A, B>(WorldShim, id: Entity, Entity<A>, Entity<B>) -> (A, B)) & (<A, B>(WorldShim, id: Entity, Entity<A>, Entity<B>) -> (A, B))
& (<A, B, C>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>) -> (A, B, C)) & (<A, B, C>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>) -> (A, B, C))
& <A, B, C, D>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> (A, B, C, D), & <A, B, C, D>(WorldShim, id: Entity, Entity<A>, Entity<B>, Entity<C>, Entity<D>) -> (A, B, C, D),
has: (WorldShim, Entity, ...Entity) -> boolean,
--- Searches the world for entities that match a given query --- Searches the world for entities that match a given query
query: (<A>(WorldShim, Entity<A>) -> QueryShim<A>) query: (<A>(WorldShim, Entity<A>) -> QueryShim<A>)
& (<A, B>(WorldShim, Entity<A>, Entity<B>) -> QueryShim<A, B>) & (<A, B>(WorldShim, Entity<A>, Entity<B>) -> QueryShim<A, B>)
@ -1104,6 +1153,7 @@ World.component = world_component
World.add = world_add World.add = world_add
World.set = world_set World.set = world_set
World.get = world_get World.get = world_get
World.has = world_has
World.target = world_target World.target = world_target
World.parent = world_parent World.parent = world_parent

View file

@ -25,6 +25,24 @@ local N = 10
type World = jecs.WorldShim type World = jecs.WorldShim
TEST("world", function() TEST("world", function()
do CASE "should allow remove a component that doesn't exist on entity"
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
do
world:remove(id, Poison)
CHECK(true) -- Didn't error
end
world:set(id, Health, 50)
world:remove(id, Poison)
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
do CASE("should find every component id") do CASE("should find every component id")
local world = jecs.World.new() :: World local world = jecs.World.new() :: World
local A = world:component() local A = world:component()
@ -60,10 +78,9 @@ TEST("world", function()
world:clear(e) world:clear(e)
CHECK(world:get(e, A) == nil) CHECK(world:get(e, A) == nil)
CHECK(world:get(e, B) == nil) CHECK(world:get(e, B) == nil)
end end
do CASE("iterator should not drain the query") do CASE("should drain query while iterating")
local world = jecs.World.new() :: World local world = jecs.World.new() :: World
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -85,7 +102,8 @@ TEST("world", function()
for _ in q do for _ in q do
j+=1 j+=1
end end
CHECK(i == j) CHECK(i == 2)
CHECK(j == 0)
end end
do CASE("should be able to get next results") do CASE("should be able to get next results")
@ -224,20 +242,6 @@ TEST("world", function()
CHECK(world:get(id1, Health) == 50) CHECK(world:get(id1, Health) == 50)
end end
do CASE("should allow remove that doesn't exist on entity")
local world = jecs.World.new()
local Health = world:entity()
local Poison = world:component()
local id = world:entity()
world:set(id, Health, 50)
world:remove(id, Poison)
CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == 50)
end
do CASE("should increment generation") do CASE("should increment generation")
local world = jecs.World.new() local world = jecs.World.new()
local e = world:entity() local e = world:entity()
@ -530,6 +534,34 @@ TEST("world", function()
CHECK(withoutCount == 0) CHECK(withoutCount == 0)
end end
do CASE "should find Tag on entity"
local world = jecs.World.new()
local Tag = world:component()
local e = world:entity()
world:add(e, Tag)
CHECK(world:has(e, Tag))
end
do CASE "should return false when missing one tag"
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, C)
world:add(e, D)
CHECK(world:has(e, A, B, C, D) == false)
end
end) end)