voxel_engine 0.3.0-dev
voxel_engine: ^0.3.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.
example/voxel_engine_example.dart
// The subjects of this package in one run, which is the reason they are one
// package: content declares the blocks, worldgen builds a world out of the same
// ids, core generates, meshes and casts a ray into it, and content mines what
// the ray hit into a bag.
//
// dart run example/voxel_engine_example.dart
//
// Each subject also has an example of its own beside this one, signals and net
// included.
import 'dart:typed_data';
import 'package:vector_math/vector_math.dart';
import 'package:voxel_engine/content.dart';
import 'package:voxel_engine/core.dart';
import 'package:voxel_engine/worldgen.dart';
/// 1. The blocks. Air is block 0; the order numbers them for the engine.
final BlockRegistry<BlockType> 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'),
BlockType('dirt', color: 0x8A5E3B, hardness: 0.5, tool: 'shovel'),
BlockType('grass', color: 0x5C9E3A, hardness: 0.6, tool: 'shovel', drop: 'dirt'),
BlockType('sand', color: 0xDCCB8A, hardness: 0.5, tool: 'shovel'),
BlockType('log', color: 0x6B4F2A, hardness: 2.0, tool: 'axe'),
BlockType('leaves', color: 0x3F8A2E, hardness: 0.2, opaque: false),
BlockType('coal_ore', color: 0x3A3A3E, hardness: 3.0, tool: 'pickaxe', tier: 1, drop: 'coal'),
BlockType.liquid('water', color: 0x3366CC),
]);
/// Everything you can hold: a block each, plus the items that are not blocks.
final ItemRegistry<ItemType> items = ItemRegistry([
...ItemRegistry.forBlocks(blocks),
const ItemType('coal', color: 0x202020),
const ItemType('wooden_pickaxe', color: 0xB08850, tool: 'pickaxe', tier: 1, stack: 1, durability: 60, damage: 2),
]);
/// 2. The world, written in the ids the registry above numbered.
const WorldGenSpec 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',
trees: [TreeSpec.oak(log: 'log', leaves: 'leaves')],
treeChance: 12,
),
],
beach: Biome('beach', top: 'sand'),
ores: [Ore('coal_ore', share: 0.11)],
);
/// The generated chunks, as core's rays and physics read a world: whole chunks
/// on demand, kept in a map. A game streams them on isolates instead — that is
/// `ChunkStreamer`, in `core_example.dart`.
class GeneratedWorld implements VoxelQuery {
/// A world generated by [generator], whose blocks [table] describes.
GeneratedWorld(this.generator, this.table);
/// Fills a chunk from the spec.
final ChunkGenerator generator;
@override
final VoxelBlockTable table;
final Map<ChunkPos, Uint8List> _chunks = {};
/// Chunk [pos], generated the first time it is asked for.
Uint8List chunkAt(ChunkPos pos) => _chunks.putIfAbsent(pos, () => generator.generateIn(pos.x, pos.z, 0));
@override
int getBlockXYZ(int x, int y, int z) {
if (y < 0 || y >= ChunkSize.sizeY) return 0;
final c = ChunkStreamer.chunkOfXZ(x, z);
return chunkAt(c)[ChunkSize.index(x - c.x * ChunkSize.sizeX, y, z - c.z * ChunkSize.sizeZ)];
}
}
void main() {
// The spec is compiled against the registry's numbering: one id table, two
// subjects.
final generator = world.compile(blocks.ids, 2024);
final voxels = GeneratedWorld(generator, blocks.table);
// 3. Meshing a chunk needs its eight neighbours, the way a worker isolate
// gets them, so that a face on a border knows whether it is hidden.
final mesh = blocks.table.mesher().build(0, 0, [for (final offset in ChunkStreamer.ring) voxels.chunkAt(offset)]);
print('chunk (0, 0) meshed into ${mesh.faces} faces in ${mesh.ms.toStringAsFixed(1)} ms');
// 4. A ray straight down from the sky finds the ground.
final hit = VoxelRaycast.solid(voxels, Vector3(12.5, 120, 3.5), Vector3(0, -1, 0), 200)!;
final top = voxels.getBlockXYZ(hit.block.x, hit.block.y, hit.block.z);
print(
'a ray down at (12, 3) lands on ${blocks.idOf(top)} at ${hit.block}, '
'in the ${generator.biomeAt(12, 3).name}',
);
// 5. Breaking it is content again: the tool decides the time and whether the
// block breaks at all, and the drop is not always the block.
const rules = MiningRules();
final pick = items['wooden_pickaxe'];
final deep = voxels.getBlockXYZ(hit.block.x, hit.block.y - 6, hit.block.z);
for (final id in [top, deep]) {
print(
'${blocks.idOf(id)}: ${rules.mineTime(blocks[id], null).toStringAsFixed(2)} s by hand, '
'${rules.mineTime(blocks[id], pick).toStringAsFixed(2)} s with a wooden pickaxe '
'(-1 meaning never), drops ${blocks.dropOf(id)}',
);
}
// 6. The drops go in a bag that knows stack sizes and wears the tool down.
final bag = Inventory(stackSize: (id) => items[id].stack, maxDurability: (id) => items[id].durability)
..add('wooden_pickaxe', 1)
..add(blocks.dropOf(top), 1)
..add(blocks.dropOf(deep), 1);
bag.wear(bag.find('wooden_pickaxe'), 1);
print(
'the bag holds ${blocks.dropOf(top)} and ${blocks.dropOf(deep)}; '
'the pickaxe has ${bag.durAt(bag.find('wooden_pickaxe'))} uses left',
);
}