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.
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 #
| Setting | Type | Default | Meaning |
|---|---|---|---|
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 |
rayTop | number | 1000 | height the downward rays start from: above everything that may be ground |
rayLength | number | 2000 | length of a ray; nothing below rayTop − rayLength is ground |
spawn | [x, y, z] | [0, 0, 0] | where the player starts |
spawnYaw | number | 0 | direction the player faces, in radians |
spawnOnGround | boolean | true | put the spawn on the ground under it |
cache | boolean | true | answer from the height grid where it is known; off: one ray per call |
cellSize | number 0.1 to 16 | 1 | metres between two height samples; match your terrain's resolution |
tileCells | integer 2 to 64 | 16 | cells along a tile edge |
maxTiles | integer | 1024 | tiles kept; the least recently read go first |
edgeTolerance | number | 0.5 | above this height difference inside a cell, the point casts its own ray (a step, a wall) |
raysPerTick | integer | 1024 | rays per game tick to fill tiles |
prefetchRadius | number | 32 | tiles filled ahead around the player, in metres |
maxSkips | integer 0 to 16 | 4 | characters, moving bodies and ignored colliders a ray goes through |
ignore | list of strings | [] | prefixes of entity ids that are never ground (roofs, foliage, triggers) |
invalidateOn | list of topics | [] | topics after which the whole cache is dropped |
watchColliders | boolean | true | drop the tiles under a registered static collider that came, went or moved |
watchEvery | integer | 10 | ticks 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 ornull;bounds()returns{ min, max }(y fromrayTop − rayLengthtorayTop);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()returnstiles,filled,queued,inFlight,bytes,calls,cached,exact,rays,dropped,moving,colliders.world.ready(event): once at mount, with thebounds.ground.changed(event): cached heights were dropped, withreason(colliders,engine.scene.entered,invalidateor the topic),area(ornullfor all) andtiles.
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 #
groundYisnull: nothing static is under the point betweenrayTopandrayTop − rayLength, or only ignored entities, characters and moving bodies (more thanmaxSkips).- The ground is the top of a roof: add its id prefix to
ignore. - A slope looks smoothed near an edge: lower
edgeToleranceorcellSize. - The height is stale after the game changed its terrain: emit an
invalidateOntopic or callinvalidate. nullright 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.