Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

Latch

A latch point: a place on a body where it latches to a matching point on another body that comes within reach. A trailer hitch takes the coupler of a trailer backed onto it, two train cars couple on contact, a ship docks at a port lined up within an angle, a door catches as it swings shut, a magnet clicks onto a plate, a part snaps into a socket.

Public fields: tag, accepts, offset, radius, maxAngle, maxSpeed, armed, lock, joint, partner (written by the simulation).

Methods: :release(), :onLatch(fn), :onRelease(fn), :latchJoint(), :state().

When two points latch

The point is a frame: the entity's own frame, moved by offset. Put the Latch on the body itself, or on an entity under the body (a marker at the hitch ball, at a coupler, at a port), whose body the point rides on. After every physics step, two points latch when all of these hold:

  • each accepts the other's tag (an empty accepts latches to points of its own tag, so two train couplers tagged coupler latch to each other);
  • they ride on different bodies, both points are armed, and neither is latched;
  • they stand within the larger of their two radius (metres);
  • the turn that takes one frame onto the other is within the smaller of their two maxAngle (degrees; 180 latches turned any way);
  • they close no faster than the smaller of their positive maxSpeed (m/s; 0 latches at any speed).

A point near several takes the nearest. The step that makes a latch joins the two with a Joint and the next step holds it.

The joint a latch makes

The Joint is named latch, on the entity of the point that makes it, and holds the two frames together: point on point and turned alike. So lay out each point where, and turned as, its partner will stand once joined: a coupler marker on a trailer turned as the hitch marker on the truck is when the trailer stands straight behind it. Its fields come from joint (the making point's, else its partner's, else a fixed joint; the making point is the one onLatch names as host, so state joint on one point of a pair): any Joint kind and any Joint field but the anchors and ends the latch states, so a ball leaves the trailer free to swing, a hinge about axis leaves one turn free, a fixed joint docks, and breakForce / breakTorque make a catch that bursts open. It reads like any joint: :latchJoint() returns it, and Physics.jointReaction, Physics.jointState and Physics.bodyState(id).joints take it.

Releasing, breaking, latching again

  • :release() (or Physics.releaseLatch(id)) takes the joint off and frees both points. lock = "permanent" on either point refuses it: the latch holds until its joint breaks or a point is removed.
  • Taking the latch's Joint off by hand (Physics.removeJoint(latch:latchJoint())) releases the latch too, with reason "release".
  • A latch joint past its breakForce / breakTorque frees both points.
  • Removing a Latch, or its entity, frees its partner on the next step.

A freed point latches again once it has stood clear of every point it latches to, so pulling a trailer away and backing it in again hitches it again. :onLatch(fn) and :onRelease(fn) hear each latch and each release (reason "release", "break", "removed" or "refused"); Physics.onLatch and Physics.onUnlatch hear every latch in the world.

Refused

A Latch refuses, naming the field: an empty tag, an accepts entry that is not a tag, a negative radius, a maxAngle outside 0 to 180, a negative maxSpeed, a joint.kind that is not a name, a joint stating connected, target, name, localAnchor or remoteAnchor, and a second, named Latch on one entity. Added with one of these it does not start; written live, the field keeps the value it held and :errors() names why. :state().problem names a point with no body at or above it, or on a soft body. A latch whose joint the Joint refuses (a kind no Joint has, a rope with no maxDistance) is not made: both points are freed and onRelease hears reason "refused" with the Joint's error.

Cost, determinism, multiplayer

Each step places every latch point on its body and sorts them into a grid of cells as wide as the widest reach, comparing each point with the points of the 27 cells around it: the cost per point per step is one placement, one grid insertion and a comparison with each point within three cells of it, the same however many points stand further away, and a world with no Latch does no work. The widest reach sets the cell for every point, so keep a point's radius to the reach it latches at. The pairs are taken nearest first, ties in the order the points' entities were made, so two engines running the same scene latch the same pairs on the same step. The latch is made on the peer that simulates both bodies; its Joint and partner reach every peer. A point on a body another peer simulates is that peer's to latch, so two points whose bodies two different peers simulate do not latch.

-- a truck's ball hitch and a trailer's coupler: back the trailer in and it hitches
local hitch = entity.spawn("hitch", { parent = truck.id, position = { 0, 0.5, -2.6 } })
hitch.component.add("Latch", { tag = "hitch", accepts = { "coupler" }, radius = 0.15, joint = { kind = "ball" } })
local coupler = entity.spawn("coupler", { parent = trailer.id, position = { 0, 0.5, 2.1 } })
coupler.component.add("Latch", { tag = "coupler", accepts = { "hitch" } })
coupler.component.get("Latch"):onLatch(function(e) print("hitched", e.distance) end)

-- release, drive away, back in again: it hitches again
coupler.component.get("Latch"):release()

-- a door catch that bursts open past 4000 N
catch.component.add("Latch", { tag = "catch", radius = 0.05, maxAngle = 5, joint = { kind = "fixed", breakForce = 4000 } })