Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ tests/world
tests/events
tests/manycomponents
tests/tbenchmarks
tests/tremoval

benchs/vecs_bench
benchs/vecs.csv
Expand Down
47 changes: 28 additions & 19 deletions src/world.nim
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# `vecs` is a free open source ECS library for Nim.
import std/[packedsets, hashes, macros, intsets, options]
import typetraits, tables, sets
import entityid, componentid, archetypeid, archetype, entity, ecsseq, queries, components, operations, operationmodes, events
import entityid, componentid, archetypeid, archetype, entity, ecsseq, unsafeseq, queries, components, operations, operationmodes, events
export entityid, components.Meta, operationmodes
export components
export events
Expand Down Expand Up @@ -287,10 +287,7 @@ template accessor[T](world: var World, archetype: Archetype, archetypeEntityId:


proc componentData[T](componentList: EcsSeq[T]): ptr UncheckedArray[T] {.inline.} =
if componentList.len == 0:
return nil

cast[ptr UncheckedArray[T]](unsafeAddr componentList[0])
cast[ptr UncheckedArray[T]](componentList.rawPtr.rawSeqDataPtr)


macro buildReadTuple(world: var World, t: typedesc, archetype: untyped, archetypeEntityId: untyped): untyped =
Expand Down Expand Up @@ -353,19 +350,19 @@ macro buildComponentColumns(world: var World, t: typedesc, archetype: untyped):
if isOp(fieldType, "Opt"):
quote do:
block:
let componentId = `world`.componentIdFrom typeof `componentType`
let componentId = `componentType`.toComponentId

if `archetype`.contains(componentId):
let ind = `archetype`.getIndex(componentId)
let ind = `archetype`.toIndexMap[componentId.int] - 1
let componentList = cast[EcsSeq[`componentType`]](`archetype`.componentLists[ind])
componentData(componentList)
else:
cast[ptr UncheckedArray[`componentType`]](nil)
else:
quote do:
block:
let componentId = `world`.componentIdFrom typeof `componentType`
let ind = `archetype`.getIndex(componentId)
let componentId = `componentType`.toComponentId
let ind = `archetype`.toIndexMap[componentId.int] - 1
let componentList = cast[EcsSeq[`componentType`]](`archetype`.componentLists[ind])
componentData(componentList)

Expand Down Expand Up @@ -432,19 +429,34 @@ proc consolidateAddComponents(world: var World, id: EntityId, componentsToAdd: T
proc consolidateRemoveComponents(world: var World, id: EntityId, compIdsToRemove: PackedSet[ComponentId]) =
var entity = world.entities[id.value]
var previousArchetype = world.archetypes[entity.archetypeIndex]
var componentIds: seq[ComponentId]
var nextArchetypeId = previousArchetype.id

for componentId in compIdsToRemove.items:
nextArchetypeId.excl componentId

for compId in compIdsToRemove.items:
componentIds.add compId
var nextIndex = world.archIdToIndex.getOrDefault(nextArchetypeId, -1)

if nextIndex < 0:
var componentIds: seq[ComponentId]

for componentId in compIdsToRemove.items:
componentIds.add componentId

nextIndex = world.nextArchetypeRemovingFrom(previousArchetype, componentIds)

let nextIndex = world.nextArchetypeRemovingFrom(previousArchetype, componentIds)
var nextArchetype = world.archetypes[nextIndex]

entity.archetypeIndex = nextIndex
entity.archetypeEntityId = previousArchetype.moveRemoving(entity.archetypeEntityId, nextArchetype)
world.entities[id.value] = entity


proc setMetaId(archetype: Archetype, archetypeEntityId: int, id: EntityId) {.inline.} =
let metaIndex = archetype.getIndex(Meta.toComponentId)
let metaComponents = cast[EcsSeq[Meta]](archetype.componentLists[metaIndex])
metaComponents[archetypeEntityId].id = id


iterator archetypes*(world: var World): Archetype =
## Iterate through all the world's archetypes.
##
Expand Down Expand Up @@ -787,16 +799,14 @@ proc add*[T: tuple](world: var World, components: T, mode: OperationMode = Defer
let entity = Entity(archetypeIndex: archetypeIndex, archetypeEntityId: archetypeEntityId)
result = world.allocateEntity(entity)

for meta in world.write(result, Meta):
meta.id = result
world.archetypes[archetypeIndex].setMetaId(archetypeEntityId, result)
else:
let archetypeIndex = world.archetypeFrom (Meta,)
let archetypeEntityId = world.archetypes[archetypeIndex].add (Meta(),)
let entity = Entity(archetypeIndex: archetypeIndex, archetypeEntityId: archetypeEntityId)
result = world.allocateEntity(entity)

for meta in world.write(result, Meta):
meta.id = result
world.archetypes[archetypeIndex].setMetaId(archetypeEntityId, result)

world.add(result, components, mode)

Expand All @@ -818,8 +828,7 @@ proc addEmpty*(world: var World): EntityId {.discardable.} =
let entity = Entity(archetypeIndex: archetypeIndex, archetypeEntityId: archetypeEntityId)
result = world.allocateEntity(entity)

for meta in world.write(result, Meta):
meta.id = result
world.archetypes[archetypeIndex].setMetaId(archetypeEntityId, result)


proc addWithSpecificId*(world: var World, id: EntityId) =
Expand Down
100 changes: 100 additions & 0 deletions tests/tremoval.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# ISC License
# Copyright (c) 2025 RowDaBoat
# `vecs` is a free open source ECS library for Nim.
import unittest
import ../src/[examples, vecs]


type RemovalPayload = ref object
value: string


type RetainedComponent = object
payload: RemovalPayload


suite "Component removal should":
test "accept duplicate component types":
var world = World()
let entityId = world.add((Character(name: "Marcus"), Health(health: 100), Weapon(name: "Sword")), Immediate)

world.remove(entityId, (Health, Weapon, Health), Immediate)

check world.has(entityId)
check world.read(entityId, Meta).id == entityId
check world.read(entityId, Character).name == "Marcus"
check not world.has(entityId, Health)
check not world.has(entityId, Weapon)


test "retain reference values when removal creates and then reuses a destination":
var world = World()
let firstPayload = RemovalPayload(value: "First")
let secondPayload = RemovalPayload(value: "Second")
let firstId = world.add((RetainedComponent(payload: firstPayload), Health(health: 100), Character(name: "Marcus"), Weapon(name: "Sword")), Immediate)
let secondId = world.add((RetainedComponent(payload: secondPayload), Health(health: 80), Character(name: "Elena"), Weapon(name: "Staff")), Immediate)

world.remove(firstId, (Weapon, Health), Immediate)
world.remove(secondId, (Health, Weapon), Immediate)
GC_fullCollect()

var query: Query[(Meta, Character, RetainedComponent)]
var entityCount = 0

for (meta, character, retained) in world.query(query):
inc entityCount
check not world.has(meta.id, Health)
check not world.has(meta.id, Weapon)

if meta.id == firstId:
check character.name == "Marcus"
check retained.payload == firstPayload
check retained.payload.value == "First"
else:
check meta.id == secondId
check character.name == "Elena"
check retained.payload == secondPayload
check retained.payload.value == "Second"

check entityCount == 2


test "retain reference values when destination columns have a different order":
var world = World()
let existingPayload = RemovalPayload(value: "Existing")
let movedPayload = RemovalPayload(value: "Moved")
let existingId = world.add((Character(name: "Elena"), RetainedComponent(payload: existingPayload)), Immediate)
let movedId = world.add((RetainedComponent(payload: movedPayload), Health(health: 100), Character(name: "Marcus"), Weapon(name: "Sword")), Immediate)

world.remove(movedId, (Weapon, Health), Immediate)
GC_fullCollect()

check world.read(existingId, Character).name == "Elena"
check world.read(existingId, RetainedComponent).payload == existingPayload
check world.read(existingId, RetainedComponent).payload.value == "Existing"
check world.read(movedId, Meta).id == movedId
check world.read(movedId, Character).name == "Marcus"
check world.read(movedId, RetainedComponent).payload == movedPayload
check world.read(movedId, RetainedComponent).payload.value == "Moved"
check not world.has(movedId, Health)
check not world.has(movedId, Weapon)


test "preserve a pending addition across immediate removal":
var world = World()
let entityId = world.add((Character(name: "Marcus"), Health(health: 100)), Immediate)
world.add(entityId, Weapon(name: "Sword", attack: 10))

world.remove(entityId, Health, Immediate)

check not world.has(entityId, Health)
check not world.has(entityId, Weapon)
check world.read(entityId, Character).name == "Marcus"

world.consolidate()

check not world.has(entityId, Health)
check world.read(entityId, Meta).id == entityId
check world.read(entityId, Character).name == "Marcus"
check world.read(entityId, Weapon).name == "Sword"
check world.read(entityId, Weapon).attack == 10
Loading