Physics
Museum.
Six small Unity scenes that each isolate one principle of classical mechanics — the kind of room where you'd press a brass button and watch a single equation play itself out in the spotlight.
A room for
every equation.
Most physics tutorials demo the engine. This catalogue does the opposite — each scene is tuned away from engine defaults until only the equation remains.
Where the equation is a closed-form trajectory, the Rigidbody is removed and transform.position is written directly. Where the equation is F = m·a, the
force mode is intentionally mass-dependent so heavy bodies feel heavy. Where damping matters,
Unity 6's renamed linearDamping is named exactly because aerodynamic drag and the solver's velocity
damping are not the same thing — and the museum should not pretend otherwise.
Each exhibit ships with its formula, its working code, and the one design decision that keeps the lesson legible. Press the buttons.
The Kinematic
Catapult.
A projectile under gravity, written as a closed-form trajectory and read out frame by frame.
note
transform.position directly keeps the formula pure.
using UnityEngine;
public class KinematicCatapult : MonoBehaviour
{
public Vector2 vxRange = new(3.5f, 6.8f);
public Vector2 vyRange = new(6.0f, 9.6f);
public float g = 9.81f;
Vector3 x0;
Vector3 v;
float t;
bool flying;
void Awake() => x0 = transform.position;
public void Launch()
{
v = new Vector3(
Random.Range(vxRange.x, vxRange.y),
Random.Range(vyRange.x, vyRange.y),
0f);
transform.position = x0;
t = 0f;
flying = true;
}
void Update()
{
if (!flying) return;
t += Time.deltaTime;
// x_f = ½ a t² + vᵢ t + xᵢ — written directly.
// No Rigidbody: drag and contact resolution stay out of the lesson.
Vector3 a = Vector3.down * g;
transform.position = 0.5f * a * t * t + v * t + x0;
if (transform.position.y <= x0.y) flying = false;
}
}
The Drag
Race.
Two cubes, identical thrust, one is ten times heavier. The √10 lives in the timing strip.
note
ForceMode.Force is the mass-dependent choice — and the choice. Using
ForceMode.Acceleration would have hidden the lesson by giving both cubes
the same a, and the heavy cube would finish in a dead heat with the light one.
The √10 is the exhibit.
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class DragRace : MonoBehaviour
{
[Tooltip("Newtons — identical for the 10 kg and 100 kg cube.")]
public float thrust = 80f;
Rigidbody rb;
void Awake() => rb = GetComponent<Rigidbody>();
// ForceMode.Force is mass-dependent.
// a = F / m → the 100 kg cube takes √10 ≈ 3.16× longer to cover the track.
public void Throttle() =>
rb.AddForce(Vector3.forward * thrust, ForceMode.Force);
}
The Skydiver
& Parachute.
Gravity pulls; damping pushes back. At equilibrium, velocity stops climbing.
note
Rigidbody.drag to linearDamping — a
long-overdue clarification, because aerodynamic drag (a function of v², area,
and air density) and the engine's linear-velocity damping are not the same thing.
Naming them apart lets the engine say so out loud.
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class SkydiverChute : MonoBehaviour
{
public float bFreefall = 0.2f; // open-air damping
public float bChute = 1.5f; // parachute-deployed damping
Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>();
// Unity 6: Rigidbody.drag → Rigidbody.linearDamping.
// It is *not* aerodynamic drag; it is the solver's
// linear-velocity damping coefficient, and the rename
// says so explicitly.
rb.linearDamping = bFreefall;
}
public void DeployParachute() => rb.linearDamping = bChute;
}
The Impulse
Shooter.
Hit a block dead-centre, it translates. Hit a corner, it tumbles. The hit-point is the design.
note
AddForceAtPosition — not AddForce — is the soul of the
exhibit. Striking a Jenga block dead-centre and striking it on the corner has to
feel different in the hand, or there is no exhibit. The cross product
r × J is what supplies the difference.
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class ImpulseShooter : MonoBehaviour
{
public float impulseMagnitude = 6f; // N·s
Rigidbody rb;
void Awake() => rb = GetComponent<Rigidbody>();
void OnMouseDown()
{
var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (!Physics.Raycast(ray, out var hit) || hit.rigidbody != rb) return;
Vector3 J = Vector3.up * impulseMagnitude;
// AddForceAtPosition splits the impulse two ways:
// Δv = J / m
// Δω = I⁻¹ ( r × J ) with r = hit.point − centre
// AddForce would discard r — and with it, the exhibit.
rb.AddForceAtPosition(J, hit.point, ForceMode.Impulse);
}
}
The Spin
Cabinet.
A torque-impulse spins a unit cube. Damping pulls it back down. The constant is the homework.
note
torqueVector = (0, 10, 0) in the C# gives Δω ≈ 60 rad/s
on a 1 kg unit cube — close to 10 revolutions per second. The constant wants tuning;
the cabinet is the lesson, the constant is homework left for the visitor.
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class SpinCabinet : MonoBehaviour
{
[Tooltip("τ in N·m·s — homework left for the visitor.")]
public Vector3 torqueVector = new(0f, 10f, 0f);
Rigidbody rb;
void Awake() => rb = GetComponent<Rigidbody>();
// Δω = I⁻¹ · τ_impulse.
// For a unit cube of mass 1 kg this is Δω ≈ 60 rad/s ≈ 10 rev/s.
public void ApplySpin() =>
rb.AddTorque(torqueVector, ForceMode.Impulse);
}
The Reset
Plinth.
A single brass button that sets every other exhibit back to opening hours.
opening hours.
Press once. Positions, rotations, velocities, and angular velocities are returned to their starting frames. The solver is then told, gently, to forget the last hour.
- 01Catapultidle
- 02Drag Raceidle
- 03Skydiveridle
- 04Impulseidle
- 05Spin Cabinetidle
note
rb.Sleep(). Zeroing a Rigidbody's velocity while it
is still in contact with the floor lets the next FixedUpdate hand it a
micro-velocity from contact resolution — and the exhibit looks subtly broken. Sleep
tells the solver: don't touch this one.
using UnityEngine;
public class ResetPlinth : MonoBehaviour
{
public Rigidbody[] subjects;
Vector3[] x0;
Quaternion[] R0;
void Awake()
{
x0 = new Vector3[subjects.Length];
R0 = new Quaternion[subjects.Length];
for (int i = 0; i < subjects.Length; i++)
{
x0[i] = subjects[i].position;
R0[i] = subjects[i].rotation;
}
}
public void ResetAll()
{
for (int i = 0; i < subjects.Length; i++)
{
var rb = subjects[i];
rb.position = x0[i];
rb.rotation = R0[i];
rb.linearVelocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
// Zeroing while in contact lets the next FixedUpdate hand the
// body a micro-velocity from contact resolution, and the
// exhibit looks subtly broken. Sleep tells the solver:
// don't touch this one.
rb.Sleep();
}
}
}
The museum
is the argument.
Game-engine physics is a black box wrapped in friendly methods. The danger isn't that the box is wrong — it's that the box is right in ways you didn't intend.
Every exhibit here picks the least convenient API that still works, because it's
the one that forces the equation to be visible. ForceMode.Force over
.Acceleration. AddForceAtPosition over AddForce.
transform.position over a Rigidbody, when the equation is closed-form.
Read the code, press the buttons, then go open the repository. The lights stay on.