Scenery
Dress a ground with trees, rocks, bushes and cliff walls placed from a seed, away from routes and clearings: layers, keep-out zones, live coverage, the scenery.cover hook.
Scenery for your ground (world.scenery) #
What it is for #
world.scenery places Library models over the ground of a game: clumps of trees, rocks, bushes and flowers, walls of cliffs lining the routes, and one fitted piece in each box of a built level. The placement is a pure function of a seed, so the same seed gives the same pieces at the same spots. The kit only computes placements; the renderer draws each layer with instancing, so hundreds of trees cost a few draw calls. A layer is placed only when its slot has a model bound.
It keeps off three things: the routes of lanes.routes (a kit such as moba.lanes), the clearings of world.clearings ({ at: [x, z], radius }: bases, camps, buildings), and the ground of keep. It reads the bounds of world.ground.
Turn it on #
In gripforge.game, install the kit after world.ground:
{ "id": "world.scenery", "config": { "seed": 12, "density": 1.3 } }
It requires world.ground and game.engine. Bind models to its slots (scenery_tree, scenery_rock, scenery_bush, scenery_flower, scenery_cliff, scenery_architecture, scenery_landmark); several models on one slot (or variants such as scenery_tree:oak) are mixed. A slot with no model places nothing, so a game can start with trees only.
First example #
<!-- test: kits/game_engine/host/tests/world_scenery.rs::scenery_keeps_off_the_routes_and_the_clearings --> A 120 m ground, one route along X from -50 to 50, one clearing of radius 10 at [0, 30], and two layers:
{ "slot": "cliff", "mode": "line", "height": 5, "clearance": 11, "gap": 5.2 }
{ "slot": "tree", "mode": "scatter", "count": 40, "height": 7, "clearance": 9, "gap": 3.2 }
The same seed returns the same list. The cliffs stand exactly 11 m from the route on both sides, the 40 trees stay at least 9 m from it and out of the clearing, and another seed gives another forest.
Settings and defaults #
| Setting | Type | Default | Meaning |
|---|---|---|---|
seed | integer 0-999999 | 7 | Seed of the placement. |
density | number 0-4 | 1 | Multiplier of every scatter count. |
layers | array (max 48) | cliffs, trees, rocks, bushes, flowers | What to place. |
keep | array (max 800) | none | Ground kept free of scatter and line pieces. |
coverage | number 0-1 | 1 | Share of the pieces shown at start. |
A layer is { slot, mode, height, clearance, gap, count?, cluster?, vary?, openings?, volumes? }:
mode: "scatter":countpieces in clumps (cluster0-1), at leastgapmetres apart.mode: "line": one piece everygapmetres on both sides of every route,clearancemetres from it, turned along the route;openings(0-0.9) leaves a share out, for paths through the wall.mode: "volumes": one piece in each box ofvolumes({ at, size, yaw, y?, colliders? }), scaled uniformly to stay inside its box.collidersare optional solid boxes relative to the volume base; omitted, the piece is decorative.heightis the height in metres a model is brought to.
A keep entry is a disc ({ at, radius }) or a box ({ at, size: [width, depth], yaw }).
A scene of game.engine may set seed, density, layers and keep in layers["world.scenery"]; the placements are recomputed on entry.
Services, events, hooks #
world.scenery(service):placements()lists every piece shown as{ slot, index, x, z, yaw, height };layers()lists the layers in use;coverage(share, slot)sets the share (0-1) of pieces shown, of every layer or of one slot, and returns it.world.scenery.settings(service):keys(),get(key),set(key, value)for the player settingscoverageandcoverage.<slot>(0-1), saved with the other settings.scenery.changed(event):{ count }, emitted when the share shown changed; the renderer drawsplacements()again.scenery.cover(hook):{ slot, from, share }before the share changes. Mutableshare; cancellable (the share stays, nothing is emitted).slotis empty for every layer. Loading a save does not ask.
Each piece has a fixed rank: lowering the coverage hides pieces and raising it brings the same ones back in the same order, so a forest can thin out and grow back. The shares are saved once one has moved.
Hooks and modding #
A game or a mod may override seed, density, layers, keep and coverage, and the live settings coverage and coverage.*. A hook that never lets the forest fall under a third:
{ "id": "forest_survives", "hook": "scenery.cover", "if": [{ "field": "share", "op": "<", "value": 0.33 }], "set": { "share": 0.33 } }
Engines #
The placement is the same Rust logic on the web and, through the kit library, in the native player, Godot and Unity.
When it does not do what you expect #
- Nothing appears for a layer: its slot has no model bound.
- Pieces stand on a path: the route is not published by a kit such as
moba.lanes, orclearanceis too small. - The forest changes between runs: the
seedchanged; keep it fixed. - Collisions are missing:
collidersare only used when a volume carries them, and hidden or unbound pieces have no body.
Limits #
It does not draw anything, and placement is not terrain editing. Collision boxes are conservative bounds of the parts, not triangle-exact, unless a volume gives indexed meshes.
Versions #
Kit 1.9.0. It provides world.scenery 1.1.0 and relies on world.ground@^1 and game.engine@^1.