Ground from the scene

Serve world.ground from the colliders your game already loads (its own terrain, a level mesh, floors) so the character kits stand on it without a world generator; bounded rays, a tiled height cache, invalidation.

Source: module-kits:kits/world_scene_ground/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs world-scene-groundMCP tool: gripforge_docs { "topic": "world-scene-ground" }

The ground of your own scene (world.scene_ground) #

What it is for #

The character kits (character.first_person, character.third_person), NPCs, navigation and placement ask world.ground where the ground is. The world generators serve it with the world they build. When your game brings its own terrain or level (an engine heightmap, a level mesh, floors and platforms), world.scene_ground serves world.ground from those colliders instead: the height at a point is the first static collider a ray cast straight down meets through the physics port. It generates and draws nothing, so you no longer install a world generator you do not use.

Turn it on #

gripforge_gamekit_install   { project_id: "gkp_…", id: "world.scene_ground" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "world.scene_ground",
                              config: { "bounds": [-3000, -2500, 3000, 2500], "rayTop": 800, "rayLength": 1200 } }

It requires game.engine, physics.port and the physics of the host. Remove the other provider of world.ground (a world generator) from the same scene: two providers are reported as an overlap.

First example #

<!-- test: kits/game_engine/host/tests/world_scene_ground.rs::the_ground_is_the_scene_s_own_mesh_and_the_cache_answers_like_the_rays --> With a relief mesh loaded by the game and the config

{ "cellSize": 1, "tileCells": 8, "edgeTolerance": 2 }

the first groundY(3, 4) casts its own ray and answers the height of the mesh there; it also asks for the tile around the point. One tick later the tile is filled, and every groundY inside it is read from the grid, within a centimetre of what a ray gives. Off the mesh, groundY answers null.

Settings and defaults #

SettingTypeDefaultMeaning
bounds[minX, minZ, maxX, maxZ][-1024, -1024, 1024, 1024]limits of the world in metres, read by bounds(), extent() and inside(); groundY answers outside them too
rayTopnumber1000height the downward rays start from: above everything that may be ground
rayLengthnumber2000length of a ray; nothing below rayTop − rayLength is ground
spawn[x, y, z][0, 0, 0]where the player starts
spawnYawnumber0direction the player faces, in radians
spawnOnGroundbooleantrueput the spawn on the ground under it
cachebooleantrueanswer from the height grid where it is known; off: one ray per call
cellSizenumber 0.1 to 161metres between two height samples; match your terrain's resolution
tileCellsinteger 2 to 6416cells along a tile edge
maxTilesinteger1024tiles kept; the least recently read go first
edgeTolerancenumber0.5above this height difference inside a cell, the point casts its own ray (a step, a wall)
raysPerTickinteger1024rays per game tick to fill tiles
prefetchRadiusnumber32tiles filled ahead around the player, in metres
maxSkipsinteger 0 to 164characters, moving bodies and ignored colliders a ray goes through
ignorelist of strings[]prefixes of entity ids that are never ground (roofs, foliage, triggers)
invalidateOnlist of topics[]topics after which the whole cache is dropped
watchCollidersbooleantruedrop the tiles under a registered static collider that came, went or moved
watchEveryinteger10ticks between two looks at the colliders

The first static collider from the top is the ground: a roof or a bridge over a point is its ground unless its id is in ignore. A tile costs (tileCells + 1)² rays once and about 1 KiB of memory (16 cells).

Services, events #

  • world.ground (service): groundY(x, z) returns the height under the point or null; bounds() returns { min, max } (y from rayTop − rayLength to rayTop); extent() the larger side; inside(p); nearestRoadPoint(p) returns the point on the ground (there are no roads); defaultSpawn() returns { position, yaw }.
  • world.scene_ground (service): cast(x, z) casts one ray now, the cache bypassed; invalidate(area?) drops the tiles over [minX, minZ, maxX, maxZ] or { at, radius }, or all of them, and returns how many; stats() returns tiles, filled, queued, inFlight, bytes, calls, cached, exact, rays, dropped, moving, colliders.
  • world.ready (event): once at mount, with the bounds.
  • ground.changed (event): cached heights were dropped, with reason (colliders, engine.scene.entered, invalidate or the topic), area (or null for all) and tiles.

When your game changes its own geometry in a way the physics port does not see (an engine terrain edited in place), emit one of the invalidateOn topics or call invalidate with the area.

Hooks and modding #

The hook ground.spawn is asked once per scene, before defaultSpawn() answers, with { position, yaw } (the position already on the ground). Both fields are mutable; it is not cancellable.

{ "id": "north_start", "hook": "ground.spawn", "set": { "yaw": 3.14 } }

A scene whose world names world.scene_ground sets bounds, spawn and spawnYaw in its world.config. Every scene change drops the cache. Most settings are moddable; the kit has no collection and saves nothing.

Engines #

One shared implementation: the web runs it as a kit module, Godot, Unity and Unreal in the native kit library, where the rays are the engine's own, so the colliders of the scene your game loaded itself are the ground.

When it does not do what you expect #

  • groundY is null: nothing static is under the point between rayTop and rayTop − rayLength, or only ignored entities, characters and moving bodies (more than maxSkips).
  • The ground is the top of a roof: add its id prefix to ignore.
  • A slope looks smoothed near an edge: lower edgeTolerance or cellSize.
  • The height is stale after the game changed its terrain: emit an invalidateOn topic or call invalidate.
  • null right after a teleport with the physics on its own thread: the tile fills on the next ticks.

Limits #

One height per point (the first static collider from the top); no water, roads or materials; colliders the game changes outside the physics port are not seen until it says so.

Versions #

Kit world.scene_ground 1.0.0, requires game.engine@^1, physics.port@^1 and physics.rigidbody@^1.