-
-
Notifications
You must be signed in to change notification settings - Fork 0
Occupants and Partitions
This page covers the two main extension-oriented ways to attach runtime meaning to a voxel:
- occupants, which represent dynamic presence in the grid
- partitions, which attach typed metadata or behavior directly to a voxel
They solve different problems, and using the right one keeps both the code and the mental model cleaner.
Occupants answer "what dynamic thing is currently here?"
They are built for entities that move, enter and leave voxels, participate in scan queries, and need runtime lookup by position, group, or ticket.
Partitions answer "what typed metadata or behavior is attached to this voxel?"
They are built for durable voxel-local extensions such as terrain tags, domain-specific rules, or custom behaviors that should live with the voxel itself instead of inside the scan system.
IVoxelOccupant is the interface that scan and occupant workflows depend on.
It requires:
-
GlobalIdfor stable identity -
Positionfor world-space resolution -
OccupantGroupIdfor grouped query filtering
GridForge owns occupancy bookkeeping internally. If you need to inspect what GridForge is currently tracking for an occupant, use:
GridOccupantManager.GetOccupiedIndices(...)GridOccupantManager.TryGetOccupancyTicket(...)
That keeps the tracked WorldVoxelIndex plus scan-cell ticket relationship inside GridForge instead of forcing every consumer implementation to mirror it manually.
Occupants are managed through GridOccupantManager and stored indirectly through ScanCell.
The flow looks like this:
- resolve the target voxel
- find the voxel's owning scan cell
- insert the occupant into the scan cell bucket for that voxel's
WorldVoxelIndex - record the returned ticket in GridForge's internal occupancy registry
- increment the voxel's occupant count
- ensure the scan cell is tracked in
grid.ActiveScanCells - publish manager-level and voxel-level occupant events
That means occupants are both:
- voxel-local for exact placement
- scan-cell indexed for efficient query flow
The add path rejects a few important cases:
- null occupants
- duplicate registration to the same voxel identity
- voxels without vacancy
- voxels whose scan cell cannot be resolved
If the add succeeds, the target voxel's OccupantCount increases and the owning scan cell becomes active for later scans.
Removal uses GridForge's tracked occupancy record to recover the scan-cell ticket that was assigned at add time.
If removal succeeds:
- the scan cell bucket entry is removed
- the tracked registry entry for that voxel is removed
- the voxel's
OccupantCountdecreases - empty scan cells are dropped from
grid.ActiveScanCells - the active scan-cell set itself is released back to a pool when the grid becomes unoccupied
That last point is worth calling out because it is easy to forget: active scan-cell tracking is intentionally allocation-conscious and can disappear entirely when no occupants remain.
OccupantGroupId is the lightweight grouping hook used by scan queries.
Typical examples:
- ally vs enemy teams
- factions or ownership groups
- sensor or channel ids
- simulation categories
This lets query code filter cheaply without forcing every caller to bolt on its own secondary indexing scheme.
Occupants matter architecturally because they are what give ScanCell its purpose.
Instead of scanning every voxel in a region and then asking each voxel for a list of entities, GridForge can:
- find relevant scan cells
- inspect only active cells
- enumerate occupants from those cells
- apply optional group or occupant predicates
That is why the occupant system is tightly coupled to scan-cell activation state.
Use GridOccupantManager.TryRegister(world, occupant) when:
- you have the owning
GridWorld - the occupant already knows its world-space
Position - you want the manager to resolve the correct grid and voxel automatically
Use grid.TryAddVoxelOccupant(...) overloads when:
- you already know the target grid
- you already know the target
VoxelorVoxelIndex - you want to avoid repeating global lookup
Use GridOccupantManager.TryDeregister(world, occupant) or TryRemoveVoxelOccupant(...) when:
- you have the owning
GridWorld - cleaning up an entity
- moving an occupant from one voxel to another
- unloading game state tied to a grid
TryDeregister(world, occupant) is especially useful when an occupant has already moved in world space. GridForge removes the occupant from the voxel registrations it is actually tracking rather than relying on the occupant's current Position.
IVoxelPartition is much smaller because partitions do not live in scan cells.
It requires:
-
WorldIndexfor the parent voxel identity -
SetParentIndex(...)so the voxel can assign ownership OnAddToVoxel(...)OnRemoveFromVoxel(...)
A partition is attached directly to a Voxel, not routed through a manager-wide query overlay.
Partitions are stored in a PartitionProvider<IVoxelPartition> on the voxel.
The provider keys partitions by their exact concrete Type, which has two important consequences:
- only one partition of a given concrete type can be attached at once
- two different concrete types can coexist even if they happen to share the same simple type name
This exact-type rule shows up clearly in the tests and is an important part of the mental model.
Voxel.TryAddPartition(...) does more than just store the instance.
It:
- rejects null
- reserves the concrete type key in the provider
- sets the partition's parent index
- calls
OnAddToVoxel(...)
If OnAddToVoxel(...) throws, the voxel rolls the provider state back and logs the failure. That is a good safety property: failed attachment does not leave behind a half-registered partition entry.
Voxel.TryRemovePartition<T>() behaves a little differently:
- it removes the partition from the provider first
- it calls
OnRemoveFromVoxel(...) - if the callback throws, the failure is logged but the partition remains removed
That makes removal resilient and prevents a bad cleanup callback from trapping the voxel in a stale attached state.
Use an occupant when:
- the thing is dynamic
- it should participate in scan queries
- it needs grouped runtime filtering
- it may move, register, deregister, or be tracked across more than one occupied voxel over time
Use a partition when:
- the data is voxel-local metadata or behavior
- you want typed retrieval directly from the voxel
- the concept does not belong in scan results
- the attachment should live with the voxel rather than with a moving entity
If you find yourself inventing a fake occupant just to tag a voxel with metadata, that usually wants to be a partition instead.
- Treating occupants as durable metadata instead of dynamic runtime presence
- Forgetting that blocked voxels do not have vacancy for normal occupant registration
- Re-implementing your own occupancy bookkeeping when GridForge already tracks the voxel/ticket relationship
- Assuming
TryDeregister(...)only works whileoccupant.Positionstill points at the registered voxel - Assuming partitions are resolved by interface hierarchy instead of exact concrete type
- Expecting partition remove callbacks to keep the partition attached when they throw
- Scan Cells and Query Flow for the query side of occupant storage
- VoxelGrid and Voxel Model for voxel-local state and lifecycle
- Diagnostics and Logging for how occupant and partition failures are surfaced