# gander physics 2D physics (Box2D v3), 3D physics (Jolt), a walking character controller, ragdolls and cloth. Everything is in `gander::` (module `physics.goose`, part of the package). Bodies, joints, characters, ragdolls and cloth are handles: `Body2`, `Joint2`, `Body`, `Joint`, `Character`, `Ragdoll`, `Cloth` (`.id == 0` is none). ## Rules that bite - **Physics only moves when you step it.** Call `gander::physics2d_step(dt)` (2D) and/or `gander::physics_step(dt)` (3D) once per frame, before reading positions and before drawing. Both run fixed 1/60 s ticks inside, so the simulation is frame-rate independent and deterministic in `gander test`. - **Physics does not draw.** Each frame, read the body's position and draw your sprite / mesh / model there (`gander::body_xform(b)` is a ready `Mat4` for 3D; it has no scale, so multiply your mesh scale on the right: `gander::mul(gander::body_xform(b), gander::scale(size))`). - Shapes are created **centered**. `body_box` takes the full width/height in 2D but **half extents** in 3D. - Kinds: `gander::STATIC` (never moves), `gander::DYNAMIC` (simulated), `gander::KINEMATIC` (moved by you, pushes dynamic bodies). - Contact / sensor events cover only the latest step call: read them right after stepping, every frame. - `gander::physics_debug_draw()` (3D) draws every body's bounds: green awake, grey asleep. Use it when something falls through the floor. ## 2D (pixels, +Y down, radians clockwise) Scale: 32 px = 1 m (`physics2d_scale(px)`), gravity 9.8 m/s^2 down (`physics2d_gravity(x, y)` in px/s^2). Defaults for new shapes: `physics2d_defaults(density, friction, bounce)` (1, 0.6, 0). | Create | | | --- | --- | | `body2d_box(x, y, w, h, kind) -> Body2` | center + full size | | `body2d_circle(x, y, r, kind)` | | | `body2d_capsule(x, y, h, r, kind)` | standing capsule, total height h (characters) | | `body2d_polygon(x, y, points: f32[], kind)` | convex, 3..8 points (x0, y0, x1, y1...) relative to (x, y) | | `body2d_chain(points: f32[], looped)` | static ground polyline in world pixels (hills, caves), at least 4 points | | `body2d_sensor(x, y, w, h)` | static trigger area | | `body2d_add_box(b, ox, oy, w, h, angle)`, `body2d_add_circle(b, ox, oy, r)` | compound shapes | | `body2d_destroy(b)`, `body2d_valid(b)` | | State: `body2d_pos(b) -> float2`, `body2d_angle`, `body2d_vel -> float2` (px/s), `body2d_spin` (rad/s), `body2d_set_pos/_set_angle/_set_vel/_set_spin`, `body2d_impulse(b, ix, iy)`, `body2d_impulse_at(b, ix, iy, px, py)`, `body2d_force`, `body2d_torque`, `body2d_mass(b) -> f32`. Behavior: `body2d_fixed_rotation(b, true)` (characters), `body2d_gravity_scale`, `body2d_damping(b, linear, angular)`, `body2d_bullet(b, true)` (fast objects), `body2d_friction`, `body2d_bounce`, `body2d_density`, `body2d_filter(b, category_bits, mask_bits)`, `body2d_tag(b, i32)` / `body2d_get_tag(b)`, `body2d_on_ground(b) -> bool` (a contact pushing up from below within ~45 degrees). Events and queries: - `physics2d_contact_count()`, `physics2d_contact(i) -> Contact2 { a, b, x, y, nx, ny, began }` (began false: they separated; normal points from a to b). - `physics2d_sensor_count()`, `physics2d_sensor_event(i) -> Contact2` (a = the sensor, b = the visitor). - `raycast2d(x1, y1, x2, y2, mask) -> Hit2 { body, x, y, nx, ny, fraction }` (`body.id == 0`: no hit; mask 0 = all). - `physics2d_overlap_box(x, y, w, h) -> count`, `physics2d_overlap_circle(x, y, r) -> count`, then `physics2d_overlap_body(i)`. Joints (anchors in world pixels): `joint2d_revolute(a, b, x, y)`, `joint2d_distance(a, b, ax, ay, bx, by)`, `joint2d_prismatic(a, b, x, y, axis_x, axis_y)`, `joint2d_weld(a, b, x, y)`, `joint2d_mouse(ground, b, x, y)` + `joint2d_target(j, x, y)` (drag with the pointer), `joint2d_motor(j, speed, max_force)` (revolute rad/s, prismatic px/s), `joint2d_limits(j, lo, hi)` (radians / pixels), `joint2d_destroy(j)`. ## 3D (meters, +Y up) Gravity -9.81 (`physics_gravity(v)`). | Create | | | --- | --- | | `body_box(pos, half, kind) -> Body` | half extents | | `body_sphere(pos, r, kind)` | | | `body_capsule(pos, h, r, kind)`, `body_cylinder(pos, h, r, kind)` | upright, total height h | | `body_convex(model, xf, kind)`, `body_convex_mesh(mesh, xf, kind)` | convex hull of a model / mesh placed at xf (props, debris) | | `body_mesh(model, xf)`, `body_mesh_mesh(mesh, xf)` | exact triangles, static only (levels, buildings, rocks) | | `body_heightfield(heights: f32[], samples_x, samples_z, cell, origin)` | static terrain, `heights[z * samples_x + x]`, origin = sample (0, 0) | Density is water-like (1000 kg/m^3), so a 1 m box is 1000 kg; set real masses with `body_mass(b, kg)`. Default friction 0.6, bounce 0. State: `body_xform(b) -> Mat4`, `body_pos`, `body_rot -> float4`, `body_vel`, `body_spin`, `body_set_pos/_set_rot/_set_xform/_set_vel/_set_spin`, `body_impulse(b, i)`, `body_impulse_at(b, i, point)`, `body_force`, `body_torque`, `body_move(b, pos, rot, dt)` (kinematic: doors, platforms, elevators), `body_friction`, `body_bounce`, `body_mass`, `body_damping(b, linear, angular)`, `body_gravity_scale`, `body_ccd(b, true)` (fast small things), `body_sensor(b, true)` (trigger, reported as contacts), `body_tag(b, i32)` / `body_get_tag(b)` (-1 for ragdoll parts), `body_layer(b, l)`, `body_wake(b)`, `body_awake(b)`, `body_destroy(b)`, `body_valid(b)`. Layers 0..15: `LAYER_STATIC` 0 (static bodies), `LAYER_DYNAMIC` 1 (everything else), `LAYER_CHARACTER` 2, `LAYER_RAGDOLL` 3, `LAYER_DEBRIS` 4 (hits only static), 5..15 yours. All pairs collide except static/static, ragdoll/character and debris/non-static; change with `physics_collide(a, b, on)`. Query masks are layer bits (`1 << layer`), 0 = all. Queries: - `raycast(origin, dir, max_dist, mask, ignore) -> RayHit { body, dist, point, normal }` (`dist < 0`: no hit; `ignore`: a body to skip, e.g. `character_body(player)`). `raycast(origin, dir, max_dist)` hits anything. - `spherecast(origin, radius, dir, max_dist, mask, ignore) -> RayHit`. - `physics_overlap_sphere(center, r, mask) -> count` then `physics_overlap_body(i)`. - `physics_contact_count()`, `physics_contact(i) -> Contact { a, b, point, normal, speed, began }` (speed = closing speed: play an impact sound when it is above ~2). Joints (world anchors; `gander::NO_BODY` as b attaches to the world): `joint_hinge(a, b, point, axis, min_deg, max_deg)` (min >= max: free), `joint_fixed(a, b)`, `joint_point(a, b, point)`, `joint_cone(a, b, point, twist_axis, swing_deg, twist_min, twist_max)`, `joint_distance(a, b, pa, pb, min, max)`, `joint_motor(j, rad_per_s, max_torque)` (hinges), `joint_destroy(j)`. ## Character controller `character(feet, height, radius) -> Character`: a capsule that walks. It steps up stairs (0.4 m), sticks to slopes up to 50 degrees, rides moving platforms, pushes dynamic bodies, collides with other characters, and has an inner body on `LAYER_CHARACTER` so raycasts can hit it. - `character_update(c, move, jump, dt)` once per frame: `move` = desired horizontal velocity (m/s, world), `jump` = upward speed when it should jump (only used on the ground, 0 otherwise). Gravity and air control are handled. - `character_pos(c)` (feet), `character_vel`, `character_on_ground`, `character_ground_normal`, `character_set_pos`, `character_set_vel` (knockback), `character_height(c, h) -> bool` (crouch; false when blocked above), `character_body(c)`, `character_destroy(c)`. - `character_limits(c, step, max_slope_deg)`: the tallest step walked up (default 0.4 m) and the steepest slope walked on (default 50 degrees; crater banks 58). ## Ragdolls `ragdoll(anim) -> Ragdoll` lays 12 bodies on a character's skeleton by bone name (Hips, Spine*, Neck, Head, Left/Right Arm, ForeArm, Hand, UpLeg, Leg, Foot; Meshy, Mixamo, Unreal and Blender names all map): pelvis, spine, chest, head, upper and lower arms and legs, cone joints at hips / shoulders / spine / neck, hinges that bend only the natural way at knees and elbows. - Draw the character with `draw_ragdoll(r, xf)` **always** (alive too): it animates normally and records the pose so a ragdoll keeps the motion it had. - `ragdoll_start(r, impulse, hit_point)` on death: goes limp from the current pose, the impulse (N s; a rifle round ~40..80 along the bullet) goes into the body nearest hit_point. - `ragdoll_state(r)`: `RAGDOLL_ALIVE`, `RAGDOLL_FALLING`, `RAGDOLL_SETTLED` (baked into a static pose after it rests, or after 4.2 s; costs nothing afterwards). At most 8 fall at once (`ragdoll_max_active(n)`, 1..32); starting one more settles the oldest. - `ragdoll_limits(r, part, swing, twist)`: joint limits in degrees for one part ("head", "l_upperarm", "r_shin" ...): swing is the cone half angle (for elbows and knees the largest bend), twist the turn about the bone; -1 keeps the default. `ragdoll_damping(r, linear, angular)` (0.1 / 0.6): higher falls limper and settles sooner. Both apply from the next `ragdoll_start`. - On a character whose model is still streaming (`model_async`) the ragdoll waits: its calls do nothing until the model arrives. - `ragdoll_impulse(r, impulse, point)` while falling (explosions), `ragdoll_reset(r)` back to animation (respawn). - `ragdoll_bone_hit(r, point, out_name) -> part` for hit-location damage: writes the bone name ("Head", "LeftLeg") and returns 0 pelvis, 1 spine, 2 chest, 3 head, 4/5 left arm, 6/7 right arm, 8/9 left leg, 10/11 right leg. - `body_ragdoll(b) -> Ragdoll` tells which ragdoll a raycast hit. ## Cloth Capes, flags, banners, curtains and tarps: a sheet simulated in `physics_step` (Jolt soft bodies). It is pushed by bodies, characters and ragdolls but never blocks them, and raycasts and shape casts pass through it, so a curtain never stops a player or a bullet. - `cloth(ClothDesc { .. }, xf) -> Cloth` makes the sheet in its pin frame `xf`: x across, hanging down -y, the front facing +Z. - `width`, `height` in metres. - `pin`: `CLOTH_PIN_TOP` (top edge centred on the frame: capes, curtains), `CLOTH_PIN_LEFT` (left edge, running out along +x: a flag from its pole), `CLOTH_PIN_CORNERS` (the two top corners: banners on hooks), `CLOTH_PIN_NONE` (nothing: it falls and drapes over what is below). - `weight` (kg/m^2: 0.1 silk, 0.25 a flag, 0.5 a heavy cape, 1 canvas), `bend` (0 drapes like silk .. 1 holds like canvas), `damping`, `wind` (how much `cloth_wind` moves it). - `backstop`: capes only (see below). `detail`: grid spacing, leave it 0. - `swing_top`, `swing_bottom`: swing limits for pinned sheets (Unreal's painted max distance): how far in metres each row may move from where it hangs in the pin's frame, from the pinned edge to the hem, linear down the rows. A cape: 0.03 at the shoulders, 0.5 at the hem, so it moves with the stride and the wind but never flies up over the head. -1 (default) off. - `cloth_pin(c, xf)`: move the pinned edge. Every frame **before** `physics_step` for a cape; never for a flag that stays put. A jump of more than 4 m carries the whole sheet (respawn, teleport) instead of stretching it. - `draw_cloth(c, material)` every frame. Both sides are lit with any material; a texture's u runs across from the left edge, v down from the top. - `draw_cloth_lined(c, outer, lining)`: instead of `draw_cloth`, a sheet with a lining: `outer` on the front (+Z as made: away from the wearer for a cape), `lining` on the back. Give both single-sided materials. - `cloth_swing_scale(c, k)`: scale every swing limit at run time (1 as made, 1.5 looser while sprinting or in a gale, 0.5 tighter while kneeling). - `cloth_wind(float3 m/s, gust 0..1)`: wind on every sheet (5 m/s makes a flag stand out, 12 is a gale; gust 0.5 gives ripples and gusts). Wind only pushes a sheet facing it: a flag side-on to the wind hangs. - `cloth_push(c, at, radius, vel)`: shove it around a point (a shot through a flag, a blast, an NPC brushing a curtain). - `cloth_point(c, u, v) -> float3`: a point on the sheet as drawn (hang a lantern on a banner, put a sound on a flag's tip). - `cloth_ignore(c, body)`: something the sheet passes through: a cape's wearer (`character_body(player)`), so it never fights the capsule. - `cloth_capsule(c, bone_a, bone_b, radius) -> i32`: a capsule between two bones of the wearer that the sheet stays out of (thighs, shins, arms, a sword; `bone_b` "" makes a sphere at `bone_a`). Make them once, up to 24. - `cloth_wearer(c, anim, xf)`: every frame next to `cloth_pin`, the wearer's animator and the matrix it is drawn with, so the capsules follow its bones. - `cloth_free(c)`. Costs about 0.7 microseconds per vertex per frame. The default grid is 8 cm (5 cm in Pro builds, at most 32 x 32 vertices, 48 x 48 in Pro): a cape is about 0.1 ms, a 2 m curtain about 0.6 ms on the web. Keep it to a few sheets near the player, and `cloth_free` sheets far away. **Capes.** Characters face -Z, so the cape goes on their back at +Z. Pin it with the same matrix you draw the character with, moved to the shoulders; use only the spine bone's position, so the cape hangs straight down from the character instead of twisting with every step of the animation. `backstop` (about 0.02) keeps the cape from swinging into the body, with no colliders on the body: ```goose var cape: gander::Cloth; fn cape_frame(a: gander::Anim, xf: gander::Mat4) -> gander::Mat4 { let s = gander::position(gander::bone(a, "spine_03")); // "Spine2" on Mixamo rigs gander::mul(xf, gander::translate(gander::v3(0.0, s.y + 0.1, s.z + 0.17))) } // main: after the animator exists cape = gander::cloth(gander::ClothDesc { width: 0.6, height: 1.05, pin: gander::CLOTH_PIN_TOP, weight: 0.5, bend: 0.2, backstop: 0.02 }, cape_frame(anim, hero_xf)); gander::cloth_ignore(cape, gander::character_body(player)); // frame gander::cloth_pin(cape, cape_frame(anim, hero_xf)); // before physics_step gander::physics_step(dt); gander::draw_animated(anim, hero_xf); gander::draw_cloth(cape, cape_mat); ``` **A lined cape with a collar.** Swing limits keep the hem's reach believable without colliders; the lining shows where the cape turns over; a collar is a second short, stiff sheet pinned a little higher (it is drawn over the cape's top edge): ```goose cape = gander::cloth(gander::ClothDesc { width: 0.6, height: 1.05, weight: 0.5, bend: 0.2, backstop: 0.02, swing_top: 0.03, swing_bottom: 0.5 }, cape_frame(anim, hero_xf)); collar = gander::cloth(gander::ClothDesc { width: 0.5, height: 0.14, weight: 0.8, bend: 0.9, swing_top: 0.0, swing_bottom: 0.04 }, gander::mul(cape_frame(anim, hero_xf), gander::translate(gander::v3(0.0, 0.05, 0.02)))); // frame: pin both, then gander::draw_cloth_lined(cape, wool_outer, linen_lining); gander::draw_cloth_lined(collar, fur, fur); ``` **A cape that wraps the body.** `backstop` alone keeps a cape off the back, but a stride or a swing still passes through it. Capsules on the limbs make it drape over them instead: after every physics tick, any part of the sheet inside one is put on its surface (a fast swing cannot tunnel through). Size each a little wider than the limb with its armour; a sword's capsule goes from the hand to the blade's socket. Keep the cape's `backstop` small (0.02) with capsules: a loose one lets it swing forward into the legs and bunch. Keep the pinned edge out of every capsule (the engine warns when one holds it: narrow the cape or the capsule). ```goose // main, after cloth(): legs, arms and the blade gander::cloth_capsule(cape, "thigh_l", "calf_l", 0.10); gander::cloth_capsule(cape, "thigh_r", "calf_r", 0.10); gander::cloth_capsule(cape, "calf_l", "foot_l", 0.07); gander::cloth_capsule(cape, "calf_r", "foot_r", 0.07); gander::cloth_capsule(cape, "upperarm_r", "lowerarm_r", 0.07); gander::cloth_capsule(cape, "hand_r", "blade_tip", 0.04); // a socket (gander glb socket) // frame, before physics_step gander::cloth_pin(cape, cape_frame(anim, hero_xf)); gander::cloth_wearer(cape, anim, hero_xf); ``` **Flag on a pole.** The frame sits at the top of the pole; the flag runs out along +x and turns with the wind: ```goose flag = gander::cloth(gander::ClothDesc { width: 1.8, height: 1.1, pin: gander::CLOTH_PIN_LEFT, weight: 0.2, bend: 0.15 }, gander::place(pole_top, 0.0)); gander::cloth_wind(gander::v3(6.0, 0.0, 1.0), 0.5); ``` **Tarp over crates.** `CLOTH_PIN_NONE` with the frame tipped flat (the sheet's down becomes +z), a metre above what it should cover: ```goose let xf = gander::trs(gander::v3(x - 1.1, top + 1.0, z - 1.1), gander::quat_axis(gander::v3(1.0, 0.0, 0.0), -1.5708), gander::v3(1.0, 1.0, 1.0)); tarp = gander::cloth(gander::ClothDesc { width: 2.2, height: 2.2, pin: gander::CLOTH_PIN_NONE, weight: 0.8 }, xf); ``` ## Recipes FPS player: ```goose var player: gander::Character; var yaw: f32 = 0.0; var pitch: f32 = 0.0; // main(): player = gander::character(gander::v3(0.0, 0.0, 0.0), 1.8, 0.3); export "gander_frame" fn frame(dt: f32) { let look = gander::look_axis(); yaw -= gander::deg(look.x); pitch = clamp(pitch - gander::deg(look.y), -1.4, 1.4); let mv = gander::move_axis(); let fwd = gander::v3(-sin(yaw), 0.0, -cos(yaw)); let right = gander::v3(cos(yaw), 0.0, -sin(yaw)); let speed: f32 = if gander::action_down("sprint") { 6.5 } else { 4.0 }; let jump: f32 = if gander::action_pressed("jump") { 5.0 } else { 0.0 }; gander::character_update(player, (fwd * mv.y + right * mv.x) * speed, jump, dt); gander::physics_step(dt); let eye = gander::character_pos(player) + gander::v3(0.0, 1.65, 0.0); gander::camera_xform(gander::trs(eye, gander::quat_euler(pitch, yaw, 0.0), gander::v3(1.0, 1.0, 1.0)), 70.0); } ``` Bullets (hitscan) with ragdoll deaths and headshots: ```goose let eye = gander::camera_pos(); let dir = gander::camera_forward(); let hit = gander::raycast(eye, dir, 200.0, gander::MASK_ALL, gander::character_body(player)); // enemies are drawn with draw_ragdoll; find who was hit by distance to the hit point // (or keep a capsule body per enemy on a game layer and use its tag) var bone: u8[>..] = []; let part = gander::ragdoll_bone_hit(enemy_doll, hit.point, bone); if part == 3 { damage = 100; } if enemy_hp <= 0 { gander::ragdoll_start(enemy_doll, dir * 60.0, hit.point); } ``` Platformer (2D): ```goose // main(): hero = gander::body2d_capsule(80.0, 600.0, 56.0, 14.0, gander::DYNAMIC); // gander::body2d_fixed_rotation(hero, true); let v = gander::body2d_vel(hero); var vy = v.y; if gander::action_pressed("jump") && gander::body2d_on_ground(hero) { vy = -420.0; } gander::body2d_set_vel(hero, gander::move_axis().x * 260.0, vy); gander::physics2d_step(dt); let p = gander::body2d_pos(hero); // draw the hero sprite at p ``` Coins / triggers (2D): `coin = gander::body2d_sensor(x, y, 30.0, 30.0)`, then after stepping loop `physics2d_sensor_event(i)` and check `e.began && e.a.id == coin.id`. Cars and trucks (3D): use the vehicle API, never hinge-jointed wheels (no suspension, they hop and flip). Build a dynamic chassis, then one call makes the whole car: suspension, engine with torque curve, automatic gearbox, differentials, anti-roll bars, tyre grip, lowered centre of mass. ```goose car_body = gander::body_box(gander::v3(0.0, 1.5, 0.0), gander::v3(0.9, 0.4, 2.0), gander::DYNAMIC); gander::body_mass(car_body, 1300.0); // wheelbase 2.6, track 1.6, wheels at y -0.3 (chassis-local), radius 0.38, travel 0.3 m, all-wheel drive car = gander::vehicle_car(car_body, 2.6, 1.6, -0.3, 0.38, 0.3, 2); gander::vehicle_tune(car, 700.0, 7500.0, 32.0, 0.8); // torque, redline, steering lock, grip (gravel) gander::vehicle_suspension(car, 1.5, 0.5); // soft rally springs // every frame, before physics_step: let m = gander::move_axis(); gander::vehicle_drive(car, m.y, m.x, if gander::action_down("brake") { 1.0 } else { 0.0 }, if gander::action_down("handbrake") { 1.0 } else { 0.0 }); gander::physics_step(dt); // draw: the body from body_xform(car_body); each wheel (axle along X) from // vehicle_wheel_xform(car, i), e.g. a wheel model authored axle-along-X, or // MESH_CYLINDER times gander::rotate_z(1.5708) (its axis is Y) for i: i32 in 0..4 { gander::draw_model(wheel, gander::vehicle_wheel_xform(car, i)); } // HUD and feel: vehicle_speed (m/s, x 3.6 = km/h), vehicle_rpm (engine // sound pitch), vehicle_gear; vehicle_wheel_slip(car, i) x/y past ~0.2/0.15 // = dust, skid marks and tyre squeal; vehicle_wheel_contact for airtime. ``` Forward is the chassis's -Z, wheels 0..3 are front left, front right, rear left, rear right. Throttle below zero reverses once stopped. Drive on a `body_heightfield` (terrain) or meshes: the wheels cast cylinders, so kerbs, rocks and ramps work. Thrown objects / debris: `body_convex(model, xf, gander::DYNAMIC)`, then `body_impulse`; put small debris on `LAYER_DEBRIS` so it never blocks players. Terrain: build the height grid you render with, then `body_heightfield(heights, nx, nz, cell, origin)` so characters and ragdolls stand on it.