rook/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Files 7524e23c398cf40fee5ee6886d8b9cb75adfcbbf
| 1 | /** |
| 2 | * Everybody else, drawn between the things the server said about them. |
| 3 | * |
| 4 | * A remote body is only ever heard about a few dozen times a second, so it |
| 5 | * has to be drawn somewhere in between. The old way was an exponential chase |
| 6 | * — `now.x += (x - now.x) * 0.35` every frame — which is wrong twice: it is a |
| 7 | * different speed at 60Hz than at 144Hz, and it is a pursuit that never |
| 8 | * arrives, so every other player was permanently behind and rubbery. |
| 9 | * |
| 10 | * This is entity interpolation instead. Each body keeps the last few samples |
| 11 | * with the times they landed, and the frame is drawn at `now - delay`, about |
| 12 | * a snapshot interval in the past. That moment falls between two samples on |
| 13 | * hand, so the body moves at exactly the speed the server moved it and |
| 14 | * arrives exactly where the server put it — one interval late, which nobody |
| 15 | * can see. A handful of samples rather than strictly two is what buys the |
| 16 | * delay a margin: a snapshot that arrives late or early still brackets the |
| 17 | * moment being drawn instead of stalling the body. |
| 18 | * |
| 19 | * When a snapshot really is missed the newest pair's own velocity is carried |
| 20 | * forward for one interval and then held: a guess that long is better than a |
| 21 | * stall, and a guess longer than that is fiction. The delay follows the rate |
| 22 | * snapshots actually arrive at, so the same code is right at 15Hz and at |
| 23 | * 30Hz without being told which it is. |
| 24 | * |
| 25 | * Nothing here is ever sent anywhere: it is a drawing, and the server is |
| 26 | * still the only thing that says where anybody is. |
| 27 | */ |
| 28 | |
| 29 | /** The render delay, as a multiple of the interval snapshots arrive at. */ |
| 30 | export const LAG = 1.3; |
| 31 | /** ...never tighter than this, never looser, in seconds. */ |
| 32 | export const LAG_MIN = 0.045; |
| 33 | export const LAG_MAX = 0.13; |
| 34 | /** How many samples a body keeps: enough to bracket the delay, and no more. */ |
| 35 | export const KEEP = 4; |
| 36 | /** How fast the measured interval follows the arrivals. */ |
| 37 | const INTERVAL_PULL = 0.25; |
| 38 | /** A gap this long means the body was gone, not slow: start it again. */ |
| 39 | export const GONE = 0.4; |
| 40 | /** Faster than anything can run: a respawn, or a body back from nowhere. */ |
| 41 | const TOP_SPEED = 400; |
| 42 | |
| 43 | const clamp = (n, low, high) => (n < low ? low : n > high ? high : n); |
| 44 | |
| 45 | /** The short way round from one angle to another, in (-π, π]. */ |
| 46 | export const shortWay = (from, to) => ((to - from + Math.PI * 3) % (Math.PI * 2)) - Math.PI; |
| 47 | |
| 48 | /** One timeline for the whole snapshot stream: the bodies, and the rate. */ |
| 49 | export const makeTimeline = (interval = 1 / 15) => ({ bodies: new Map(), interval, at: 0 }); |
| 50 | |
| 51 | /** |
| 52 | * A snapshot landed at `at` seconds. The gap since the last one is what the |
| 53 | * render delay is built out of; a gap wilder than any real rate (a tab that |
| 54 | * was in the background, a reconnect) says nothing about the rate and is |
| 55 | * left out of it. |
| 56 | */ |
| 57 | export function arrive(timeline, at) { |
| 58 | const gap = at - timeline.at; |
| 59 | if (timeline.at > 0 && gap > 1 / 120 && gap < 0.5) |
| 60 | timeline.interval += (gap - timeline.interval) * INTERVAL_PULL; |
| 61 | timeline.at = at; |
| 62 | return timeline; |
| 63 | } |
| 64 | |
| 65 | /** How far behind the present this timeline is drawn, in seconds. */ |
| 66 | export const delay = (timeline) => clamp(timeline.interval * LAG, LAG_MIN, LAG_MAX); |
| 67 | |
| 68 | /** The moment being drawn, given the clock now. */ |
| 69 | export const renderTime = (timeline, now) => now - delay(timeline); |
| 70 | |
| 71 | const mark = (at, body) => ({ t: at, x: body.x, y: body.y, angle: body.angle }); |
| 72 | |
| 73 | /** |
| 74 | * What the server just said about one body. Out-of-order samples are |
| 75 | * dropped; a body that has been away, or that has moved further than anyone |
| 76 | * can run, starts fresh rather than sliding across the map to its new place. |
| 77 | * |
| 78 | * The drawn position lives on the track — one object per body, so a caller |
| 79 | * may hold the answer it was given while asking about somebody else. |
| 80 | */ |
| 81 | export function feed(timeline, id, body, at) { |
| 82 | const track = timeline.bodies.get(id); |
| 83 | if (!track) { |
| 84 | timeline.bodies.set(id, { marks: [mark(at, body)], out: { x: 0, y: 0, angle: 0 } }); |
| 85 | return timeline; |
| 86 | } |
| 87 | const last = track.marks[track.marks.length - 1]; |
| 88 | if (at <= last.t) return timeline; |
| 89 | const gap = at - last.t; |
| 90 | const jumped = Math.hypot(body.x - last.x, body.y - last.y) > TOP_SPEED * gap + 4; |
| 91 | if (gap > GONE || jumped) track.marks.length = 0; |
| 92 | track.marks.push(mark(at, body)); |
| 93 | if (track.marks.length > KEEP) track.marks.shift(); |
| 94 | return timeline; |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Where a body was at a moment, given the samples it has — ascending in |
| 99 | * time, and at least one. |
| 100 | * |
| 101 | * Between two samples this is a straight walk from one to the other at the |
| 102 | * server's own speed. Before the oldest it holds, because running backwards |
| 103 | * to meet a moment we have no answer for would be inventing one. After the |
| 104 | * newest it carries the last pair's velocity for one interval — no more, |
| 105 | * ever — and then holds there. |
| 106 | */ |
| 107 | export function sample(marks, when, out = { x: 0, y: 0, angle: 0 }) { |
| 108 | const last = marks[marks.length - 1]; |
| 109 | if (marks.length === 1 || when >= last.t) return beyond(marks, when, out); |
| 110 | if (when <= marks[0].t) return at(marks[0], marks[0], 0, out); |
| 111 | let i = marks.length - 2; |
| 112 | while (i > 0 && marks[i].t > when) i -= 1; |
| 113 | const from = marks[i]; |
| 114 | const to = marks[i + 1]; |
| 115 | const span = to.t - from.t; |
| 116 | return at(from, to, span > 0 ? (when - from.t) / span : 1, out); |
| 117 | } |
| 118 | |
| 119 | /** Past the newest sample: one interval of the last pair's velocity, held. */ |
| 120 | function beyond(marks, when, out) { |
| 121 | const last = marks[marks.length - 1]; |
| 122 | const before = marks[marks.length - 2]; |
| 123 | if (!before) return at(last, last, 0, out); |
| 124 | const span = last.t - before.t; |
| 125 | if (span <= 0) return at(last, last, 0, out); |
| 126 | return at(before, last, clamp((when - before.t) / span, 1, 2), out); |
| 127 | } |
| 128 | |
| 129 | /** A point `u` of the way from one sample to another; the turn is short. */ |
| 130 | function at(from, to, u, out) { |
| 131 | out.x = from.x + (to.x - from.x) * u; |
| 132 | out.y = from.y + (to.y - from.y) * u; |
| 133 | out.angle = from.angle + shortWay(from.angle, to.angle) * u; |
| 134 | return out; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * Where to draw a body out of a snapshot. One with no samples yet — the |
| 139 | * first frame it is seen on — is drawn where the server says, which is the |
| 140 | * only honest answer available. |
| 141 | */ |
| 142 | export function bodyAt(timeline, body, when, out = null) { |
| 143 | const track = timeline.bodies.get(body.id); |
| 144 | if (!track) return body; |
| 145 | return sample(track.marks, when, out ?? track.out); |
| 146 | } |