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.
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.
A compact but complete simulation core built around a strict 4-byte-per-cell memory layout and chunk-level activity culling.
Simulation, rendering upload, and collision queries run entirely on pre-allocated typed arrays — no per-frame objects, arrays, or closures.
The world is split into 64×64 cell chunks that fall dormant after two quiet sweeps, skipping simulation and GPU upload entirely.
Immovable solids, falling solids, sliding liquids, rising gases, and propagating energy, each with density-aware displacement.
A flat 256×256 lookup table resolves material transformations, probabilities, and yields in constant time.
EngineObjects gain ground support, surface friction, Archimedes buoyancy, and viscous drag when touching the grid.
The entire world compresses into a compact binary stream for instant save, load, and network replication.
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>
Every cell is exactly 4 contiguous bytes, packed so a whole cell can be copied or compared with a single 32-bit read.
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.
Nine materials are registered automatically; custom ones register at any free id from 9–255 via registerMaterial().
| Material | Archetype | Density | Notes |
|---|---|---|---|
| Bedrock | Immovable solid | 100,000 | Indestructible in normal play |
| Stone | Immovable solid | 2,700 | Dissolved by acid, destroyed by strong blasts |
| Sand | Falling solid | 1,600 | Piles at a natural angle of repose |
| Water | Sliding liquid | 1,000 | Dispersion rate 4 — spreads far |
| Oil | Sliding liquid | 800 | Floats on water, highly flammable |
| Fire | Propagating energy | 0 | 30-step lifetime, decays into Smoke, emits light |
| Smoke | Rising gas | 0.5 | 60-step lifetime, decays into Air |
| Acid | Sliding liquid | 1,200 | Dissolves Stone and Sand into Smoke |
The full public surface, every member documented with JSDoc in the source for inline IDE intellisense. See README.md for extended examples.
initGranularEngine(config): GranularEngineAllocates buffers, registers default materials/reactions, and initialises the renderer.
updateGranularEngine(): voidAdvances the simulation by the configured number of sub-steps.
renderGranularEngine(): voidUploads dirty chunk regions and draws the terrain aligned to the LittleJS camera.
paintCircle(worldX, worldY, radiusWorld, materialId): voidStamps a filled circle of a material into the grid.
paintLine(x0, y0, x1, y1, radiusWorld, materialId): voidSweeps a thick brush stroke between two world points.
createExplosion(worldX, worldY, radiusWorld, power = 1.0): voidCarves a crater, propels debris, ignites flammables, pushes nearby EngineObjects, and triggers camera shake.
sampleWorld(worldX, worldY): objectReads material and cell state at a coordinate (reused result object, zero allocation).
raycastWorld(x0, y0, x1, y1): objectCasts a ray through the grid, returning the first hit cell and surface normal.
registerMaterial(config): numberRegisters a custom material definition and returns its id.
registerReaction(actorId, targetId, yieldActor, yieldTarget, probability, yieldLife?): voidRegisters a constant-time material interaction rule.
serializeGrid(): Uint8ArrayCompresses the world into a lossless RLE binary stream.
deserializeGrid(bytes): booleanRestores world state from a stream, resizing and waking all chunks as needed.
GranularEngineThe orchestrator instance exposed as the granularEngine singleton.
GranularPhysicsBridge.resolveEntityCollision(entity)Resolves ground contact, friction, buoyancy, and drag for a LittleJS EngineObject.
GranularGridBuffer, ChunkManager, MaterialRegistry, ReactionMatrixLower-level building blocks available for advanced or standalone use (see source JSDoc).