271 lines
13 KiB
Markdown
271 lines
13 KiB
Markdown
---
|
|
id: physics
|
|
try: try/Body2D.aescene
|
|
try_select: physics.Body2D
|
|
icon: cat.physics
|
|
group: physics
|
|
group_title: Physics 2D
|
|
group_icon: cat.physics
|
|
group_order: 50
|
|
title: Physics 2D
|
|
order: 1
|
|
---
|
|
# Physics 2D
|
|
|
|
Aego simulates 2D rigid bodies with its own solver. A body is a node with **Rigid Body 2D**; colliders on that node and its children give it a shape; scripts react to contacts and query the world. Physics runs during Play and in `aego run`. In edit mode nothing moves, and the Scene View shows collider outlines and their problems.
|
|
|
|
Quick start: Create → Physics → Rigid Body gives a falling box, Create → Physics → Static Collider a floor for it. Press Play.
|
|
|
|
---
|
|
id: physics/units
|
|
icon: view.grid
|
|
parent: physics
|
|
order: 1
|
|
title: Units and axes
|
|
---
|
|
# Units and axes
|
|
|
|
Physics works in world units. One unit is whatever size you choose for your game; shapes between 0.1 and 10 units behave best. `render.pixelsPerUnit` in `project.aego` is used only for the default size of imported sprites, the default `Camera2D.Zoom` and **Snap to Sprite**; physics never reads it, so changing it does not change how anything falls.
|
|
|
|
Axes follow the 2D scene: +X right, +Y down, positive angles turn clockwise. Default gravity is `(0, 9.81)`, pointing down the screen. Normals and velocities in events and queries use the same axes, so a floor under a body has a normal with a negative Y.
|
|
|
|
---
|
|
id: physics/components
|
|
try: try/CircleCollider2D.aescene
|
|
try_select: physics.CircleCollider2D
|
|
icon: comp.rigidbody
|
|
parent: physics
|
|
order: 2
|
|
title: Components
|
|
---
|
|
# Components
|
|
|
|
| Component | Purpose |
|
|
|---|---|
|
|
| Rigid Body 2D | makes the node a body: `static`, `kinematic` or `dynamic` |
|
|
| Box / Circle / Capsule / Polygon / Edge Collider 2D | shapes |
|
|
| Distance Joint 2D | keeps a distance to the `Target` body, rigid or springy |
|
|
| Revolute Joint 2D | pins to the `Target` body at `Anchor`, with optional limit and motor |
|
|
|
|
Every collider has `Offset`, `IsTrigger`, `Friction`, `Restitution` and `Density`. Capsules lie along the node's local X axis; `Length` includes the caps. Polygons are convex with 3 to 8 points. Edge colliders are polylines for level geometry; `Loop` closes them, `OneSided` makes them solid only from the side the points wind towards (left-to-right points are solid from above).
|
|
|
|
Rigid Body 2D:
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| `Type` | `static` never moves, `kinematic` moves by velocity and ignores forces, `dynamic` reacts to everything |
|
|
| `MassMode`, `Mass` | `auto` takes mass from collider density, `override` uses `Mass` |
|
|
| `GravityScale` | multiplier of the project gravity |
|
|
| `LinearDamping`, `AngularDamping` | slow the body down over time |
|
|
| `FixedRotation` | the body never turns |
|
|
| `Bullet` | swept collision for small fast bodies |
|
|
| `AllowSleep` | let the body rest when it stops |
|
|
| `Interpolate` | draw between physics steps; scripts still see the exact pose |
|
|
| `Layer`, `Mask` | see Layers |
|
|
| `Velocity`, `AngularVelocity`, `Awake` | live values during Play, read-only |
|
|
|
|
---
|
|
id: physics/hierarchy
|
|
icon: panel.hierarchy
|
|
parent: physics
|
|
order: 3
|
|
title: Bodies and hierarchy
|
|
---
|
|
# Bodies and hierarchy
|
|
|
|
A collider belongs to the nearest node at or above it that has a Rigid Body 2D. One body can therefore be built from several child nodes, each with its own collider; mass, centre of mass and inertia come from all of them. A collider with no body above it becomes a static body of its own.
|
|
|
|
The collider's position, rotation and uniform scale relative to its body are baked into the shape. Boxes and polygons also take non-uniform scale; circles and capsules need a uniform one and report PHY001 otherwise. Nodes under a 3D parent are not simulated (PHY005).
|
|
|
|
The transform of a moving body belongs to physics. Write it from `FixedUpdate` or through `Teleport` and it is applied as a teleport at the next step, keeping the velocity. A write from `Update` is reported once per body (PHY007) and applied at the next step. Static and kinematic bodies follow their node every step. Reparenting a moving body during Play recreates it with the same world pose and velocity (PHY006).
|
|
|
|
---
|
|
id: physics/layers
|
|
icon: view.overlays
|
|
parent: physics
|
|
order: 4
|
|
title: Layers
|
|
---
|
|
# Layers
|
|
|
|
Up to 32 named layers and the pairs that collide are declared in `project.aego` or in Project Settings → Physics:
|
|
|
|
physics {
|
|
layers=[Default, Ground, Player, Enemy]
|
|
collide {
|
|
Player=[Ground, Enemy]
|
|
Enemy=[Ground, Player]
|
|
}
|
|
}
|
|
|
|
The matrix is symmetric: `Player=[Enemy]` also lets enemies hit players. Without a `collide` block every layer collides with every other. An unknown layer name in `collide` is a load error.
|
|
|
|
A body stores its layer as an index, so renaming a layer keeps every body on it; clearing a name keeps the index. Two shapes collide when both bodies' `Mask` accept the other's layer and the matrix allows the pair. Changing a layer or mask during Play ends the contacts it forbids in the next step.
|
|
|
|
---
|
|
id: physics/events
|
|
icon: panel.events
|
|
parent: physics
|
|
order: 5
|
|
title: Events
|
|
---
|
|
# Events
|
|
|
|
Declare a method with one of these names in any script on the body node or on the collider node:
|
|
|
|
func OnCollisionEnter(other NodeRef, c physics.Contact)
|
|
func OnCollisionStay(other NodeRef, c physics.Contact)
|
|
func OnCollisionExit(other NodeRef, c physics.Contact)
|
|
func OnTriggerEnter(other NodeRef)
|
|
func OnTriggerExit(other NodeRef)
|
|
|
|
Both participants get the event, each from its own side. `other` is the other body's node; `c.Normal` points from the other body into the receiver, so a floor under you reports `c.Normal.Y < 0`. `c.SelfCollider` and `c.OtherCollider` name the collider nodes. Events run after the physics step, before `script.events_fixed` ends the fixed tick.
|
|
|
|
`OnCollisionStay` runs every step while two bodies touch, and only when some script handles it; prefer Enter/Exit counters. Exit also comes when a body is destroyed or disabled; `other.IsAlive()` is then false.
|
|
|
|
Destroying nodes inside a handler is safe: the removal happens after all handlers ran.
|
|
|
|
---
|
|
id: physics/scripting
|
|
icon: file.script
|
|
parent: physics
|
|
order: 6
|
|
title: Scripting
|
|
---
|
|
# Scripting
|
|
|
|
Body methods are called on the component:
|
|
|
|
body := node.Get<Body2D>()
|
|
v := body.Velocity
|
|
body.SetVelocity(Vec2{x * Speed, v.Y})
|
|
body.ApplyImpulse(JumpImpulse)
|
|
|
|
| Method | |
|
|
|---|---|
|
|
| `ApplyForce(f)`, `ApplyForceAt(f, p)`, `ApplyTorque(t)` | push during the next step, call from `FixedUpdate` |
|
|
| `ApplyImpulse(j)`, `ApplyImpulseAt(j, p)` | change the velocity at once, scaled by mass |
|
|
| `SetVelocity(v)`, `SetAngularVelocity(w)` | |
|
|
| `MoveTo(p, angle)` | drive a kinematic body to a pose by the end of the next step |
|
|
| `Teleport(p, angle)` | move without sweeping, velocity kept |
|
|
| `WakeUp()`, `Sleep()`, `Mass()` | |
|
|
|
|
Queries live in `aego/physics`. They ignore the caller's own body and skip triggers:
|
|
|
|
| Function | |
|
|
|---|---|
|
|
| `Raycast(origin, dir, maxDist) RayHit` | closest hit |
|
|
| `Linecast(from, to, mask) RayHit` | first hit on the layers in `mask`, `-1` for all |
|
|
| `RaycastAll(from, to, out) int` | fills `out` nearest first |
|
|
| `OverlapPoint(p, out)`, `OverlapCircle(c, r, out)`, `OverlapBox(c, size, out)` | bodies touching the area |
|
|
| `Gravity()`, `SetGravity(g)` | |
|
|
| `Layer(name)`, `LayerMask(name)` | layer index and mask bit by name |
|
|
|
|
Functions that fill `out` write at most `len(out)` entries; size the slice once with `append`. Queries work only during Play.
|
|
|
|
---
|
|
id: physics/settings
|
|
icon: panel.settings
|
|
parent: physics
|
|
order: 7
|
|
title: Settings
|
|
---
|
|
# Settings
|
|
|
|

