Log inGet started
World

Fencing with Fences

Branchmain
Branchesmain
Commitc0197a6317

README.md

PisteCamera

The fencing broadcast: a locked side-on view straight down the property line. It lives on the PlayerPrototype's CameraRig beside the Camera component, so every joining user gets their own.

It frames by extent rather than by separation. Each frame it works out the exact span it has to cover — both fencers, the boundary post, a margin — and sits at the nearest distance whose frustum covers it, floored by minDistance and ceilinged by the widest span the duel's own leash can produce. Framing in proportion to the gap pulls the camera out whether the frame needs it or not, and a game whose whole content is two figures and a fifth-of-a-second wind-up cannot afford figures that small.

It also reacts: DuelFx publishes punch, kick and shake, and this turns them into a dolly and a wobble. The reaction is applied after the extent solve and only ever pulls the camera IN, so it can never uncover something the solve decided had to be covered.

The shot is solved, not guessed

The frame is drawn at a 60° vertical field of view whatever Camera.fov says — that field is a silent engine no-op, filed as #8071 — so fov here means "the angle actually on screen" and every number below is measured against it.

At 60° and 16:9, height / lookHeight / minDistance place, in tangent space:

fencer's feet84.2 % down the frame
crown of the mask44.9 % down the frame
figure39.3 % of frame height
empty lawn below the feet15.8 % of frame height

The dial that decides the dead lawn is lookHeight, not height: the margin under the feet is exactly the part of the frame NOT spent on the world between the ground and the point the camera aims at. Aiming at the mask rather than at the chest is what buys it back. height sits at a standing fencer's eye so the piste reads flat and neither figure is looked down on.

fencerFramePct and lawnBelowPct report those two numbers live, computed the way the frame is actually projected rather than by the small-angle shortcut — they agree with worldToScreen to seven decimal places, so a test can assert the framing without re-deriving the projection.

The contact framing

Side-on is the framing that makes distance legible, and distance is the whole skill, so the broadcast holds it for every frame the two are fencing. It is also the framing in which two men holding fence panels at two metres are one beige mass: the panels lie in the plane of the view, so they stack instead of separating, and the one frame that decides the bout is the one nobody can read.

The umpire already stops the bout on a touch. This takes that stop:

  • the station swings contactYaw off the piste axis, orbiting the focus rather than sliding along it, so the two fencers land at different depths and their panels stop being coplanar;
  • it cranes contactLift and aims contactAim lower, which drops the near panel's top edge below the contact instead of across it;
  • it closes to a distance solved from the pair's own span with contactPad of the fight's margin, floored at contactClose of minDistance and ceilinged at minDistance — so contact is never framed wider than the fight, and the figure can only grow;
  • it slides contactBias of the way toward the fencer who took the touch, who is the subject of the moment;
  • and it holds contactHold seconds past the umpire's own stop, so the beat has a tail rather than a cut.

Nothing above touches the frame the distance is judged in: contactPct is 0 for every frame of state == "fence", and the dials it blends toward are all stated as offsets or fractions of the shot's own, so re-tuning the shot re-tunes the contact framing with it.

contactForce holds the framing for a number of seconds on demand. The beat is about six tenths of a second long and a screenshot cannot be asked for from outside the engine inside it, so this is how the moment gets photographed.

Public fields

  • height: number — the camera's world Y.
  • lookHeight: number — the world Y it aims at; the difference from height is the whole of the pitch, and this is the dial that sets the dead lawn.
  • minDistance: number — the nearest the broadcast ever sits. This is the number that decides how large a fencer reads when the two are at measure. At 4.60 the extent solve covers a 7.1 m gap before it pulls out at all, which is wider than the fight gets outside a full retreat — so the shot holds still through an exchange instead of breathing on every step.
  • padding: number — world metres held either side of the outermost thing in the shot.
  • fov: number — the vertical field of view the frame is actually drawn at.
  • smoothing: number — how quickly the rig chases its solved pose.
  • reactionRoom: number — the furthest a punch or a kick may pull the camera in, in metres. A metre buys much more of the frame from 4.60 than from 6.60.
  • focusX: number — the smoothed X the rig sits at. Live output.
  • distance: number — the smoothed distance the rig sits at. Live output.
  • fencerFramePct: number — the share of frame height a 2.13 m fencer covers. Live output.
  • lawnBelowPct: number — the share of frame height below their feet. Live output. The frame's dead space, as a number.
  • contactYaw: number — degrees the station swings off the piste axis at contact.
  • contactLift / contactAim: number — metres the camera rises and metres the aim point drops at contact.
  • contactClose / contactPad: number — the contact station's near bound and its margin, each as a fraction of minDistance and padding.
  • contactBias: number — how far the frame slides off the pair's midpoint toward the fencer who took the touch.
  • contactRise / contactFall: number — how fast the swing goes on and comes off, per second.
  • contactHold: number — seconds the framing runs on past the umpire's stop.
  • contactForce: number — seconds of contact framing to hold on demand, counted down in update.
  • contactPct: number — 0 side-on, 1 fully in the contact framing. Live output, and the field a test asserts the beat on.
  • contactSide: number — which way the swing went: −1 the struck fencer is west, +1 east. Live output.

Lifecycle

  • awake() — pushes fov onto the sibling Camera.
  • update(dt) / editorUpdate(dt) — solves and applies the pose. It runs in edit mode too, so the shot can be judged without entering play.

Usage

entity(id).component.add("PisteCamera", {})
entity(id).component.get("PisteCamera").minDistance = 5.2

Notes

  • The scene's authored props outrank every default in this file. scenes/main.scene/scene.json carries a PisteCamera prop block, and a cold layers.load re-attaches the component with those numbers — so the shot the world runs after a reload is the one written there, not the one tuned here. Read the live component, not this file, before believing any framing number.
  • Leave the sibling Camera's behavior field unset. A camera-controller component named there is re-instantiated on every scene load and fights this one for the rig's transform.
  • The fencers and the director are held by id and re-scanned only when a cached id stops resolving. Asking entity.findAll() once a frame is a walk of the whole arena, which is a few hundred parts: it cost 4.2 ms a frame and made this the second most expensive script in the world. It now costs 0.21.