voxel_engine 0.5.0-dev
voxel_engine: ^0.5.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.
Changelog #
0.5.0-dev #
Breaking
-
A
ChunkMeshResultbuilt by hand passesvisibility:(OC1, below). -
The chunks sight can reach from the camera (OC2).
SectionOcclusion(new, incore) searches breadth-first from the camera's section over a window of chunks, crossing a section only between faces itsChunkVisibilityjoins and never against a direction the path already took; a chunk with no visibility is crossed as open.update(camera, section, min:, max:)reruns only when one of those changed ormarkChangednamed a chunk in the window, and says whether it ran;reachedis the chunks it found.SectionOcclusion.sectionAt(y)gives a camera's section, clamped into the world. -
A chunk says which faces of each section its open cells join (OC1).
ChunkVisibility(new, incore): eight 16-tall sections, one 15-bit mask each of the face pairs (negX…posZ, opposite =face ^ 1) that one connected group of non-opaque cells touches;connects(section, faceIn, faceOut),pairBit,pairsOf, andChunkVisibility.openfor a chunk with nothing to occlude.ChunkMesherfloods each section of the chunk's own cells after the fill, on per-isolate scratch, andChunkMeshResult.visibilitycarries it, from a worker too. -
A dungeon's rooms are joined (KL-025). Each room's box used to brick over the doorway of the corridor that led into it, so the shaft reached the first room only.
Dungeonnow opens a two-high doorway in the west wall of the second and last rooms. Every world's dungeons open, saved ones too: a save keeps only its edits over what the seed draws.
0.4.0-dev #
Breaking
-
StructureSpec(name, structure, regionChunks:, chance:, biomes:)takes aStructure; a build function is aCustomStructure(build, radius:, depth:, blocks:)(VA11). -
A dungeon and a mine say where their parts are (KL-022).
DungeonPlan.of(dungeon, site, roll)(new): the floor rolled from the site, its threerooms(DungeonRoom, new: acentreover the floor and ahalf), the guard rooms'spawnersand the last room'schest, in world cells.MinePlan.of(mine, site, roll)(new): the corridor'sfloor, its rolledlength, itsend, thechestthere and thespawnerof the one mine in three that has one.DungeonandMinedraw from their plan, so a game reads the cells the generator drew without scanning the world. The roll is the drawing'sStructureSite.roll, withStructureSite.placed(new) as the site, or a game'sSpecGenerator.rollOf(site)(new), the same hash. Both draw the same blocks as before. -
WorldGenSpec.plaza(new) ofPlaza(new): a square of open-sky ground pressed flat, a showroom's floor (the app's playground, VA-Zl5). Its columns stand at itsheightin the land biome it names and carve no caves; the ground outside eases back overblend(16) blocks; no tree, plant or cave comes withinclearing(8) of it and no structure's site withinstructureClearing(48). Refused in a cavern or naming a biome the land has not.WorldGenSpec.withPlaza(new) presses one into a world; a world without one generates what it did. -
ChunkMesher.liquidTop(new, 0.875): how high a liquid's top is drawn in a cell with no liquid of its own above, what a game reads to tell an eye over a pool's surface from one under it (VA-Zf). -
Minecraft's world, as rows (VA-Ze).
Biome.flats(new) ofFlats(new:height2,keep0.3,reach9, the app's swamp): a column standing from one toreachblocks over the sea is pressed towardheightover it, wherever the biome declaring it is the one the column would then grow (so a desert tried first keeps its ground). Only a land biome's; a cavern's or a shore's throws.WorldGenSpec.shores(new): biomes tried in order on the beach's columns, the first whose climate holds taking it, else the beach. A cold one withiceis a frozen shore.WorldGenSpec.allBiomes(new) lists the land's, the shores, the ocean and the beach.Temple(new, stock): a step pyramid ofstone, 9 x 9 and five steps, over a 3 x 3 x 3 chamber reached from the south, twochests, alight, aplateon atrap; every furnishing nullable. (The app's fortress stays its ownCustomStructure: its layout is read by its boss, VA-Zl.)SpecGenerator.structureNamed(new). A spec using none of them generates what it did.
-
Minecraft's items, declared as rows (VA-Zd); the engine only declares them, the kit's player does what they say.
ItemType.launcher(new) ofLauncher(new: theshotby name, theammoitem a shot spends or none, acooldown, 0.5 s): an item that shoots.ItemModel.shapeOfdraws one as a bow.ItemType.light(new, 0..15): the light an item gives in hand;ItemRegistry.forBlocksgives a block's item its block's.Food.cures(new): eating it ends every bad effect.StatusEffects.hasBad(new).MiningRules.cuts(new, tool kind → block tags) andMiningRules.cut(new): a tool that cuts a block takes it at once (mineTime0.05), anddropsis true for it.BlockType.bed(new): a bed.BlockType.holdable(new, true; false for a liquid): a block only the world makes (a portal, an open door, a rail's curve) is no item, andItemRegistry.forBlocksleaves it out.ItemShape.shears(new, a stock shape), and a tool of kindshearstakes it.
-
ItemType.glider(new) ofGlider(new:speed,fall,steer, a hang glider's by default): an item that glides, carried in the bag. The engine only declares it; the kit's player glides with it (VoxelAction.glide). -
Inventory.roomForStack(new): how many of a stackputwould take — a new one as many asroomFor, a worn or bonused one all or nothing. A host reads it of a peer's declared bag before it hands a drop over. -
SignalRules.usedInto(new): what a use turns a block into (a lever flipped, a button pressed), null for neither;SignalNetwork.usewrites it. A client, which runs no circuits, reads it to flip a lever as a block edit of its own that the host's circuits answer. -
LootTable.oneOf(new): a table that gives one of its entries or nothing — each entry's chance is its slice of one roll, in order, and what the slices leave gives nothing (a fishing line's catch: 70 % fish, 10 % salmon, ...).LootTable.check(new) throws for slices that sum over 1, androllchecks them;LootTable.oneOf(the flag) tells the two kinds apart. A table built as before rolls each entry by its own chance, unchanged. -
A richer world, declared as rows (VA11). A spec that uses none of them generates what it did before, chunk for chunk (
spec_test.dartpins it).WorldGenSpec.strata(new) ofStratum(block, belowY:)(new): the rock below a height is that block instead ofstone(dark stone in the deep); ores vein it as they vein stone, in a cavern too.Biome.covers(new) ofCover(block, perMille:, minHeight:, maxHeight:, patch:)(new): the surface block where the ground stands in a window of height and a roll hits, one roll perpatchsquare, tried in order — snow on the peaks, gravel on the sea floor, patches of mud. A cavern's floors take them too.Biome.pools(new, aPools(bed:, threshold:, scale:)): one block of the world's water overbedwhere noise runs high on land above the sea, never beside lower ground or over a cave, so the water stays put. Nothing grows in a pool.TreeSpec.weight(new, 1): a biome picks its trees by weight.TreeSpec.belowY(new): a tree grows only on ground below it.TreeSpec.oak/spruce/palmtake both.Plant.maxHeight(new;heightwhen null): a plant standsheighttomaxHeighttall (a cactus, two or three).Plant.spread(new, 0..4): that many neighbours (+x, +z, -x, -z) level with it may grow one too, each on a coin — a patch of melons, whole across chunk borders.Plant.byWater(new): it grows only beside water at the surface (the sea, a river, a pool), and a column away from water skips it without spending its share (reeds). A cavern throwsArgumentErrorfor pools, spreading and water-seeking plants.- Breaking:
StructureSpec(name, structure, regionChunks:, chance:, biomes:)takes aStructure(new): how far it reaches (radius), how deep it sits (depth), how far trees stay off (clearing), itsblockNamesandbuild(site). A build function is aCustomStructure(build, radius:, depth:, blocks:)(wasStructureSpec(name, build:, radius:, depth:));StructureBuildmoved tostructure.dart. A structure's blocks are inWorldGenSpec.blockNames, so a world fails to compile naming a missing one. - The stock structures (new), every block taken by name, the furnishings nullable:
Dungeon(three rooms under the ground, guarded, a ladder shaft up),Tower,Well,Camp,Ruins,Mine(a head frame, a ladder down to a corridor atfloorYwith supports, lights, a rail to its chest,veinsin its rock shell, liquids sealed off) andVillage(huts on a ring around a well, doors to the well, paths, an optionalVillageFarm). - Structures keep apart: a site within reach (the two radii) of an earlier structure's
candidate is dropped. Two structures of one name throw
ArgumentError. StructureSite.hashAt(a cell's hash),worldY,isRock(the world's stone, strata and ores) andisOpen(air, its water or lava);StructureSite.leveltakesfloor:(the floor's dy, 0 by default).- The generator works out each column's height and biome once a chunk (the surface, the plants and the pools share them) where it used to twice.
-
WorldGenSpec.cavern(aCavernSpec, new; null by default): a world that is one great cave (VA10) — a slab of itsstonebetween a bedrockfloorandroof(7 and 100), opened by 3D noise overthreshold(about two fifths open) and stretched byscale, its sea (water, lava in an underworld) in the open cells up toseaLevel, open sky above the roof. Each floor above the sea takes its column's biome'stopover itsunderand its plants;CavernSpec.hangs(Plants) hang from the ceilings; ores vein the rock; structures stand on the lowest floor above the sea, which is whatsurfaceHeightanswers there. A cavern throwsArgumentErrorfor biomes with trees, caves on, or a roof at the top of the world. -
DimensionGenerator(new): a world of several dimensions, oneWorldGenSpeceach, compiled for one seed; theChunkGeneratorthat hands chunk (x, z) of dimensiondtospecs[d]'s generator, for the worker isolates.SpecGenerator.generateInstill ignores its dimension: one spec is one dimension. -
Biome.precipitation(aPrecipitation, new:rain, the default,snowornone): what falls on a biome when the weather turns (VA9). The generator does not read it. -
Inventory.put(stack)(new): a new stack tops up stacks asadddoes, a worn or bonused one takes an empty slot whole asaddStackdoes; returns what did not fit. -
What an item looks like (VA5):
ItemType.shape(anItemShape, new, null by default) andItemModel.of(item, blocks, items)(new), its voxels built once a look — two items that look alike are one model, so a renderer makes one mesh or icon for both. A shape is one of the kit's (ItemShape.block, the item's block as the mesher draws it: a cube, a slab, stairs, a torch, a sprout, a flower, a door, a fence, a ladder, a wire, a rail;pickaxe,axe,shovel,hoe,sword,bow,lump,gem,cap,tunic,pail, theStockItemShapes, painted in the item's colours; a full pail is the empty one's colour with its liquid's in it) or aCustomItemShapeof the game's own boxes of voxels. An item that declares none takes one from its row (ItemModel.shapeOf): a block's item its block, a tool of the five stock kinds its kind, food a lump, armour a tunic, a bucket a pail, anything else a gem; a tool of any other kind throwsArgumentErrorand must declare its shape. Each model says how it is held (ItemGrip, new:blockagainst the palm,flatoruprightout of the fist), where (origin), at what size (scale,ItemShape.voxelSizefor the stock ones) and itsbounds. -
ItemType.bucket(aBucket, new):Bucket.emptynames, per liquid kind, the full item it becomes once it scoops that liquid's source;Bucket.fullnames the source block it pours and the item it becomes once poured (VA2). -
BlockTypesays what a block does on its own (VA2), all off by default:falls(sand, gravel),support(aSupport, new:Support.below, on any solid block or on the ones named inon, orSupport.side, an opaque block on one of the four sides),onWall(the block placed instead against a wall: a torch's wall torch) andloot(aLootTablerolled for its drops instead ofdrop),facing(aFacing, new:Facing.compass, four variants by the side the placer looks toward, orFacing.axis, two by the axis they look along),tall(two cells, both halves this block),usedInto(the block a use turns it into: a door opens, its open state closes),grows(aGrowth, new: the next stage of a crop, the seconds of light it takes and the least light it grows in) andturnsWith(tool kind to the block that tool turns it into: a hoe tills) andstorage(aStorage, new: the slots a chest holds and theLootTablea generated one is found with).BlockRegistry.standsanswers whether a block would stay at a cell, and the registry refuses a row naming a block that does not exist. -
ItemType.food(aFood: hunger it fills, health it gives back, an effect it starts for some seconds at some power, an item it leaves behind) andItemType.armor(anArmor: the slot it is worn in and the points it is worth), both new, both null for an item that is neither eaten nor worn. The comment that told a game to subclassItemTypefor food and armour is gone: they are fields now, so the kit's player reads them from any game's rows. -
ChunkStreamer.putChunkstores a volume generated elsewhere (a test's floor), with the edits recorded for it written over it, as a generation landing would. Since 0.2.0-dev madechunksread-only it was the one way in that was missing: a world built without jobs had nowhere to put its chunks. -
A
NetConnectionwhose peer hangs up while a write is in flight closes (donecompletes, and aNetHostdrops the peer throughonLeave), where the write'sSocketException(Broken pipe) used to reach the zone unhandled, which ends a plain Dart process. A message sent that way is lost, as it was.
0.3.0-dev #
ChunkWorkerPool.defaultWorkersis two thirds of the cores (workersFor, new), no longer all of them but one: 8 on an M2 Pro instead of 11, 5 on a Galaxy S24 instead of- Past that the window's fill time did not fall (on the phone 735 ms with 5 workers, 728 with 7), each job ran slower (a mesh 10.6 ms at 4 workers, 15.8 at 7), and the extra isolates took the cores the UI and raster threads need.
ChunkStreamersends a chunk's mesh job when the last generation of its ring lands, no longer at the nextupdate: the window's fill was 387 → 350 ms in a prototype (M2 Pro, radius 6), and the ring's copy leaves the frame for the message handler. Only the nine chunks around the one that landed are looked at, so jobs that answer at once (a headless world's) still fill over several updates.ChunkMesher.buildno longer allocates on every cell of its loop: the ladder, the rails and the fence drew with closures declared in the loop's body, which captured the cell's variables, so Dart allocated a context for them on every one of a chunk's 32768 cells (and boxed the cell's coordinates into it), whatever the cell held. They are methods now; the meshes are the same, vertex for vertex.ChunkMesherkeeps the four surfaces it fills from onebuildto the next: each build allocated 4 × 216 KB of growable arrays, and again each time one doubled, all of it garbage once the result was copied out. A result's arrays are exact-size copies now, which outlive the next build as before.ChunkMesher.buildWith(new) meshes likebuildand hands the result to a callback, its arrays the mesher's own and valid only inside it.ChunkWorkerPool's workers use it: they copy the mesh into transferables anyway, so the exact-size copiesbuildmakes (~0.3 MB for a chunk of the example's hills, and 64 KB of light) are no longer made.eulerYXZInto(new) writeseulerYXZ's rotation into a quaternion it is given, the product of the three axis rotations worked out by hand;eulerYXZis built on it. A rig part posed every step no longer allocates eight vectors and quaternions for it.NetHost.broadcastencodes its message once for every peer, no longer once per peer, and each send is one write, no longer two (writelnwrote the line, then the newline): a host's 20 Hz state of 40 creatures cost it 369 µs with 4 peers and 797 with 8 (M2 Pro), against 106 and 122 now.EncodedMessage(new) is a message encoded once;NetConnection.sendEncoded(new) sends one to a connection.
0.2.0-dev #
ChunkMeshermerges cube and liquid faces greedily: coplanar neighbours of the same block, light, AO corners and lowered top become one quad, across a direction only where the AO does not change along it, so the interpolated colour is what the unit faces had. The kit example's radius-6 window meshes 114,193 faces where it meshed 185,130 (solid −31%, liquid −98.5%). Breaking: the per-block colour variation is no longer inMeshSurface.colors(it made every face different); a renderer computes it from the cell with the newChunkMesher.voxelTint, as voxel_scene's terrain shader does. Theglowsurface, meant for an unlit material, keeps it baked and unmerged.
0.1.2-dev #
- Formatted by
dart formatat the 120 columns the code is written at (formatter: page_width: 120inanalysis_options.yaml); pana took 10 pub points for formatting. Pathfinder.findkeys its cells by an int (the offset from the start, packed) and keeps its nodes in lists reused from one search to the next, allocating nothing per cell it opens; the paths are the same, the search about 1.6 times faster. APathCostscallback that searches again throws aStateError. With 40 creatures (mobs:6, M2 Pro) a step's p99 goes from 8.7 ms to 4.7.ChunkStreamer.getBlockXYZandlightAtfind a chunk by shifts and an int key (ChunkStreamer.keyOf) instead of a double division and a record key: 41 → 14.5 ns and 66 → 32 ns a call.ChunkStreamer.chunkAtXZreturns a column's volume the same way.ChunkSizegainsshiftX/shiftZandmaskX/maskZ.ChunkStreamer.chunksis read-only (anUnmodifiableMapView): the streamer keeps it in step with the int-keyed copy its queries read.
0.1.1-dev #
- No code change. The README says why the web is not a target: streaming spawns
isolates and
net.dartis TCP sockets, throughdart:isolateanddart:io.
0.1.0-dev #
First version. It is the five pure-Dart packages that came
before it — voxel_core, voxel_worldgen, voxel_content, voxel_signals
and voxel_net — merged into one package with a library per subject. No code
changed in the move; package:voxel_core/voxel_core.dart became
package:voxel_engine/core.dart, and so on for the other four.
core.dart:VoxelBlockTable,ChunkGenerator,ChunkWorkerPool,ChunkStreamer,ChunkMesherwith light and ambient occlusion,VoxelBody,VoxelRaycast,Reach,LiquidFlow,Pathfinder,VoxelModel,EditDeltaCodec.worldgen.dart:WorldGenSpeccompiling to aChunkGenerator— biomes by climate, terrain recipes, caves, ores, trees, plants, structures, andFastNoiseLite.content.dart:BlockRegistry,ItemRegistry,MiningRules,Inventory,RecipeBook,LootTable,StatusEffects.signals.dart:SignalNetworkandSignalRulesfor wires, levers, lamps, doors and pistons;RailGraphfor rails that lay themselves.net.dart:NetHost,NetConnectionandconnectToHostover TCP.voxel_engine.dartexports all five.- A sixth example,
voxel_engine_example.dart, runs the subjects together. - Dartdoc describes the behaviour in its own words instead of by reference to another
game (
ItemType.tier,VoxelModel,VoxelBody.wading).