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
- Turn starts. On
TurnStartfor someone else,CoopRunmakes a freshSpectateFeedand shows the waiting screen (CoopWatchView: NOW BATTING: NAME, team score, outs, batting order, PLAY BY PLAY). - Batter begins. The batter’s machine sends
AtBatBegin(bat, outs for the turn, aim ring radius, team score). The spectator callsAtBatController.BeginSpectatewith aRoundContextbuilt like the batter’s (their batter figure, their bat, their cosmetics, the inning,startingScore= team score) and switches the camera to the batting view. - Streaming (batter → host → everyone else):
AtBatPose, unreliable, 30 per second: aim, hands (bat knob), bat direction, body twist, stamped with the batter’sTime.time.AtBatPitch: the wholePitchDataas 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: theSwingResult(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.
- The delayed clock.
SpectateFeedkeepsOffsetso that local time = batter time +Offset. Each arriving message proposesnow − 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. - 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,PitchThrownis raised. A Swing raisesSwingStarted. The Result raisesSwingResolved; on contactHitPhysics.Simulatedraws the flight and then the batter’s numbers overwrite score, home run, fair and distance. Then the normalPlayOutcomeruns (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.
- HUD.
UIRootdraws the batter’s HUD for the spectator, plus the co-op stripWATCHING NAME TEAM POOL n OUTS LEFT NEXT UP: nameand ON DECK: YOU’RE NEXT if you bat next.CoopWatchViewadds the last 3 feed lines and LEAVE GAME. - Turn over. The next
TurnStartreplaces the feed. - First turn of the run. After the match intro the spectator keeps the face-off on screen until
AtBatBeginarrives, 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
| What | Value | Where to tune |
|---|---|---|
| Playback buffer | 0.15 s | SpectateFeed.BufferSeconds |
| Offset relax per sample | 0.002 s | SpectateFeed.RelaxPerSample |
| Poses kept | last 3 s | SpectateFeed.KeepPoseSeconds |
| Pose rate | 30 per second | CoopRun.PoseInterval |
| Pose message size | 41 bytes (id + 10 floats) | SpectateMessages.Pose |
| Give up on a pitch with no result | arrival + 4 s | AtBatController.SpectateGiveUpSeconds |
| Feed lines on the live overlay | 3 | CoopWatchView.DrawLiveOverlay |
| Expected swing delay over the Steam relay | about 0.2 to 0.3 s (from the plan; not measured in code) |
In the code
| File | What it does |
|---|---|
Assets/_Project/Scripts/Batting/SpectateFeed.cs | Pure data: AddPose, AddEvent, Offset, ToLocal, ToBatter, TryPose, TryPeekDue, PitchDue |
Assets/_Project/Scripts/Batting/AtBatController.Spectate.cs | BeginSpectate, UpdateSpectate, SpectateRoutine, SpectatePitch, TryGetStreamPose (batter side), IsSpectating |
Assets/_Project/Scripts/Run/CoopRun.cs | Batter: StreamMyPose, OnMyPitch, OnMySwing, OnMySwingCompleted. Host: relay in OnSpectateMessage. Spectator: StartSpectating |
Assets/_Project/Scripts/Net/NetMessages.cs | SpectateMessages (ids 40-44) |
Assets/_Project/Scripts/UI/CoopWatchView.cs | Waiting screen and live overlay |
Assets/_Project/Scripts/UI/HudView.cs | DrawCoopStrip |
Assets/_Project/Editor/SpectateFeedTest.cs | Tools > 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.Simulateis 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
AtBatResultwithout bumpingNetSession.Protocol(stillHRD-COOP-7). A build from before #46 and one after it pass the version check (sameApplication.version) but misread every result. See Protocol Versions.
Related
- Co-op Run Flow: when spectating starts and stops
- At-Bat Loop: the at-bat the spectator replays
- Net Messages: payloads of 40-44
- Hit Physics: the locally simulated flight
- Special Balls: the pitch carries its effect
- Protocol Versions: HRD-COOP-2 added the effect byte