|
|
|
|
Project Settings → Physics edits the `physics` block of `project.aego`: gravity, substeps, iterations, contact stiffness, sleep thresholds, layers and the matrix. An empty or zero field takes the engine default, shown greyed out. Substeps and both iteration counts accept 1 to 64. Changes apply at the next Play.
|
|
|
|
Tall free-standing stacks and fast motors need the stiff profile: `substeps=8` and `contactHertz=60`, at about twice the solver cost. At the default 30 Hz with 4 substeps a free column stands up to about twelve boxes; bonded brick layers stand much higher.
|
|
|
|
---
|
|
id: physics/editor
|
|
try: try/EdgeCollider2D.aescene
|
|
try_select: physics.EdgeCollider2D
|
|
icon: tool.move
|
|
parent: physics
|
|
order: 8
|
|
title: Editor tools
|
|
---
|
|
# Editor tools
|
|
|
|

|
|
|
|
- **Handles.** Select a node with a collider to drag its sides, radius, length or vertices; double-click an edge of a polygon to add a vertex, Alt-click a vertex to remove it. Joints show anchors on both bodies. Every drag is one undo step.
|
|
- **Snap to Sprite** in the inspector of box, circle and capsule colliders fits the shape to the node's sprite.
|
|
- **Physics menu.** Debug Overlay with Shapes, Contacts, AABBs, Joints, Islands and Broadphase Tree. In edit mode only shapes are drawn.
|
|
- **Vectors** (V in the Scene View) draws velocity, angular velocity, forces, contact impulses, gravity and script fields with `@export(gizmo="vector")`. Drag the tip of a field arrow to change the value.
|
|
- **Game View → Overlays** draws the same layers over the running game with the game camera.
|
|
- **Problems** lists collider and joint errors with a link to the node; broken geometry blocks Play.
|
|
- **Profiler → Physics** shows bodies, pairs, islands and stage timings during Play.
|
|
|
|
---
|
|
id: physics/lifecycle
|
|
icon: panel.history
|
|
parent: physics
|
|
order: 9
|
|
title: Lifecycle
|
|
---
|
|
# Lifecycle
|
|
|
|
What happens to bodies, shapes and contact events when the scene changes during Play.
|
|
|
|
| Action during Play | Result |
|
|
|---|---|
|
|
| Destroy a body node | its shapes leave the world at the next step, Exit events in the same step |
|
|
| Destroy a collider child | the shape is removed, mass and inertia recomputed |
|
|
| Disable a node | all shapes below it switch off with Exit events; enabling brings Enter at the next step |
|
|
| Remove Rigid Body 2D | child colliders become static bodies |
|
|
| Remove a collider | the shape is removed with Exit |
|
|
| Change layer or mask | forbidden pairs end in the same step |
|
|
| Change a trigger flag or the geometry | contacts of the shape end, new ones begin at the next step |
|
|
| Add a body or collider | picked up at the next step, starting at rest |
|
|
| Reparent a moving body | recreated with world pose and velocity, PHY006 |
|
|
| Script hot reload | the world keeps running, handlers are rebound by name |
|
|
| Native rebuild | the play world is recreated with the editor |
|
|
| Stop | the world is destroyed without Exit events |
|
|
|
|
---
|
|
id: physics/world
|
|
icon: vec.gravity
|
|
parent: physics
|
|
order: 10
|
|
title: World
|
|
---
|
|
# World
|
|
|
|
`modules/physics/world` is the solver under the components, usable on its own from Go (see `demos/physics2d`).
|
|
|
|
`Step(dt)` collides once and then solves in `Config.Substeps` substeps (4 by default): each substep applies gravity, solves contacts as stiff springs that push overlapping shapes apart at most `ContactPushVelocity` units per second, moves the bodies and relaxes the push so it does not turn into a bounce. The engine steps physics at the fixed rate (`FixedHz`, 60 by default), never with the frame delta.
|
|
|
|
A body moves at most `MaxTranslationPerStep` units and turns at most `MaxRotationPerStep` radians per step. Fast small bodies should set `Bullet`: their contacts look ahead by the distance they travel in one step.
|
|
|
|
Every definition is validated: NaN, infinities, negative sizes, concave polygons and zero-length segments are rejected with an error and the world stays unchanged.
|
|
|
|
| Material | Meaning | Mix of two shapes |
|
|
|---|---|---|
|
|
| `Friction` | 0 is ice, 1 is rubber | `sqrt(a·b)` |
|
|
| `Restitution` | 0 stops, 1 bounces back at full speed | `max(a, b)` |
|
|
| `Density` | mass per square unit | — |
|
|
|
|
Bounces slower than `RestitutionThreshold` are not restituted. A group of touching bodies falls asleep when all of them move slower than `SleepLinear` and `SleepAngular` for `SleepTime` seconds; a contact with an awake body, a force, an impulse, a teleport or a new joint wakes it.
|
|
|
|
The same binary on the same CPU with the same sequence of calls produces bit-identical results; `Snapshot()` and `Restore()` capture the whole state for replays and rollback.
|
|
|
|
---
|
|
id: physics/pitfalls
|
|
icon: status.warning
|
|
parent: physics
|
|
order: 11
|
|
title: Common mistakes
|
|
---
|
|
# Common mistakes
|
|
|
|
- **Moving a dynamic body with SetPosition from Update.** Use `SetVelocity`, forces or `Teleport` from `FixedUpdate`.
|
|
- **Reading input edges in FixedUpdate.** `input.Pressed` is true for one frame, and a frame may have zero or two fixed steps. Read it in `Update` and act in `FixedUpdate`, or use `input.Down`.
|
|
- **Circle on a stretched node.** Circles and capsules need a uniform scale. Scale the collider fields instead.
|
|
- **Objects falling through thin floors.** Mark small fast bodies as `Bullet`, or make the floor thicker.
|
|
- **Wobbling tall towers.** Use the stiff profile, stagger the boxes like bricks, or build the tower as one body.
|
|
- **Nothing collides.** Check both masks, the layer matrix, and that the node is 2D and not under a 3D parent.
|
|
- **Units that feel floaty.** A 64-pixel sprite at zoom 64 is one unit; gravity 9.81 assumes a unit is about a metre.
|