From 0567856a5998fc8e9c0a0bfb8debe0ff16ccba92 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 27 May 2024 03:39:20 +0200 Subject: [PATCH] Add docs (#45) * Initial commit * Add section for standalone * Fix docs * Add pages to docs * Remove redundant files --- README.md | 2 +- docs/api-types.md | 45 ++ docs/tutorials/quick-start/getting-started.md | 19 + docs/tutorials/quick-start/rbxm.png | Bin 0 -> 34402 bytes lib/init.lua | 273 +++++---- mkdocs.yml | 186 ++++++ tests/world.lua | 552 +++++++++--------- 7 files changed, 665 insertions(+), 412 deletions(-) create mode 100644 docs/api-types.md create mode 100644 docs/tutorials/quick-start/getting-started.md create mode 100644 docs/tutorials/quick-start/rbxm.png create mode 100644 mkdocs.yml diff --git a/README.md b/README.md index 059e172..c5ece64 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/api-types.md b/docs/api-types.md new file mode 100644 index 0000000..b446999 --- /dev/null +++ b/docs/api-types.md @@ -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. + diff --git a/docs/tutorials/quick-start/getting-started.md b/docs/tutorials/quick-start/getting-started.md new file mode 100644 index 0000000..bd702d2 --- /dev/null +++ b/docs/tutorials/quick-start/getting-started.md @@ -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 +``` \ No newline at end of file diff --git a/docs/tutorials/quick-start/rbxm.png b/docs/tutorials/quick-start/rbxm.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8f38d5b842c87789249317e03a70c24f45ab48 GIT binary patch literal 34402 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

J;wRKgqu9eJayK1e z&$AVytU`@SAnaioMNr@;{ouuamPluxB~H|%&-?gOOH5F|#~1C+>D+ViJ>nTRXxkbt zIyHasbYvF$#9}I6QXIVsJfkJ%YoVW{aw7kU(F~YIAbQdbIDZ_A#%uC;Pd>q@*h?$q zM~olW;yp`L6RFy6fTwpI68_=r%=2#(al7Q%p6<)mru)FCgu7=WC3mc|V>rV%nN(9m z-Stm5P6($YvOortT#dB~Rj7F#?vPTF)U4TBBID`%-BXA}xc_h^lKe#e#!dUzMd;nfpLyjmu*jPWfq_Sb(~V;vA5AbQYCi8?PmI2zrUE%e61hY zN1TH7X{3hey*k}XOS?yEB09Iq%=t>#ob;V+5WStfns$`7D7BR`&LmjKdX()`JC%e} z)3WgfkqR>S*99TsL00vpqBy1{_bdD>H*+?(4f72Zew5j-Lp=KVe?R!R7L6oa|9xj& z|CQ6}hSJ1+dm!785Y=}5$jH_Yuk{AjAvGSXl8_ZJg)J)5u4-?c6UA>LU!q=m;}@$G z(7WoSbk1*z2(Y$q=DMJ>Hz`u~QMx(?o@-V7KPv{Zbmw8m?`n8`AfRe}Do;f3pC9WN zSDCiTNjbaK@1tWWK7vMlkt6!$A`DC85q0S@QUn+^c+#iBm6@&qe; zZ-q4nlgt7;v?clAD5)v^=?hp7{lNOPchSU6K%24-Qln&fr>h?>3hHk5!L%82-HHf@ zbagbBW`y-B9tlQq4pL7b)c@fS()lLp+~%KT!ewnZtKA;5pN12$gl|9Ng8G4RA=5z)H9uFX zCpxQSL}PL<>^F~HGm%rM7#!34p@H8gslH!oI>OqaFA-;AQS8?J>!OpR5GeZhG$A;@ zB3AF*p$QUTYmrDAlyG#+X%y1!4{6ph+1a08w0CZPs3-!aewW%RzhC@9ToSE2-r8GP zCH;?w=LtJ-Dp3maeK}D&WsmkL;Q{)we(!qxI?~M43r1~B6uARB9=OZRo zcVz=aiRQNME2YDSjXQuR1JV7-R+rdTL+N<(?lIF=lAcJ(WpCx#oP3>I+n(|slpLK+ zOgx@xs6VnUWqeNWOyQ-|_R>wr`k!uQ z@Ac^HEepgVS~NQ|oo=G*fMK(7RYi~7Oee6NwM|Hf9Z1<$^brq9e0UDK~^OyxR|)B>e7Fy2MHR z#xp<*!f`AVIh)qmvj-GLvcdfx zfKusWY$=f`)b6B^RU5vJ_RCHE$Av{0B_7uUdChy}RF2OO$O5^(55kxmM*cxPkNyS; zY^lV2y3FwGC!qDW!x%KId|{aw&p|(!|8}6i+TWM>M_-xz?Y!PH5xez`{~wL}x8vA< zb^W5vNrJ^0zer$~-hVZ#!@CM24@~M%_zls8{0}g!yOfS749u?GWco?vV##yL!l{Vk zADpJp4WDmepaK&3HHZWEGkwApHAE;J>QX5~9{3r;G7kR|8Ee8L~|w4;n(n9e!RX1lJ`4QzdWfq1$eXwlqP@A zcK{DO0rQtB9!K9T#TMU8A=?q^1Atk+LpSf!FL#!(rm*hl96z+kM7RCs)ic1qXVOYi J`A-br{vYTowmAR* literal 0 HcmV?d00001 diff --git a/lib/init.lua b/lib/init.lua index 5cf38ea..9ef0b10 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -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, diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..5029861 --- /dev/null +++ b/mkdocs.yml @@ -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 diff --git a/tests/world.lua b/tests/world.lua index f0eff7d..79a2686 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -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(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() \ No newline at end of file +FINISH() +