Skip to main content

Crate bevy_stigmergy

Crate bevy_stigmergy 

Source
Expand description

Stigmergy: coordination through the environment.

Agents write into a shared field and read it back, so they coordinate through the world rather than by messaging each other (Holland & Melhuish, “Stigmergy, self-organization, and sorting in collective robotics”, 1999; Tang, Liu & Pan, ACO review, IEEE/CAA JAS 2021 — deposit + evaporation + positive feedback). Each channel is a scalar grid over cells, and the three standard influence-map operations are placement (deposit), diffusion (blur to neighbours), and query (sample/gradient) — Lewis, “Escaping the Grid”, Game AI Pro 2 Ch.29.

The field is computed once and shared by every agent (Mark, “Modular Tactical Influence Maps”, Ch.30), which is where emergent group behaviour comes from: nobody negotiates, and a crowd still converges on a trail, disperses from a threat, or recruits toward an alarm.

Two stores are provided:

  • StigGrid — N scalar channels. Concentrations that decay, spread, and can be climbed.
  • RallyGrid — one vector per cell, storing a bearing rather than an amount, for tracking a moving target (Tang et al. 2019).

§What this crate deliberately does not do

It owns no resources, registers no systems, and has no schedule. The caller decides when the field ticks and in what order relative to everything else — which is the only useful arrangement, because “deposit before or after the agents think” is a gameplay decision, not a library’s.

It owns no threads either. StigGrid::evaporate_diffuse runs on the thread that calls it, so a Bevy system never parks waiting on another pool; a large grid hands its stencil to the caller’s own pool with StigGrid::evaporate_diffuse_with, and gets the same bits.

It also does not name the channels. A channel table is game content; a library that shipped one would be naming somebody else’s game. Channels are usize indices, and N is yours to pick.

§Cell space, not world space

Every entry point takes an IVec2 cell. The crate does not decide how big a cell is or where the grid sits in the world.

CellFrame is an optional plain value type for the mapping nearly every caller writes anyway — an axis-aligned grid of uniform cells: world_to_cell, cell_centre, the ascending cells() a floor set wants, and covering for a world rectangle. It is a convenience, not a requirement: a caller with a hex sheet, a rotated board or a camera-relative projection ignores it and keeps its own mapping, and nothing in the crate behaves differently either way.

§Determinism

The passes are written to be bit-reproducible, because a simulation that hashes its state cannot afford otherwise:

  • The neighbour sum keeps a fixed E/W/S/N order. Float addition is non-associative, so the order is load-bearing rather than stylistic.
  • The diffusion is a pure stencil over disjoint output slots — each cell reads the previous grid and writes only its own slot — so its result is identical however the pass is split and on however many threads it runs, which is what makes StigGrid::evaporate_diffuse_with safe to hand to the caller’s pool.
  • Skipping rock cells is exact, not an approximation: deposits are floor-masked, 0 · retain is still 0, and the diffusion double-buffer’s rock cells stay 0 across the swap.
  • StigGrid::channels and RallyGrid::cells expose the full grids so a caller can fold the rock-cells-stay-zero invariant into its own fingerprint, not just the floor cells.

Deposits are applied with a non-associative +=, so a caller that produces them in an unordered way (an ECS query, a hash map) must sort its batch before submitting. This crate cannot do that for you: it never sees the batch, only the individual deposits.

Re-exports§

pub use grid::in_grid;
pub use grid::row_major;

Modules§

grid
The grid convention every field in this crate shares, and the shared deposit kernel.

Structs§

CellFrame
An axis-aligned world↔cell mapping: where the grid starts, how wide a cell is, and how many cells there are.
ChannelDef
Per-channel behaviour. The caller supplies one of these per channel, typically from its own config.
DiffuseChunk
One slice of a channel’s pass: a contiguous run of floor cells and the span of the next grid that only they write.
DiffuseChunks
The chunks of one channel’s pass, in ascending cell order: an ExactSizeIterator of DiffuseChunks whose floor-cell counts differ by at most one.
RallyDef
Tuning for the vectorial pheromone (mirrors crate::ChannelDef, but for the vector store).
RallyGrid
A vectorial pheromone map: each floor cell stores a 2-D direction, not a scalar concentration.
StigGrid
N scalar influence channels over one fixed cell grid, row-major y*width + x.