quot;{name}: visible from {visible}/{total} grid points ({(total > 0 ? 100f * visible / total : 0f):F1}%)."); } void ForEachGridPoint(System.Action<Vector3> action) { Vector3 c = transform.position; for (float x = -gridRadius; x <= gridRadius; x += gridSpacing) for (float z = -gridRadius; z <= gridRadius; z += gridSpacing) { if (x * x + z * z > gridRadius * gridRadius) continue; Vector3 p = new Vector3(c.x + x, 0f, c.z + z); if (TerrainAt(p) != null) action(p); } } void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(Target, 2f); if (vantagePoints != null) { foreach (Transform v in vantagePoints) { if (v == null) continue; Terrain t = TerrainAt(v.position); if (t == null) continue; Vector3 eye = v.position; eye.y = SampleWorldHeight(t, eye) + eyeHeight; bool ok = IsVisibleFrom(v.position, out Vector3 block); Gizmos.color = ok ? Color.green : Color.red; Gizmos.DrawLine(eye, ok ? Target : block); if (!ok) Gizmos.DrawSphere(block, 1.5f); } } if (drawCoverage) { ForEachGridPoint(p => { Terrain t = TerrainAt(p); Vector3 g = new Vector3(p.x, SampleWorldHeight(t, p) + 0.5f, p.z); Gizmos.color = IsVisibleFrom(p, out _) ? new Color(0f, 1f, 0f, 0.6f) : new Color(1f, 0f, 0f, 0.35f); Gizmos.DrawCube(g, Vector3.one * 2f); }); } } } ``` **How to use it in class** 1. Put the landmark (a tower, a dead tree, a glowing crystal) on the peak from part 1. Add `LandmarkSightlines`, set `visibleTopOffset` to its top. 2. Add vantage points at spawn and at each route decision. Select the landmark: green/red lines appear. 3. Turn on **Draw Coverage**. Sculpt a ridge in front of the spawn and watch the grid go red behind it. That ridge is now a **reveal**: put the crest on the main route so the player sees the landmark at the moment you choose. 4. Context menu > **Log Coverage** after each iteration. Paste the % into your level design doc next to the beat chart. **Why this design** - Gizmos only run when selected (`OnDrawGizmosSelected`), so a 1,000-point grid doesn't slow the whole editor. - `SampleHeight` returns height **relative to the terrain**; adding `GetPosition().y` gives world height. Forgetting this is the classic bug when terrains aren't at y = 0. - `TerrainAt` handles multiple tiles; sightlines often cross tile borders. - Trees on terrain have no colliders unless the prototype has a collider *and* Terrain Collider "Enable Tree Colliders" is on - so by default this test ignores trees. That is deliberate: foliage *partially* hides a landmark, which is usually what you want. If dense forest should block, use the collider option and a dedicated occluder layer. ## 4.3 Editor tool: snap and align props to terrain Every landmark, rock and building needs to sit on the ground. Bind it to a shortcut. ```csharp // Assets/Editor/SnapToTerrainMenu.cs using UnityEditor; using UnityEngine; public static class SnapToTerrainMenu { [MenuItem("Tools/Terrain/Snap Selection To Terrain %#t")] // Ctrl/Cmd+Shift+T static void Snap() => Apply(align: false); [MenuItem("Tools/Terrain/Snap And Align Selection To Terrain")] static void SnapAlign() => Apply(align: true); static void Apply(bool align) { foreach (Transform t in Selection.transforms) { Terrain terrain = FindTerrainUnder(t.position); if (terrain == null) continue; Undo.RecordObject(t, "Snap To Terrain"); Vector3 p = t.position; p.y = terrain.SampleHeight(p) + terrain.GetPosition().y; t.position = p; if (align) { TerrainData d = terrain.terrainData; Vector3 local = p - terrain.GetPosition(); Vector3 n = d.GetInterpolatedNormal(local.x / d.size.x, local.z / d.size.z); t.rotation = Quaternion.FromToRotation(t.up, n) * t.rotation; } } } static Terrain FindTerrainUnder(Vector3 p) { foreach (Terrain t in Terrain.activeTerrains) { Vector3 o = t.GetPosition(); Vector3 s = t.terrainData.size; if (p.x >= o.x && p.x <= o.x + s.x && p.z >= o.z && p.z <= o.z + s.z) return t; } return null; } } ``` Align for rocks and debris; **don't** align buildings or landmarks - a vertical tower reads as intentional, a leaning one reads as a bug. ## 4.4 Terrain level design checklist - [ ] Landmark visible from spawn (green line) and from every route decision point. - [ ] At least one **reveal**: a crest where the landmark appears after being hidden. - [ ] Every boundary is a readable slope/cliff/water edge with a rock texture, not an invisible wall. - [ ] Walkable slopes match the controller metrics and NavMesh Agent Max Slope. - [ ] Every dead end has a reward (Level Up! - treasure, view, lore). - [ ] Alternate open and enclosed spaces along the main route (pacing). - [ ] Clearings exist where combat happens (trees break camera and AI sensing; see [[Organic Enemy Sensing]]). - [ ] VR only: no forced slopes or drops while the player is moving with smooth locomotion; prefer gentle grades and teleport anchors. ## 4.5 Exercise Design a 3-beat route: spawn (landmark visible) -> valley (landmark hidden, forest, tension) -> ridge crest (reveal, landmark close). Prove each beat with vantage points and record coverage before and after planting trees. ## Sources - `Terrain.SampleHeight`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Terrain.SampleHeight.html - `Terrain` (activeTerrains, GetPosition): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Terrain.html - `TerrainData` (GetInterpolatedNormal): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.html - Level Up! The Guide to Great Video Game Design (2nd ed.), Scott Rogers - via [[Level Up! (Review)]]