2D physics
Box2D-backed rigidbodies, colliders, collision/trigger callbacks, and raycast/overlap queries, all driven from C#.
Fixed timestep
Physics runs on a fixed timestep (Time.FixedDeltaTime, 1/50s by default), independent of your frame rate. A jump tuned at 60fps behaves identically at 30fps or 144fps.
Two update methods matter for physics scripting:
| Method | Runs | Use for |
|---|---|---|
OnUpdate(float ts) | Once per rendered frame | Input, camera, anything visual |
OnFixedUpdate(float ts) | Once per physics substep (0, 1, or several times per frame) | Forces and impulses |
protected override void OnFixedUpdate(float ts)
{
if (m_WantsJump)
{
GetComponent<Rigidbody2DComponent>()?.ApplyLinearImpulse(new Vector2(0, m_JumpForce), true);
m_WantsJump = false;
}
}Applying an impulse in OnUpdate instead makes its strength depend on frame rate — the exact bug the fixed timestep exists to avoid. Read input in OnUpdate, apply forces in OnFixedUpdate.
Rendering interpolates between physics substeps automatically (Time.FixedDeltaTime at 50Hz on a 144Hz display would otherwise judder) — nothing to opt into, and nothing to worry about: gameplay code always reads the true simulated position, never a smoothed one.
FixedDelta and MaxFrameDelta (the spiral-of-death clamp for a debugger pause or a stutter) are both authored in Project Settings → Physics Settings.
Colliders and sensors
BoxCollider2D/CircleCollider2D on an entity with a Rigidbody2D define its shape. Checking Is Sensor in the Properties panel turns a collider into a trigger: it reports overlap but applies no physical force, and other bodies pass straight through it. Use sensors for coins, checkpoints, death zones, and level exits.
Collision and trigger callbacks
public class Player : GameEntity
{
protected override void OnCollisionEnter(Collision2D collision)
{
if (collision.Entity?.HasComponent<Damage>() == true)
TakeHit();
}
protected override void OnTriggerEnter(GameEntity? other)
{
if (other?.Name == "Coin")
Collect(other);
}
}| Callback | Fires for | Collision2D/entity carries |
|---|---|---|
OnCollisionEnter / OnCollisionExit | A solid contact begins/ends (neither side is a sensor) | Normal, contact point, and impulse |
OnTriggerEnter / OnTriggerExit | A sensor overlap begins/ends (at least one side is a sensor) | Just the other entity |
Five rules worth knowing:
- Both entities get the callback, once each, per event. There's no ordering guarantee between the two sides — don't write gameplay that assumes one fires before the other.
- The other entity can be
null. If it was destroyed earlier in the same physics step (two bullets hitting one enemy, say), the side that resolves second sees a nullCollision2D.Entity/other— never a dangling reference. - Destroying an entity from inside a callback is safe. A bullet that destroys itself on impact is the common case, not an edge case — the actual destroy is deferred to the end of the physics step, same as destroying an entity from
OnUpdate. - Sensors don't push.
IsSensormakes a collider report-only; it never deflects the other body. - Native (C++)
ScriptableEntitygets the same four virtuals, for scripts written in that language instead of C#.
Queries
The other half of gameplay physics — the half that isn't event-driven.
// Ground check - the single most-used query in any platformer
if (Physics2D.Raycast(pos, Vector2.Down, 0.1f, out var hit, groundMask))
m_Grounded = true;
Physics2D.OverlapCircle(pos, 3.0f, hitEntities, enemyMask); // explosion radius
Physics2D.CircleCast(pos, 0.2f, dir, 10.0f, hits); // thick projectile
Physics2D.RaycastAll(pos, dir, 20.0f, hits); // pierceRaycastAll, OverlapCircle, OverlapBox, and CircleCast all fill a caller-supplied array and return the total number of matches, which may exceed the array's length — compare the return value against the array length to detect truncation. None of them allocate: a ground check runs once per entity per frame, and an allocation on that path is exactly the kind of thing that turns into a stutter blamed on the physics engine. See Physics2D for the full method-by-method reference, including OverlapBox and GetLayerMask (not shown above).
Queries are legal from OnUpdate and OnFixedUpdate alike, and run against the current, already-stepped world state.
Remembering what a query found
A query only tells you what's true right now — an OverlapCircle sweep that finds nearby collectibles, say, has no memory of which ones the player already picked up on an earlier pass. Pair it with Session to record that: set a flag per item when it's collected, and check it before acting on the same item again, including after a Scenes.Load.
Enable Show physics queries in the Settings panel to draw every cast issued this frame as a line with its hit point — the fastest way to see why a ground check is failing.
Collision layers
Named layers and a collision matrix live in Project Settings → Physics Settings. Each collider picks one layer; the matrix decides which layer pairs collide at all (a bullet not hitting other bullets, an enemy not colliding with other enemies, etc.). The matrix is always symmetric — the UI enforces it, so there's no way to author a one-directional rule.
ulong mask = Physics2D.GetLayerMask("Enemy") | Physics2D.GetLayerMask("EnemyBullet");
Physics2D.Raycast(pos, dir, range, out var hit, mask);One-way platforms
Check One Way on a BoxCollider2D to make it a platform a character can jump up through and land on top of. Two rules:
- Jumping up through it always passes through.
- Landing on top from above is solid.
To let the player drop through deliberately (the classic "down + jump" input), call:
GetComponent<Rigidbody2DComponent>()?.SetIgnoreOneWayPlatforms(true);
// ... next frame, once the drop-through is done:
GetComponent<Rigidbody2DComponent>()?.SetIgnoreOneWayPlatforms(false);This flag doesn't reset itself — clear it once the drop is complete.
See also
Two other API Reference topics come up often alongside physics scripting:
- Physics2D — the full method-by-method reference for
Raycast,RaycastAll,OverlapCircle,OverlapBox, andCircleCast, all used above. - Session — a trigger's
OnTriggerEnter(a checkpoint, a collected item) commonly needs to remember something past the current scene;Sessionis the recommended way to carry that value across aScenes.Load.