Skip to main content

Physics

Talesmith.Physics gives every scene a 2D physics world built from the scene's Collider2D, Rigidbody2D, CharacterController2D and tile map entities. It steps in the fixed update while the game plays, moves bodies under gravity and forces, keeps characters out of walls, and reports collisions and triggers. This page explains how it works so you can choose the right body type, layer setup and query for your game; Physics from scripts covers the calls.

Bodies and colliders​

A Collider2D gives an entity a shape. What moves it depends on the other components:

ComponentsBodyMoved byPushesIs pushed
Collider2D onlyStaticYour code changing its Transform (a teleport)NoNo
Collider2D + Rigidbody2D (Dynamic)DynamicGravity, forces, impulses, collisions, its VelocityYesYes
Collider2D + Rigidbody2D (Kinematic)KinematicIts Velocity, or MovePosition and MoveRotationDynamic bodiesNo
Collider2D + CharacterController2DKinematic characterMoveCharacter, which slides along what it hitsDynamic bodies resting on itNo

Shapes are a box, circle, capsule or polygon, placed at the transform's position plus Offset, turned by the transform's rotation plus Rotation and scaled by the transform's scale. Concave polygons are split into convex pieces of at most eight points. World units are pixels by default and Y points down, so gravity is (0, 980).

Each entity has one body. A rigid body on a child entity is moved by physics and then again by the transform hierarchy, so keep rigid bodies on root entities. Compound bodies made of several child colliders and joints are not supported.

Changes to components take effect on the next fixed step: a changed collider is rebuilt, a transform moved by your code teleports the body, and a velocity you set replaces the simulated one.

The step​

Physics runs only in play mode, inside the fixed update. Each fixed step goes in this order:

  1. Prepare. Interpolated bodies go back to their simulated pose, and colliders added or changed since the last step are picked up. Character moves and queries in the same step see them.
  2. Your code. Scripts' FixedUpdate and your fixed-step systems set velocities, apply forces and move characters.
  3. Step. The world mirrors entities into bodies, finds new pairs, updates contacts, solves, sweeps fast bodies, puts resting bodies to sleep, writes poses and velocities back to the components and delivers the step's contact events, including scripts' OnCollision and OnTrigger callbacks.

Interpolated bodies are then moved between their last two poses in LateUpdate, before cameras follow them. Code that reads an interpolated entity's Transform in Update sees the drawn pose; writing it teleports the body.

The solver uses soft contacts with substeps, and contacts start just before shapes touch, so bodies stop at surfaces instead of sinking in. The simulation is deterministic: the same scene with the same input gives bit-identical results.

Settings​

IPhysicsWorld.Settings holds a scene's settings. Gravity comes from the scene's environment, set in the scene settings; the rest from the defaults:

SettingDefault
Gravity(0, 980)World units per second squared; positive Y falls
LengthUnitsPerMeter100Scales the solver's tolerances, so pixel worlds behave like meter worlds
Substeps4More gives stiffer stacks, at proportional cost
RestitutionThreshold100Slower impacts do not bounce, so resting bodies settle
AllowSleeping, TimeToSleepon, 0.5 sResting bodies stop simulating
BroadphaseDynamicTreeSpatialHash suits many similar small bodies

config/physics.json holds the project's default gravity, layer names and which layers ignore each other. Edit it in Project Settings:

{ "gravity": [0, 980], "layers": { "1": "Player", "2": "Enemies" }, "ignoredCollisions": [ [1, 1], [2, 5] ] }

A plugin can adjust the settings every scene starts from with services.AddTalesmithPhysics(settings => …).

Layers and the collision matrix​

Every collider is on one of 32 layers. Two colliders collide when each one's CollisionMask contains the other's layer and the project's collision matrix allows the pair. Name the layers in Project Settings › Physics, then mirror the numbers in code:

/// <summary>Collision layers, as named in Project Settings.</summary>
public static class Layers
{
public const int Default = 0;
public const int Player = 1;
public const int Enemies = 2;
public const int Ground = 3;
}

PhysicsLayers.Mask(Layers.Enemies, Layers.Player) builds a mask from layer numbers, and PhysicsLayers.All (-1) is every layer. Use the matrix for rules that always hold ("enemy bullets never hit enemies") and masks on colliders for exceptions. Queries take a QueryFilter with a layer mask, whether to include triggers and one entity to ignore, usually the one asking.

Characters​

CharacterController2D is for player characters and enemies that walk. You call MoveCharacter(entity, motion) from FixedUpdate with the distance to move this step, and the character sweeps its collider, slides along up to four surfaces, climbs steps up to StepOffset, walks up slopes up to SlopeLimit, keeps SkinWidth from surfaces and stays on the ground within GroundSnapDistance when walking down. It returns which sides touched something (Below, Sides, Above) and stores them with IsGrounded, GroundNormal, Ground and LastMotion. Up decides what counts as ground, wall and ceiling.

