# 4. Beat-Chart to Gray-Box Part of [[ProBuilder Gray-Boxing and Landmarks]]. Prev: [[3. Weenies Landmarks and Lighting as Wayfinding]]. Next: [[5. Worked Example Landmark Blockout]]. Level 4 of [[Level Up! (Review)]] is the paperwork: a **beat chart** whose rows are beats and whose columns are things like setting, story, new mechanics, enemies, and power-ups. Level 9 is how those rows become a space. This part stores a *small* chart as data and stamps a gray-box from it so a critique can say "beat 3 is a long walk with no reward" while looking at both the asset and the scene. This is not a level-streaming tool. It is a disposable blockout. Re-running the builder should replace a `BeatChart_Blockout` root, not silently stack duplicates. ## 4.1 The chart asset ```csharp using UnityEngine; public enum BeatRoomKind { Approach, // shows the weenie Arena, // combat or challenge Puzzle, // key, gate, or safe experiment RewardFinger, // a dead-end with a payoff Calm, // breath Landmark // the weenie itself } [System.Serializable] public struct BeatRow { public string beatName; public BeatRoomKind kind; [Tooltip("Length of this beat along the path, in meters.")] public float length; [Tooltip("Width of the space, in meters.")] public float width; [Tooltip("Wall height. Use MovementMetrics.height plus headroom if left 0.")] public float height; public bool showWeenieFromHere; [TextArea] public string notes; // story, mechanic, enemy, reward - the other columns } [CreateAssetMenu(menuName = "Gameplay/Beat Chart", fileName = "BeatChart")] public sealed class BeatChart : ScriptableObject { public string levelName = "Plaza Approach"; public MovementMetrics metrics; public BeatRow[] beats; } ``` Keep `notes` as a single text field rather than inventing a full GDD schema. The book has more columns than you need in a teaching asset; write them into `notes` in the same order every time (`Setting / Story / Mechanic / Enemy / Reward`). ## 4.2 Editor builder Put this in an `Editor` folder ([[Custom Inspectors and Editor Tooling]]). It uses only `ShapeGenerator` APIs verified in 6.1.4 plus Editor prefab / menu hooks. ```csharp #if UNITY_EDITOR using UnityEditor; using UnityEditor.ProBuilder; using UnityEngine; using UnityEngine.ProBuilder; public static class BeatChartBuilder { const string RootName = "BeatChart_Blockout"; [MenuItem("Tools/Blockout/Build Beat Chart From Selection")] static void BuildFromSelection() { var chart = Selection.activeObject as BeatChart; if (chart == null) { EditorUtility.DisplayDialog("Beat Chart", "Select a BeatChart asset, then run this again.", "OK"); return; } Build(chart); } public static void Build(BeatChart chart) { if (chart.beats == null || chart.beats.Length == 0) return; var existing = GameObject.Find(RootName); if (existing != null) Undo.DestroyImmediate(existing); var root = new GameObject(RootName); Undo.RegisterCreatedObjectUndo(root, "Build Beat Chart"); MovementMetrics m = chart.metrics; float defaultHeight = m != null ? m.height + 1.0f : 3.0f; float z = 0f; for (int i = 0; i < chart.beats.Length; i++) { BeatRow row = chart.beats[i]; float h = row.height > 0.1f ? row.height : defaultHeight; float w = Mathf.Max(2f, row.width); float len = Mathf.Max(2f, row.length); var room = new GameObject(quot;{i:00}_{row.beatName}"); room.transform.SetParent(root.transform, false); room.transform.position = new Vector3(0f, 0f, z + len * 0.5f); var floor = ShapeGenerator.GenerateCube(PivotLocation.Center, new Vector3(w, 0.2f, len)); floor.name = "Floor"; floor.transform.SetParent(room.transform, false); floor.transform.position = room.transform.position + Vector3.up * 0.1f; TintAndCommit(floor, ColorFor(row.kind)); var wallL = ShapeGenerator.GenerateCube(PivotLocation.Center, new Vector3(0.3f, h, len)); wallL.name = "Wall_L"; wallL.transform.SetParent(room.transform, false); wallL.transform.position = room.transform.position + new Vector3(-w * 0.5f, h * 0.5f, 0f); TintAndCommit(wallL, new Color(0.2f, 0.2f, 0.2f)); var wallR = ShapeGenerator.GenerateCube(PivotLocation.Center, new Vector3(0.3f, h, len)); wallR.name = "Wall_R"; wallR.transform.SetParent(room.transform, false); wallR.transform.position = room.transform.position + new Vector3(w * 0.5f, h * 0.5f, 0f); TintAndCommit(wallR, new Color(0.2f, 0.2f, 0.2f)); if (row.kind == BeatRoomKind.Landmark) { float weenieHeight = Mathf.Max(8f, h * 3f); var weenie = ShapeGenerator.GenerateCone(PivotLocation.Center, 1.2f, weenieHeight, 12); weenie.name = "Weenie"; weenie.transform.SetParent(room.transform, false); weenie.transform.position = room.transform.position + Vector3.up * (weenieHeight * 0.5f); TintAndCommit(weenie, new Color(1f, 0.85f, 0.2f)); } z += len; } Selection.activeGameObject = root; } static Color ColorFor(BeatRoomKind kind) => kind switch { BeatRoomKind.Approach => new Color(0.55f, 0.55f, 0.55f), BeatRoomKind.Arena => new Color(0.45f, 0.45f, 0.5f), BeatRoomKind.Puzzle => new Color(0.35f, 0.55f, 0.65f), BeatRoomKind.RewardFinger => new Color(0.35f, 0.6f, 0.35f), BeatRoomKind.Calm => new Color(0.5f, 0.6f, 0.5f), BeatRoomKind.Landmark => new Color(0.55f, 0.5f, 0.35f), _ => Color.gray }; static void TintAndCommit(ProBuilderMesh pb, Color color) { // Official 6.1.4 API: colors (IList<Color>, length must match positions) and vertexCount. if (pb.colors == null || pb.colors.Count != pb.vertexCount) { var colors = new Color[pb.vertexCount]; for (int i = 0; i < colors.Length; i++) colors[i] = color; pb.colors = colors; } else { var colors = pb.colors; for (int i = 0; i < colors.Count; i++) colors[i] = color; pb.colors = colors; } pb.ToMesh(); pb.Refresh(); EditorMeshUtility.Optimize(pb, true); } } #endif ``` `ProBuilderMesh.colors` and `vertexCount` are official 6.1.4 APIs (`colors` is an `IList<Color>` whose length must match the positions array). After assigning colors, `ToMesh()` + `Refresh()` applies them; the URP sample shaders must be imported or the tint may not show. `GetColors()` also exists (returns white if the mesh has no colors). You can instead paint with the Vertex Color tool after a build. `Undo.DestroyImmediate` on the previous root is an Editor-only rebuild. It is not a runtime API. Never hook this builder to Play Mode. `GameObject.Find` is acceptable in a one-off Editor menu; do not copy it into gameplay. ## 4.3 After the stamp The generated spine is a *starting* corridor: two walls and a floor per beat, plus a cone on Landmark rows. You then: - Cut doorways between beats (or leave gaps). - Pull a RewardFinger off to the side and put a green prop at the end. - Place a `LandmarkSightline` at beat 0 if `showWeenieFromHere` is set. - Add a spotlight on the cone (part 3). - Play it. If beat 3 "feels long," it *is* long - shorten `length` and rebuild, or put a reward mid-span. ## 4.4 Exercise Write a five-row chart: Approach (weenie visible), Calm, Arena, RewardFinger, Landmark. Build it, play it, and change one length because it bored you. That is the whole loop. ## Sources - `ShapeGenerator` (6.1.4): https://docs.unity3d.com/Packages/[email protected]/api/UnityEngine.ProBuilder.ShapeGenerator.html - API overview (colors stored on `ProBuilderMesh`): https://docs.unity3d.com/Packages/[email protected]/manual/api.html - [[Level Up! (Review)]] Levels 4 and 9 - [[Custom Inspectors and Editor Tooling]]