Granular Cellular Automata
for LittleJS

Falling sand, flowing liquids, rising gases, fire, and chemical reactions — a zero-dependency, zero-garbage-collection simulation plugin that drops straight into a LittleJS game.

Zero Dependencies Zero Runtime Allocation Single File Full JSDoc / TypeScript Types MIT Licence

Try It Right Here

The embedded sandbox below is the exact same file shipped in the repository — paint materials, punch a crater with an explosion, and walk a small character across the terrain.

Left click: paint · Right click: explode · WASD/Arrows: move · Space: jump Open Full Screen ↗

What It Does

A compact but complete simulation core built around a strict 4-byte-per-cell memory layout and chunk-level activity culling.

⚙️

Zero-Allocation Hot Path

Simulation, rendering upload, and collision queries run entirely on pre-allocated typed arrays — no per-frame objects, arrays, or closures.

🗑️

Chunk Sleep Culling

The world is split into 64×64 cell chunks that fall dormant after two quiet sweeps, skipping simulation and GPU upload entirely.

🧪

Five Physical Archetypes

Immovable solids, falling solids, sliding liquids, rising gases, and propagating energy, each with density-aware displacement.

O(1) Reaction Matrix

A flat 256×256 lookup table resolves material transformations, probabilities, and yields in constant time.

🏃

LittleJS Physics Bridge

EngineObjects gain ground support, surface friction, Archimedes buoyancy, and viscous drag when touching the grid.

💾

Lossless RLE Save/Load

The entire world compresses into a compact binary stream for instant save, load, and network replication.

Quick Start

Load LittleJS as a classic global script, then load LittleAutomata as an ES module — it reads LittleJS's globals directly, no bundler required.

<!-- index.html -->
<script src="littlejs.js"></script>
<script type="module">
import {
  initGranularEngine, updateGranularEngine, renderGranularEngine,
  paintCircle, createExplosion, MaterialId
} from './littleautomata.js';

function gameInit() {
  initGranularEngine({
    gridWidth: 512, gridHeight: 256, pixelsPerUnit: 16
  });
  paintCircle(0, -2, 6, MaterialId.STONE);
  paintCircle(0, 4, 3, MaterialId.SAND);
}

function gameUpdate() {
  updateGranularEngine();
  if (mouseIsDown(0)) paintCircle(mousePos.x, mousePos.y, 0.5, MaterialId.SAND);
  if (mouseWasPressed(1)) createExplosion(mousePos.x, mousePos.y, 2.0, 1.5);
}

engineInit(gameInit, gameUpdate, () => {}, () => {}, renderGranularEngine);
</script>

Memory Layout

Every cell is exactly 4 contiguous bytes, packed so a whole cell can be copied or compared with a single 32-bit read.

Byte 0materialId
Uint8, 0–255
Byte 1life
Uint8, timer / durability
Byte 2vx
Int8, lateral bias
Byte 3flags
updated · sleeping · variant

Two of these buffers are ping-ponged each sub-step (GranularGridBuffer), and a ChunkManager tracks per-64×64 activity, sleep counters, and dirty render rectangles — see the source for the full design rationale.

Built-In Materials

Nine materials are registered automatically; custom ones register at any free id from 9–255 via registerMaterial().

MaterialArchetypeDensityNotes
BedrockImmovable solid100,000Indestructible in normal play
StoneImmovable solid2,700Dissolved by acid, destroyed by strong blasts
SandFalling solid1,600Piles at a natural angle of repose
WaterSliding liquid1,000Dispersion rate 4 — spreads far
OilSliding liquid800Floats on water, highly flammable
FirePropagating energy030-step lifetime, decays into Smoke, emits light
SmokeRising gas0.560-step lifetime, decays into Air
AcidSliding liquid1,200Dissolves Stone and Sand into Smoke

API Reference

The full public surface, every member documented with JSDoc in the source for inline IDE intellisense. See README.md for extended examples.

Lifecycle

initGranularEngine(config): GranularEngine

Allocates buffers, registers default materials/reactions, and initialises the renderer.

updateGranularEngine(): void

Advances the simulation by the configured number of sub-steps.

renderGranularEngine(): void

Uploads dirty chunk regions and draws the terrain aligned to the LittleJS camera.

World Manipulation

paintCircle(worldX, worldY, radiusWorld, materialId): void

Stamps a filled circle of a material into the grid.

paintLine(x0, y0, x1, y1, radiusWorld, materialId): void

Sweeps a thick brush stroke between two world points.

createExplosion(worldX, worldY, radiusWorld, power = 1.0): void

Carves a crater, propels debris, ignites flammables, pushes nearby EngineObjects, and triggers camera shake.

sampleWorld(worldX, worldY): object

Reads material and cell state at a coordinate (reused result object, zero allocation).

raycastWorld(x0, y0, x1, y1): object

Casts a ray through the grid, returning the first hit cell and surface normal.

Materials & Reactions

registerMaterial(config): number

Registers a custom material definition and returns its id.

registerReaction(actorId, targetId, yieldActor, yieldTarget, probability, yieldLife?): void

Registers a constant-time material interaction rule.

Persistence

serializeGrid(): Uint8Array

Compresses the world into a lossless RLE binary stream.

deserializeGrid(bytes): boolean

Restores world state from a stream, resizing and waking all chunks as needed.

Classes

GranularEngine

The orchestrator instance exposed as the granularEngine singleton.

GranularPhysicsBridge.resolveEntityCollision(entity)

Resolves ground contact, friction, buoyancy, and drag for a LittleJS EngineObject.

GranularGridBuffer, ChunkManager, MaterialRegistry, ReactionMatrix

Lower-level building blocks available for advanced or standalone use (see source JSDoc).