Live Spectating

In short

While someone else bats, your machine replays their at-bat from the same batting camera: pitcher, ball, aim ring, bat, swing, slow-motion replay, ball flight, HUD and crowd. Nothing is decided on the spectator’s machine. The bat follows the batter’s streamed pose, each pitch is the batter’s own pitch, each result and its score are the batter’s. Only the batted ball’s flight is simulated locally, for the picture. Playback runs about 0.15 s behind.

How it works

  1. Turn starts. On TurnStart for someone else, CoopRun makes a fresh SpectateFeed and shows the waiting screen (CoopWatchView: NOW BATTING: NAME, team score, outs, batting order, PLAY BY PLAY).
  2. Batter begins. The batter’s machine sends AtBatBegin (bat, outs for the turn, aim ring radius, team score). The spectator calls AtBatController.BeginSpectate with a RoundContext built like the batter’s (their batter figure, their bat, their cosmetics, the inning, startingScore = team score) and switches the camera to the batting view.
  3. Streaming (batter → host → everyone else):
    • AtBatPose, unreliable, 30 per second: aim, hands (bat knob), bat direction, body twist, stamped with the batter’s Time.time.
    • AtBatPitch: the whole PitchData as the batter’s machine made it (type id, release, plate location, speed, timings, wobble, in-zone, the map’s special-ball effect, the boss’s own ball effect, the pitch’s arc angle, and The Phantom’s decoy balls). No random numbers have to match, so spectators see the same boss balls, sideways loops and decoys (Special Balls, Pitchers and Bosses).
    • AtBatSwing: the swing press time.
    • AtBatResult: the SwingResult (including the contact zone, the held hit aim, the curve and the added gravity, so the watcher’s ball flies the same hooking or slicing path), and on contact the contact point, bat direction, score, home run, fair, distance.
  4. The delayed clock. SpectateFeed keeps Offset so that local time = batter time + Offset. Each arriving message proposes now − batterTime + 0.15. A smaller value is taken at once; a larger one is let in slowly (at most +0.002 s per message, about 0.06 s per second at the pose rate). So the offset is the fastest observed delay plus 0.15 s, and creeps up if the link gets slower.
  5. Playback.
    • Poses are kept sorted by time (unreliable packets can arrive out of order), last 3 s, and interpolated (aim and hands lerp, bat direction slerp, twist lerp; held at the ends).
    • Events are queued and played when their delayed time comes. A Pitch starts SpectatePitch: the pitcher throws, the ball flies, PitchThrown is raised. A Swing raises SwingStarted. The Result raises SwingResolved; on contact HitPhysics.Simulate draws the flight and then the batter’s numbers overwrite score, home run, fair and distance. Then the normal PlayOutcome runs (replay, crowd, feedback, and the hit show’s counter and tracer; the counter’s breakdown is built without the batter’s stats). See Hit Show.
    • Decoy balls are ticked alongside the real ball and hidden when a spectated at-bat begins.
    • Bat poses are not recorded for the replay while the ball is in flight, so the moment of contact is kept.
    • A swing or result without its pitch (joined mid-pitch) is skipped.
    • If a pitch’s result never comes within 4 s after the ball’s arrival (the batter dropped), the ball is cleared.
    • If the batter skips ahead (Space during a flight), the next Pitch comes due and the spectator skips along.
  6. HUD. UIRoot draws the batter’s HUD for the spectator, plus the co-op strip WATCHING NAME TEAM POOL n OUTS LEFT NEXT UP: name and ON DECK: YOU’RE NEXT if you bat next. CoopWatchView adds the last 3 feed lines and LEAVE GAME.
  7. Turn over. The next TurnStart replaces the feed.
  8. First turn of the run. After the match intro the spectator keeps the face-off on screen until AtBatBegin arrives, then watches the opener’s walk-up (RoundContext.walkUpShot) under the intro’s tracking camera; the HUD and the live overlay fade in as it lands. See Cutscenes.
flowchart LR
    B["Batter's AtBatController"] -->|"pose 30/s (unreliable)<br/>pitch, swing, result (reliable)"| H["Host CoopRun<br/>relay only if sender is the current batter"]
    H --> F["Spectator SpectateFeed<br/>offset = min delay + 0.15 s"]
    F --> S["AtBatController.BeginSpectate<br/>SpectateRoutine / SpectatePitch"]
    S --> E["GameEvents<br/>audio, crowd, replay, HUD"]

Numbers

WhatValueWhere to tune
Playback buffer0.15 sSpectateFeed.BufferSeconds
Offset relax per sample0.002 sSpectateFeed.RelaxPerSample
Poses keptlast 3 sSpectateFeed.KeepPoseSeconds
Pose rate30 per secondCoopRun.PoseInterval
Pose message size41 bytes (id + 10 floats)SpectateMessages.Pose
Give up on a pitch with no resultarrival + 4 sAtBatController.SpectateGiveUpSeconds
Feed lines on the live overlay3CoopWatchView.DrawLiveOverlay
Expected swing delay over the Steam relayabout 0.2 to 0.3 s (from the plan; not measured in code)

In the code

FileWhat it does
Assets/_Project/Scripts/Batting/SpectateFeed.csPure data: AddPose, AddEvent, Offset, ToLocal, ToBatter, TryPose, TryPeekDue, PitchDue
Assets/_Project/Scripts/Batting/AtBatController.Spectate.csBeginSpectate, UpdateSpectate, SpectateRoutine, SpectatePitch, TryGetStreamPose (batter side), IsSpectating
Assets/_Project/Scripts/Run/CoopRun.csBatter: StreamMyPose, OnMyPitch, OnMySwing, OnMySwingCompleted. Host: relay in OnSpectateMessage. Spectator: StartSpectating
Assets/_Project/Scripts/Net/NetMessages.csSpectateMessages (ids 40-44)
Assets/_Project/Scripts/UI/CoopWatchView.csWaiting screen and live overlay
Assets/_Project/Scripts/UI/HudView.csDrawCoopStrip
Assets/_Project/Editor/SpectateFeedTest.csTools > Home Run Derby > Net > Run Spectate Feed Test: offset, interpolation, out-of-order poses, event timing, growing delay, message round trips (including the hit’s zone, aim, curve and gravity)

Gotchas

  • If the flight simulated on the spectator lands somewhere else than the batter’s, the score and headline still come from the batter. HitPhysics.Simulate is deterministic on the same build, so it normally agrees.
  • CoopWatchView’s class comment and its footer text (“Live swings for spectators come in the next update.”) are stale: the footer still shows on the waiting screen between turns, but live spectating is built.
  • The host only relays 40-44 while an inning is being played and only from the current batter; anything else is dropped.
  • A pitch’s type is looked up by id; an unknown id falls back to the first pitch type.
  • While the live view is up, the watch screen’s own cheer / groan is skipped (the replayed at-bat has its own crowd).
  • A $$$ home run watched as a spectator adds no coins to the spectator (!IsSpectating); the batter’s machine pays.
  • Swings watched as a spectator are not written to swings.csv (SwingLog); they are logged on the batter’s machine.
  • Protocol not bumped: PR #46 added four fields to AtBatResult without bumping NetSession.Protocol (still HRD-COOP-7). A build from before #46 and one after it pass the version check (same Application.version) but misread every result. See Protocol Versions.