You own the velocity: gravity, jumping and acceleration are your code, which is what makes platformer controls feel tight. Characters are kinematic, so they enter triggers and push dynamic bodies resting on them, but a crate they walk into blocks them like a wall. To push crates, apply impulses to the bodies in collision callbacks or from GetContacts. See the platformer controller for a complete controller.

Tile maps, one-way platforms and triggers​

Every entity with a tile map collides through the map's layers whose role is Collision. A non-empty cell is solid; a tile with collision shapes uses those instead of the full cell. Collision is built per chunk the first time a body or a query comes near it, so huge maps cost only what is used. On square grids full cells merge into as few rectangles as possible; on hex grids each cell is a hexagon. Edges shared by neighboring solid cells never produce a contact normal, so bodies slide across tile seams without catching. Changing a cell with SetCell rebuilds that chunk on the next step. A TileMapCollider2D on the map entity sets friction, restitution, layer and mask, or turns collision off.

One-way platforms block only from their up side, negative Y: set OneWay on a collider, or give tiles or a whole layer the bool property oneWay. Characters land on them from above and jump through from below.

Triggers are colliders with IsTrigger. They block nothing and report overlaps instead. Pickups, doors, checkpoints and damage zones are triggers. Two separate systems use the word trigger:

Physics triggersMap trigger areas
Made fromA Collider2D with IsTriggerA polygon object on a map's object layer (TriggerArea)
DetectsAny collider enteringEntities with a TriggerActivator component
Reported asOnTriggerEnter/Stay/Exit on scripts, PhysicsTriggerEntered/Stayed/Exited eventsTriggerEntered and TriggerExited events

Contact events​

After each step, the world reports what started touching, kept touching and stopped touching, in two forms.

On scripts, for the script's own entity: OnCollisionEnter, OnCollisionStay, OnCollisionExit and the three OnTrigger methods receive a ContactInfo seen from that entity. contact.Other is what it touched (a tile map reports its map entity) and contact.Normal points from the other entity toward this one, so a character standing on the ground gets a normal pointing up, (0, -1).

On the event bus, for every pair: CollisionEntered, CollisionStayed, CollisionExited and the PhysicsTrigger… events carry a PhysicsContact with both entities, a point, the normal from A to B, the step's NormalImpulse and the relative velocity. contact.For(entity) turns it into the ContactInfo seen from one side. A listener for the whole scene suits effects that do not belong to one entity:

assets/scripts/ImpactSounds.cs
namespace MyGame;

/// <summary>Plays a thud for hard impacts anywhere in the scene.</summary>
public sealed class ImpactSounds : Script
{
[Range(0, 2000)]
public float MinimumImpulse = 400;

protected override void OnStart() => Events.Subscribe((ref CollisionEntered e) =>
{
if (e.Contact.NormalImpulse > MinimumImpulse)
Audio.Play("audio/thud.wav");
});
}

Stay events fire every step while either body is awake, so keep their handlers cheap. Exit events also fire when an entity or its collider is removed, so the other entity may no longer be alive; check World.IsAlive before reading it. Delivering events allocates nothing.

Fast bodies​

Speculative contacts keep moderately fast bodies out of each other. A dynamic body that moves more than half its smallest size in one step is also swept against static shapes, so falling bodies never tunnel through floors or tiles. Set CollisionDetection to Continuous on bullets and other very fast bodies to sweep them against moving bodies too. Sweeps ignore rotation during the step.

Debugging​

The object debug view (F4 in a game window) outlines colliders colored by body type and state (static, dynamic, sleeping, kinematic, trigger), the collision built for tile maps, and contact points with their normals. It draws colliders from their components, so it works in every execution mode. PhysicsDebugOptions turns each part of the view on separately.

The performance overlay shows the markers Physics/Sync, Physics/Broadphase, Physics/Narrowphase, Physics/Solve, Physics/Continuous and Physics/Events, and the counters Physics bodies, Physics awake bodies and Physics contacts.

Performance and limitations​

Measured on a Ryzen 9 PRO 8945HS with every body awake, one fixed step with 1000 bodies takes 0.5 ms for a flying swarm and 1.3 ms for a resting pile; with 4000 bodies 1.5 to 7.2 ms depending on the setup. 1000 rays among 4000 shapes take 0.3 ms with the spatial hash. Steps and queries allocate nothing.

  • Let bodies sleep. Resting bodies fall asleep and cost almost nothing. Waking a pile every step, with forces on every body, turns the best case into the worst.
  • Use triggers and queries instead of bodies for things that only need to know about overlap.
  • Filter queries by layer. A ray with a narrow mask checks fewer shapes.
  • Prefer simple shapes. Boxes, circles and capsules are cheapest; a long polygon becomes several pieces.
  • Keep queries allocation-free. Pass a stackalloc span or a reused array for results.

The solver runs on one thread, so large piles of awake bodies are bound by it. Characters do not push dynamic bodies they walk into, and there are no joints yet.