mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Add World:target (#39)
This commit is contained in:
parent
e86b4c7f4c
commit
cf0683cf03
3 changed files with 232 additions and 85 deletions
63
README.md
63
README.md
|
@ -22,47 +22,38 @@ jecs is a stupidly fast Entity Component System (ECS).
|
|||
### Example
|
||||
|
||||
```lua
|
||||
local world = World.new()
|
||||
local world = jecs.World.new()
|
||||
local pair = jecs.pair
|
||||
|
||||
local player = world:entity()
|
||||
local opponent = world:entity()
|
||||
local ChildOf = world:component()
|
||||
local Name = world:component()
|
||||
|
||||
local Health = world:component()
|
||||
local Position = world:component()
|
||||
-- Notice how components can just be entities as well?
|
||||
-- It allows you to model relationships easily!
|
||||
local Damage = world:entity()
|
||||
local DamagedBy = world:entity()
|
||||
|
||||
world:set(player, Health, 100)
|
||||
world:set(player, Damage, 8)
|
||||
world:set(player, Position, Vector3.new(0, 5, 0))
|
||||
|
||||
world:set(opponent, Health, 100)
|
||||
world:set(opponent, Damage, 21)
|
||||
world:set(opponent, Position, Vector3.new(0, 5, 3))
|
||||
|
||||
for playerId, playerPosition, health in world:query(Position, Health) do
|
||||
local totalDamage = 0
|
||||
for opponentId, opponentPosition, damage in world:query(Position, Damage) do
|
||||
if playerId == opponentId then
|
||||
continue
|
||||
end
|
||||
if (playerPosition - opponentPosition).Magnitude < 5 then
|
||||
totalDamage += damage
|
||||
end
|
||||
-- We create a pair between the relation component `DamagedBy` and the entity id of the opponent.
|
||||
-- This will allow us to specifically query for damage exerted by a specific opponent.
|
||||
world:set(playerId, ECS_PAIR(DamagedBy, opponentId), totalDamage)
|
||||
local function parent(entity)
|
||||
return world:target(entity, ChildOf)
|
||||
end
|
||||
local function name()
|
||||
|
||||
local alice = world:entity()
|
||||
world:set(alice, Name, "alice")
|
||||
|
||||
local bob = world:entity()
|
||||
world:add(bob, pair(ChildOf, alice))
|
||||
world:set(bob, Name, "bob")
|
||||
|
||||
local sara = world:entity()
|
||||
world:add(sara, pair(ChildOf, alice))
|
||||
world:set(sara, Name, "sara")
|
||||
|
||||
print(getName(parent(sara)))
|
||||
|
||||
for e in world:query(pair(ChildOf, alice)) do
|
||||
print(getName(e), "is the child of alice")
|
||||
end
|
||||
|
||||
-- Gets the damage inflicted by our specific opponent!
|
||||
for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, opponent)) do
|
||||
world:set(playerId, health - inflicted)
|
||||
end
|
||||
|
||||
assert(world:get(player, Health) == 79)
|
||||
-- Output
|
||||
-- "alice"
|
||||
-- bob is the child of alice
|
||||
-- sara is the child of alice
|
||||
```
|
||||
|
||||
125 archetypes, 4 random components queried.
|
||||
|
|
166
lib/init.lua
166
lib/init.lua
|
@ -14,7 +14,7 @@ type Column = {any}
|
|||
type Archetype = {
|
||||
id: number,
|
||||
edges: {
|
||||
[i24]: {
|
||||
[i53]: {
|
||||
add: Archetype,
|
||||
remove: Archetype,
|
||||
},
|
||||
|
@ -26,17 +26,37 @@ type Archetype = {
|
|||
records: {},
|
||||
}
|
||||
|
||||
|
||||
type Record = {
|
||||
archetype: Archetype,
|
||||
row: number,
|
||||
dense: i24,
|
||||
componentRecord: ArchetypeMap
|
||||
}
|
||||
|
||||
type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}}
|
||||
type ComponentIndex = {[i24]: ArchetypeMap}
|
||||
|
||||
type ArchetypeRecord = number
|
||||
type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
|
||||
--[[
|
||||
TODO:
|
||||
{
|
||||
index: number,
|
||||
count: number,
|
||||
column: number
|
||||
}
|
||||
|
||||
]]
|
||||
|
||||
type ArchetypeMap = {
|
||||
cache: {[number]: ArchetypeRecord},
|
||||
first: ArchetypeMap,
|
||||
second: ArchetypeMap,
|
||||
parent: ArchetypeMap,
|
||||
size: number
|
||||
}
|
||||
|
||||
type ComponentIndex = {[i24]: ArchetypeMap}
|
||||
|
||||
type Archetypes = {[ArchetypeId]: Archetype}
|
||||
|
||||
type ArchetypeDiff = {
|
||||
|
@ -96,6 +116,7 @@ local function ECS_GENERATION(e: i53)
|
|||
return e % ECS_GENERATION_MASK
|
||||
end
|
||||
|
||||
-- SECOND
|
||||
local function ECS_ENTITY_T_LO(e: i53)
|
||||
e //= 0x10
|
||||
return e // ECS_ENTITY_MASK
|
||||
|
@ -107,7 +128,7 @@ local function ECS_GENERATION_INC(e: i53)
|
|||
return ECS_COMBINE(id, generation + 1) + flags
|
||||
end
|
||||
|
||||
-- gets the high ID
|
||||
-- FIRST gets the high ID
|
||||
local function ECS_ENTITY_T_HI(entity: i53): i24
|
||||
entity //= 0x10
|
||||
local first = entity % ECS_ENTITY_MASK
|
||||
|
@ -131,8 +152,13 @@ local function ECS_PAIR(pred: number, obj: number)
|
|||
ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true)
|
||||
end
|
||||
|
||||
local function getAlive(entityIndex: EntityIndex, id: i53)
|
||||
return entityIndex.dense[id]
|
||||
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
|
||||
end
|
||||
|
||||
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
|
||||
|
@ -239,17 +265,29 @@ local function hash(arr): string | number
|
|||
return table.concat(arr, "_")
|
||||
end
|
||||
|
||||
local function createArchetypeRecord(componentIndex, id, componentId, i)
|
||||
local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap
|
||||
local archetypesMap = componentIndex[componentId]
|
||||
|
||||
if not archetypesMap then
|
||||
archetypesMap = {size = 0, sparse = {}}
|
||||
archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap
|
||||
componentIndex[componentId] = archetypesMap
|
||||
end
|
||||
archetypesMap.sparse[id] = i
|
||||
|
||||
archetypesMap.cache[archetypeId] = i
|
||||
archetypesMap.size += 1
|
||||
|
||||
return archetypesMap
|
||||
end
|
||||
|
||||
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
|
||||
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 ty = hash(types)
|
||||
|
||||
local id = world.nextArchetypeId + 1
|
||||
|
@ -257,25 +295,27 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet
|
|||
|
||||
local length = #types
|
||||
local columns = table.create(length)
|
||||
local componentIndex = world.componentIndex
|
||||
|
||||
local records = {}
|
||||
local componentIndex = world.componentIndex
|
||||
local entityIndex = world.entityIndex
|
||||
for i, componentId in types do
|
||||
createArchetypeRecord(componentIndex, id, componentId, i)
|
||||
ensureComponentRecord(componentIndex, id, componentId, i)
|
||||
records[componentId] = i
|
||||
columns[i] = {}
|
||||
|
||||
if ECS_IS_PAIR(componentId) then
|
||||
local pred = ECS_PAIR_RELATION(entityIndex, componentId)
|
||||
local obj = ECS_PAIR_OBJECT(entityIndex, componentId)
|
||||
local first = ECS_PAIR(pred, WILDCARD)
|
||||
local second = ECS_PAIR(WILDCARD, obj)
|
||||
createArchetypeRecord(componentIndex, id, first, i)
|
||||
createArchetypeRecord(componentIndex, id, second, i)
|
||||
records[first] = i
|
||||
records[second] = i
|
||||
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)
|
||||
records[idr_r] = i
|
||||
|
||||
local idr_t = ECS_PAIR(WILDCARD, object)
|
||||
ensureComponentRecord(
|
||||
componentIndex, id, idr_t, i)
|
||||
records[idr_t] = i
|
||||
end
|
||||
columns[i] = {}
|
||||
end
|
||||
|
||||
local archetype = {
|
||||
|
@ -333,6 +373,29 @@ function World.entity(world: World)
|
|||
return nextEntityId(world.entityIndex, entityId + REST)
|
||||
end
|
||||
|
||||
-- TODO:
|
||||
-- should have an additional `index` parameter which selects the nth target
|
||||
-- this is important when an entity can have multiple relationships with the same target
|
||||
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
|
||||
return nil
|
||||
end
|
||||
local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)]
|
||||
if not componentRecord then
|
||||
return nil
|
||||
end
|
||||
|
||||
local archetypeRecord = componentRecord.cache[archetype.id]
|
||||
if not archetypeRecord then
|
||||
return nil
|
||||
end
|
||||
|
||||
return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord])
|
||||
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
|
||||
|
@ -347,41 +410,54 @@ local function destructColumns(columns, count, row)
|
|||
end
|
||||
end
|
||||
|
||||
local function archetypeDelete(entityIndex, record: Record, entityId: i53, destruct: boolean)
|
||||
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
|
||||
world:remove(entity, id)
|
||||
end
|
||||
end
|
||||
|
||||
componentIndex[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function World.delete(world: World, entityId: i53)
|
||||
local record = world.entityIndex.sparse[entityId]
|
||||
if not record then
|
||||
return
|
||||
end
|
||||
local entityIndex = world.entityIndex
|
||||
local sparse, dense = entityIndex.sparse, entityIndex.dense
|
||||
local archetype = record.archetype
|
||||
local row = record.row
|
||||
|
||||
archetypeDelete(world, entityId)
|
||||
archetypeDelete(world, ECS_PAIR(entityId, WILDCARD))
|
||||
archetypeDelete(world, ECS_PAIR(WILDCARD, entityId))
|
||||
|
||||
if archetype then
|
||||
local entities = archetype.entities
|
||||
local last = #entities
|
||||
|
||||
local entityToMove = entities[last]
|
||||
|
||||
if row ~= last then
|
||||
local entityToMove = entities[last]
|
||||
dense[record.dense] = entityToMove
|
||||
sparse[entityToMove] = record
|
||||
end
|
||||
|
||||
sparse[entityId] = nil
|
||||
dense[#dense] = nil
|
||||
|
||||
entities[row], entities[last] = entities[last], nil
|
||||
|
||||
local columns = archetype.columns
|
||||
|
||||
if not destruct then
|
||||
return
|
||||
end
|
||||
|
||||
destructColumns(columns, last, row)
|
||||
end
|
||||
|
||||
function World.delete(world: World, entityId: i53)
|
||||
local entityIndex = world.entityIndex
|
||||
local record = entityIndex.sparse[entityId]
|
||||
if not record then
|
||||
return
|
||||
end
|
||||
archetypeDelete(entityIndex, record, entityId, true)
|
||||
sparse[entityId] = nil
|
||||
dense[#dense] = nil
|
||||
end
|
||||
|
||||
export type World = typeof(World.new())
|
||||
|
@ -530,6 +606,10 @@ 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
|
||||
return nil
|
||||
end
|
||||
|
||||
local archetypeRecord = archetype.records[componentId]
|
||||
|
||||
if not archetypeRecord then
|
||||
|
@ -575,7 +655,7 @@ EmptyQuery.__index = EmptyQuery
|
|||
setmetatable(EmptyQuery, EmptyQuery)
|
||||
|
||||
export type Query = typeof(EmptyQuery)
|
||||
|
||||
local testkit = require("../testkit")
|
||||
function World.query(world: World, ...: i53): Query
|
||||
-- breaking?
|
||||
if (...) == nil then
|
||||
|
@ -603,9 +683,10 @@ function World.query(world: World, ...: i53): Query
|
|||
end
|
||||
end
|
||||
|
||||
for id in firstArchetypeMap.sparse do
|
||||
for id in firstArchetypeMap.cache do
|
||||
local archetype = archetypes[id]
|
||||
local archetypeRecords = archetype.records
|
||||
|
||||
local indices = {}
|
||||
local skip = false
|
||||
|
||||
|
@ -615,6 +696,7 @@ function World.query(world: World, ...: i53): Query
|
|||
skip = true
|
||||
break
|
||||
end
|
||||
-- index should be index.offset
|
||||
indices[i] = index
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
local testkit = require("../testkit")
|
||||
local jecs = require("../lib/init")
|
||||
local __ = jecs.Wildcard
|
||||
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
|
||||
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
|
||||
local IS_PAIR = jecs.IS_PAIR
|
||||
|
@ -9,7 +10,16 @@ local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION
|
|||
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, ...)
|
||||
|
||||
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()
|
||||
|
@ -256,6 +266,70 @@ TEST("world", function()
|
|||
end
|
||||
CHECK(count == 1)
|
||||
end
|
||||
|
||||
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, ECS_PAIR(Eats, Apples), "bob eats apples")
|
||||
world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
|
||||
|
||||
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 == 0)
|
||||
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)
|
||||
|
||||
CHECK_NO_ERR("Apples should be dead", function()
|
||||
world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
|
||||
end)
|
||||
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()
|
Loading…
Reference in a new issue