-
-
Notifications
You must be signed in to change notification settings - Fork 0
GridTracer and Coverage
GridTracer is the utility layer that translates world-space shapes into grid-space coverage. It is one of the most reusable parts of the architecture because several higher-level systems depend on the same answer:
"Which cells does this world-space region touch?"
That question shows up in:
- line tracing
- region coverage
- blocker application
- scan-region enumeration
GridTracer currently exposes four main entry points:
| API | Output | Best Used For |
|---|---|---|
TraceLine(Vector3d, Vector3d, ...) |
IEnumerable<GridVoxelSet> |
Voxelized line traversal in 3D |
TraceLine(Vector2d, Vector2d, ...) |
IEnumerable<GridVoxelSet> |
Voxelized line traversal in 2D projected onto Y=0 |
GetCoveredVoxels(...) |
IEnumerable<GridVoxelSet> |
Bounds-based voxel coverage across one or more grids |
GetCoveredScanCells(...) |
IEnumerable<ScanCell> |
Bounds-based scan-cell coverage for occupant queries |
Three of those return GridVoxelSet, which groups results by grid instead of flattening them immediately.
Coverage often crosses more than one grid. Returning grouped results preserves that architectural reality.
GridVoxelSet keeps:
- the
VoxelGridthat owns the covered cells - the list of covered voxels for that grid
That makes it easy for callers to:
- apply per-grid mutations
- reason about cross-grid boundaries
- avoid immediately losing the owning-grid context
Even though the APIs look different, they follow a similar structure:
world-space input
-> snap to voxel-aligned bounds or points
-> find candidate grids through GlobalGridManager
-> enumerate covered voxels or scan cells
-> group or yield results
The manager provides candidate grid discovery. The tracer provides coverage enumeration inside those candidates.
The line tracer is used when a continuous path in world space needs to become a discrete voxel path.
At a high level it:
- snaps the start and end
- computes the dominant-axis step count
- iterates intermediate trace positions
- resolves those positions to voxels
- groups results by grid
- optionally appends the end voxel explicitly
This makes it useful for:
- ray-like traversal
- line-of-sight style checks
- path visualization
- editor and debug tooling
GetCoveredVoxels(...) answers a slightly different question:
"Which voxels fall inside this snapped bounding region?"
The method:
- snaps min and max to voxel alignment
- finds candidate grids through the spatial hash
- iterates the voxel-aligned positions inside the covered range
- resolves valid voxels in each grid
- deduplicates results
This is the main coverage primitive behind blockers and other region-based bulk operations.
GetCoveredScanCells(...) is the scan-oriented sibling of voxel coverage.
Instead of yielding every touched voxel, it:
- snaps the bounds
- finds candidate grids
- converts covered bounds into scan-cell coordinate ranges per grid
- resolves scan-cell keys
- yields unique scan cells across the relevant grids
This is the architectural bridge between world-space regions and GridScanManager's occupant queries.
Blocker and BoundsBlocker do not perform their own region-to-voxel logic. They delegate to the tracer.
The high-level blocker flow is:
blocker bounds
-> GridTracer.GetCoveredVoxels(...)
-> per-grid covered voxel sets
-> TryAddObstacle / TryRemoveObstacle on each covered voxel
That reuse is important because it keeps coverage rules in one place instead of scattering region math across multiple mutation systems.
Coverage APIs are built with multi-grid worlds in mind.
That means:
- one traced region can touch multiple registered grids
- grouped results preserve which grid each voxel came from
- cross-grid blockers and diagnostics work without special-case code in the caller
This is one of the main reasons the tracer belongs in the architecture as a shared utility rather than a helper local to blockers.
Tracing APIs operate on voxel-aligned space, not arbitrary floating positions.
Important ideas:
- incoming bounds or endpoints are snapped to voxel alignment
- coverage is ultimately evaluated in discrete cell steps
- optional padding can expand the region before snapping
If a result seems larger or smaller than expected, the first debugging step is usually to inspect the snapped bounds rather than the original inputs.
This is one of the most important practical details on the page:
-
GridVoxelSet.Voxelsis backed by pooled storage - the tracer releases those pooled lists after yielding each grouped result
That means callers should treat grouped voxel lists as transient and consume them immediately inside the enumeration.
Good pattern:
foreach (var covered in GridTracer.GetCoveredVoxels(min, max))
{
foreach (Voxel voxel in covered.Voxels)
{
// use immediately
}
}Risky pattern:
- storing
covered.Voxelsfor later use after enumeration completes
Use:
-
TraceLine(...)when the shape is fundamentally a line -
GetCoveredVoxels(...)when you need direct per-voxel processing -
GetCoveredScanCells(...)when the goal is occupant scanning instead of cell mutation
If your logic begins to look like "iterate every voxel in this region and then look up scan cells," you are often starting from the wrong tracer API.
- Forgetting that traced results may span multiple grids
- Holding onto pooled voxel lists beyond immediate consumption
- Expecting unsnapped floating inputs to map one-to-one to final covered voxels
- Using voxel coverage when scan-cell coverage would be cheaper for occupant queries
- Architecture Overview for where the tracer fits in the full system
- Scan Cells and Query Flow for the scan-oriented consumer of scan-cell coverage
- Blockers and Obstacles once drafted, for the main mutation system built on top of voxel coverage