voxel_engine
The engine under a voxel game, in pure Dart: a chunk grid, meshing with light
and ambient occlusion, streaming on worker isolates, physics, rays, world
generation, blocks and items, circuits and a network layer. It draws nothing —
a renderer (voxel_scene) takes the meshes, and voxel_game ties everything
into a playable game.
Status: 0.4.0-dev, beta. The API can still change.
The five subjects
Each one is a library of its own. Import only what you use, or
package:voxel_engine/voxel_engine.dart for all of it.
| Library | What it does | Depends on |
|---|---|---|
core.dart |
chunk grid, mesher, streaming, physics, rays, liquids, pathfinding, the edit save format | — |
worldgen.dart |
biomes, terrain, caves, ores, trees, structures, as a declared spec | core |
content.dart |
blocks and items by string id, mining, inventory, crafting, loot, effects | core |
signals.dart |
wires, levers, lamps, doors, pistons, and rails that lay themselves | core |
net.dart |
a TCP host and its clients, exchanging JSON messages | — |
Install
dependencies:
voxel_engine: ^0.4.0-dev
Dart SDK ^3.13.0. No Flutter dependency: it runs in dart test, on a server
and on worker isolates. Not on the web: streaming spawns isolates and net.dart is TCP
sockets, through dart:isolate and dart:io, which a browser does not have.
Usage
core — a world you can walk in
-
Describe your blocks. Id 0 is air.
final table = VoxelBlockTable(const [ VoxelBlockDef(shape: BlockShape.cube, solid: false, opaque: false, r: 0, g: 0, b: 0, a: 0), // air VoxelBlockDef(shape: BlockShape.cube, solid: true, opaque: true, r: 0.4, g: 0.4, b: 0.45), // stone ]); -
Write a generator. It fills one chunk and must be pure in the position.
class Flat implements ChunkGenerator { const Flat(); @override Uint8List generateIn(int cx, int cz, int dimension) { final blocks = Uint8List(ChunkSize.volume); for (var z = 0; z < ChunkSize.sizeZ; z++) { for (var x = 0; x < ChunkSize.sizeX; x++) { for (var y = 0; y < 40; y++) { blocks[ChunkSize.index(x, y, z)] = 1; } } } return blocks; } } ChunkGenerator makeGenerator() => const Flat(); // top-level: sent to each isolate -
Stream chunks around a point. The sink receives each finished mesh.
final pool = ChunkWorkerPool(ChunkWorkerConfig(generator: makeGenerator, table: table)); await pool.start(); final streamer = ChunkStreamer(table: table, sink: mySink, loadRadius: 4) ..jobs = pool ..updateAround((x: 0, z: 0)); // Once a frame: streamer.update(); -
Read and edit the world.
streamer.getBlockXYZ,streamer.setBlock,VoxelRaycast.solid(...)for aiming,VoxelBodyfor anything that walks. -
Save only the edits with
EditDeltaCodec; the generator rebuilds the rest.
content — blocks, items and what the player carries
-
Declare blocks. Air first; the order numbers them for the engine, and
blocks.tableis theVoxelBlockTablestep 1 above asked for. A row also says what the block does, for a game to act on: itfalls, it needs asupport(Support.below,Support.side;blocks.standsanswers whether it would stay), it has anonWallform, it drops a rolledloot, it turns to face the placer (Facing.compass,Facing.axis), it is two cellstall, a use turns it into another (usedInto), itgrowsinto its next stage (aGrowth), a tool turns it into another (turnsWith), it stores things (aStorageof slots, and the loot a generated one holds).final blocks = BlockRegistry(const [ BlockType('air', color: 0, solid: false, hardness: -1, drop: ''), BlockType('stone', color: 0x7F7F84, hardness: 1.5, tool: 'pickaxe', tier: 1, drop: 'cobblestone'), BlockType('cobblestone', color: 0x6E6E70, hardness: 2.0, tool: 'pickaxe'), ]); -
Declare items. Every block you can hold is an item already (a block only the world makes says
holdable: false). One that is eaten says what it does infood(aFoodthatcuresends the bad effects), one that is worn where inarmor, one that carries a liquid inbucket(Bucket.empty,Bucket.full), one that shoots inlauncher(Launcher(shot:, ammo:)), one that lights the way in hand inlight, one that glides inglider. What it looks like is itsshape, or one read off the rest of its row;ItemModel.of(item, blocks, items)builds its voxels.final items = ItemRegistry([ ...ItemRegistry.forBlocks(blocks), const ItemType('wooden_pickaxe', color: 0xB08850, tool: 'pickaxe', tier: 1, stack: 1, durability: 60), const ItemType('apple', color: 0xD03A2A, food: Food(hunger: 4)), const ItemType('wool_tunic', color: 0xE8E8E8, stack: 1, armor: Armor('chest', 3)), const ItemType('wool_cap', color: 0xE8E8E8, stack: 1, armor: Armor('head', 1), shape: ItemShape.cap), ]); -
Mine.
const MiningRules().mineTime(block, tool)gives seconds, or -1 when that tool cannot break it; a tool whose kindcutsa block's tag takes it at once (shears through leaves). -
Carry things.
final bag = Inventory(stackSize: (id) => items[id].stack, maxDurability: (id) => items[id].durability) ..add('cobblestone', 10); -
Craft.
final book = RecipeBook(const [Recipe('stone', 1, {'cobblestone': 1}, station: 'furnace')]); book.craft(book.available('furnace').first, bag); -
Roll loot. A
LootTablerolls each entry by its own chance (a chest, a mob's drops); aLootTable.oneOfgives one entry or nothing, each chance a slice of one roll (a fishing line's catch).check()throws for slices summing over 1.const catches = LootTable.oneOf([LootEntry('raw_fish', 1, 1, 0.7), LootEntry('stick', 1, 2, 0.15)]); final got = catches.roll(math.Random()); // raw_fish 70 %, sticks 15 %, nothing 15 %
worldgen — a world declared instead of coded
-
Give it your block ids. A
BlockRegistryalready has them:blocks.ids. -
Declare the world.
const world = WorldGenSpec( biomes: [ Biome('forest', top: 'grass', under: 'dirt', climate: Climate.wet, trees: [TreeSpec.oak(log: 'log', leaves: 'leaves')], treeChance: 90), Biome('plains', top: 'grass', under: 'dirt'), ], beach: Biome('beach', top: 'sand'), ores: [Ore('coal_ore', share: 0.11)], ); -
Compile it with a seed and use it as the
ChunkGeneratorof step 2 above.ChunkGenerator makeGenerator() => world.compile(blocks.ids, 2024); -
Ask it about the world without generating chunks:
surfaceHeight(x, z),biomeAt(x, z),structuresNear(cx, cz). -
Add structures: a stock one (
Dungeon,Tower,Well,Camp,Ruins,Mine,Village,Temple) takes its blocks by name; your own is a build function. Structures keep apart: a site within reach of an earlier one's is dropped. A dungeon's and a mine's parts are where their plan says, so a game finds them without reading the world:DungeonPlan.of(dungeon, site, generator.rollOf(site)).spawners,MinePlan.of(...).chest.void tower(StructureSite s) { s.level(-2, -2, 2, 2, 'stone', clearTo: 10); s.fill(-2, 0, -2, 2, 8, 2, 'stone', hollow: true); } // structures: [ // StructureSpec('well', Well(rim: 'cobblestone', water: 'water', posts: 'fence', roof: 'planks')), // StructureSpec('tower', CustomStructure(tower, radius: 3, blocks: {'stone'}), biomes: ['plains']), // ] -
Dress the ground with rows:
strata(dark stone in the deep), a biome'scovers(snow above a height, patches of mud),pools(water over mud), trees byweight, and plants that grow taller (maxHeight), in patches (spread) or by water (byWater), low ground pressed toward the sea (flats, a swamp), and biomes of their own on the beach's columns (shores, a frozen one). -
Other kinds of world. A
cavern(CavernSpec) is one great cave between a bedrock floor and roof; aplaza(Plaza, orwithPlaza) is a square of open ground pressed flat, a showroom's floor; aDimensionGeneratorhands each dimension's chunks to its own spec, for the worker isolates.
signals — circuits and rails
-
Tell the network about every edit. It needs a
VoxelEditor, and your world callstouchwhenever a block changes:@override bool setBlock(IVec3 cell, int id) { final old = getBlockXYZ(cell.x, cell.y, cell.z); // ...write the block... signals.touch(cell, old, id); return true; } -
Declare the circuit blocks.
final signals = SignalNetwork(world, SignalRules( wireOff: wireOff, wireOn: wireOn, sources: const {leverOn}, toggles: const {leverOff: leverOn, leverOn: leverOff}, reactions: { lampOff: SignalReactions.swap(lampOff, lampOn), lampOn: SignalReactions.swap(lampOff, lampOn), }, )); -
Tick it every frame with
signals.tick(dt), and callsignals.use(cell)when the player clicks a lever. -
Rails: name each rail shape once, then place rails through the graph.
final rails = RailGraph({railNs: const RailVariant('rail', 'ns'), railEw: const RailVariant('rail', 'ew') /* ... */}); rails.place(world, cell, railNs); // turns to meet its neighbours final next = rails.nextCell(world, cell, RailGraph.e);
net — two games in one world
-
Host.
final host = await NetHost.bind(port: 7777); host ..onJoin = ((peer) => print('player ${peer.id} joined')) ..onMessage = ((peer, message) => host.broadcast(message, except: peer.id)) ..onLeave = ((peer) => print('player ${peer.id} left')); -
Join from another app or machine.
final client = await connectToHost('192.168.0.10', port: 7777); -
Talk. Messages are JSON objects;
tsays what they are.client.send({'t': 'pose', 'p': [12.5, 64.0, -3.0]}); client.listen((message) => print(message['t'])); -
Leave.
await client.close();runs the host'sonLeave, andawait host.close();drops everyone.On macOS, a Flutter app that hosts or joins needs the
com.apple.security.network.serverandcom.apple.security.network.cliententitlements.
Examples
Six files, each runnable on its own:
dart run example/voxel_engine_example.dart # the subjects in one run
dart run example/core_example.dart # streaming, rays, edits, a falling body
dart run example/worldgen_example.dart # a text map of a four-biome world, then 25 chunks
dart run example/content_example.dart # blocks, mining, a bag, crafting, loot, effects
dart run example/signals_example.dart # a lever lights a lamp; a cart rides a rail line
dart run example/net_example.dart # a host and a client over localhost
example/voxel_engine_example.dart is the
one to read first: content declares the blocks, worldgen builds a world out of
the same ids, core meshes a chunk and casts a ray into it, and content mines
what the ray hit into a bag.
Libraries
- content
- Game content on
package:voxel_engine/core.dart: block and item registries by string id, tags, mining times, inventories, crafting recipes, loot tables, status effects, what eating, wearing, pouring, gliding or shooting with an item does, and what an item looks like. - core
- The engine core: chunk grid, meshing with light and ambient occlusion, chunk streaming on isolates, voxel physics and rays. Renderer-agnostic. Every other subject of this package is written on it.
- net
- The network layer: a host and clients over TCP exchanging newline-delimited JSON messages. It depends on nothing else in this package.
- signals
- Signals and rails on
package:voxel_engine/core.dart: levers, buttons, plates, wires with decaying strength, lamps, doors, pistons and powered rails as declared block roles; and rails that orient themselves. - voxel_engine
- The whole pure-Dart engine in one import.
- worldgen
- Declarative world generation on
package:voxel_engine/core.dart: noise, terrain recipes, biomes, caves, ores, trees and structures, as aChunkGenerator.