# 2. Movement Metrics Asset Part of [[Character Controllers]]. Previous: [[1. CharacterController vs Rigidbody Characters]]. Next: [[3. CharacterController Implementation]]. **Goal:** one asset that is both the designer's **metrics sheet** (Level Up! Level 5) and the controllers' tuning data. Both controllers in Parts 3 and 4 read it, and the gym in Part 5 is generated from it. > [!note] Verification status > Uses only `ScriptableObject`, `CreateAssetMenu`, `Editor`/`CustomEditor` and `EditorGUILayout` - long-standing APIs. **Not compiled in Unity.** ## 2.1 Choose units Use **meters and seconds**. Unity's physics assumes 1 unit = 1 m (gravity 9.81). A typical adult human character: height 1.8 m, radius 0.3 m. Doors ~1 m wide x 2.1 m tall. Write these on the sheet. ## 2.2 The asset ```csharp using UnityEngine; [CreateAssetMenu(menuName = "Gameplay/Movement Metrics", fileName = "MovementMetrics")] public class MovementMetrics : ScriptableObject { [Header("Body")] public float height = 1.8f; public float radius = 0.3f; [Tooltip("Max step the character walks up without jumping.")] public float stepHeight = 0.35f; [Range(0f, 89f)] public float maxSlope = 45f; [Header("Ground speed (m/s)")] public float walkSpeed = 2.0f; public float runSpeed = 5.5f; public float sprintSpeed = 8.0f; [Tooltip("Seconds from 0 to run speed.")] public float accelTime = 0.12f; [Tooltip("Seconds from run speed to 0.")] public float decelTime = 0.08f; [Range(0f, 1f)] public float airControl = 0.5f; [Tooltip("Degrees per second the body turns toward move direction.")] public float turnSpeed = 720f; [Header("Jump")] public float jumpHeight = 1.25f; public float timeToApex = 0.36f; public float fallGravityMultiplier = 1.6f; public float jumpCutGravityMultiplier = 2.2f; public float terminalFallSpeed = 30f; [Header("Forgiveness (s)")] public float coyoteTime = 0.12f; public float jumpBufferTime = 0.12f; // ---- Derived (read-only) ---- public float Gravity => 2f * jumpHeight / (timeToApex * timeToApex); public float JumpVelocity => Gravity * timeToApex; public float FallTime => Mathf.Sqrt(2f * jumpHeight / (Gravity * fallGravityMultiplier)); public float AirTime => timeToApex + FallTime; /// Flat-ground horizontal distance of a full jump at the given speed. public float JumpDistance(float speed) => speed * AirTime; /// Tallest ledge you can land on (minus a small margin for the capsule's rounded bottom). public float MaxLedge => jumpHeight - radius * 0.25f; /// Gap you can still cross if you must land on a ledge 'dropHeight' lower (positive = lower). public float JumpDistanceToLowerLedge(float speed, float dropHeight) { float gFall = Gravity * fallGravityMultiplier; float tDown = Mathf.Sqrt(2f * (jumpHeight + Mathf.Max(0f, dropHeight)) / gFall); return speed * (timeToApex + tDown); } void OnValidate() { height = Mathf.Max(height, radius * 2f + 0.01f); stepHeight = Mathf.Clamp(stepHeight, 0f, height - 0.01f); timeToApex = Mathf.Max(0.05f, timeToApex); } } ``` > [!warning] Code not compiled in Unity > Not compiled or play-tested. The jump-distance formulas ignore air acceleration (they assume you're already at speed) - good enough for level layout, validate in the gym. ## 2.3 A metrics readout inspector Designers should *see* the derived numbers. Put this in an `Editor` folder (see [[Custom Inspectors and Editor Tooling]]). ```csharp #if UNITY_EDITOR using UnityEditor; using UnityEngine; [CustomEditor(typeof(MovementMetrics))] public class MovementMetricsEditor : Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); var m = (MovementMetrics)target; EditorGUILayout.Space(); EditorGUILayout.LabelField("Derived metrics (put these on the metrics sheet)", EditorStyles.boldLabel); using (new EditorGUI.DisabledScope(true)) { EditorGUILayout.FloatField("Gravity (m/s^2)", m.Gravity); EditorGUILayout.FloatField("Jump velocity (m/s)", m.JumpVelocity); EditorGUILayout.FloatField("Air time (s)", m.AirTime); EditorGUILayout.FloatField("Jump distance @ walk (m)", m.JumpDistance(m.walkSpeed)); EditorGUILayout.FloatField("Jump distance @ run (m)", m.JumpDistance(m.runSpeed)); EditorGUILayout.FloatField("Jump distance @ sprint (m)", m.JumpDistance(m.sprintSpeed)); EditorGUILayout.FloatField("Max ledge (m)", m.MaxLedge); EditorGUILayout.FloatField("Gap to 1 m lower ledge @ run (m)", m.JumpDistanceToLowerLedge(m.runSpeed, 1f)); } float g = m.Gravity; if (g > 9.81f * 4f) EditorGUILayout.HelpBox(quot;Gravity is {g / 9.81f:F1}x Earth. Normal for snappy platformers, but heavy props will feel different from the player.", MessageType.Info); } } #endif ``` > [!warning] Code not compiled in Unity > Not compiled. With the defaults: g = 2 x 1.25 / 0.36^2 = ~19.3 m/s^2 (about 2x Earth), jump velocity ~6.9 m/s, run jump distance ~5.5 x (0.36 + 0.28) = ~3.5 m. ## 2.4 Keep other systems in sync | System | Field that must match | |---|---| | CharacterController | `height`, `radius`, `stepOffset` = stepHeight, `slopeLimit` = maxSlope | | CapsuleCollider (Rigidbody path) | `height`, `radius` | | NavMesh Agent Type ([[NavMesh in Unity 6 (AI Navigation 2.x)]]) | Radius, Height, Step Height, Max Slope - so AI companions fit where the player fits | | Level kit (doors, corridors, stairs) | Part 5 gym | | Speed power-ups ([[2.Modifiers and Timed Buffs]]) | apply as multipliers to `runSpeed` at runtime, never edit the asset in Play mode builds | > [!tip] Pattern > This is **Type Object** from [[Game Programming Patterns (Review)]]: multiple characters (hero, heavy, kid) are just different `MovementMetrics` assets on the same controller. See also [[ScriptableObject Architecture]]. ## Docs - ScriptableObject (Manual): https://docs.unity3d.com/6000.5/Documentation/Manual/class-ScriptableObject.html - `CreateAssetMenuAttribute`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/CreateAssetMenuAttribute.html - `Editor` (custom inspectors): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Editor.html