# 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