voxel_engine 0.4.0-dev
voxel_engine: ^0.4.0-dev copied to clipboard
The pure-Dart half of the voxel kit: chunk grid, meshing, streaming, physics, world generation, block and item content, signals and a TCP net layer.
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.