rook/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine
Files b02c1ef0dc92246d3da3567c343126686c5677c3
221 lines · 9.7 KB · ~2.8k tok Raw History
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.1;
38
/** How fast the even clock snapshots are filed under is pulled toward the arrivals. */
39
const STAMP_PULL = 0.1;
40
/** How much of the even clock's lateness is read as the interval being wrong. */
41
const INTERVAL_TRIM = 0.02;
42
/** How far the moment drawn may run fast or slow to reach its delay: a share of real time. */
43
export const SLEW = 0.02;
44
/** A render clock this far off its mark is not late, it is stopped: start it again. */
45
const RESYNC = 0.25;
46
/** A gap this long means the body was gone, not slow: start it again. */
47
export const GONE = 0.4;
48
/** Faster than anything can run: a respawn, or a body back from nowhere. */
49
const TOP_SPEED = 400;
50
51
const clamp = (n, low, high) => (n < low ? low : n > high ? high : n);
52
53
/** The short way round from one angle to another, in (-π, π]. */
54
export const shortWay = (from, to) => ((to - from + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
55
56
/**
57
 * One timeline for the whole snapshot stream: the bodies, the rate, the even
58
 * clock (`stamp`) and the moment being drawn (`drawAt`). `restarts` counts
59
 * the times a body was started again rather than walked: every one is a jump.
60
 */
61
export const makeTimeline = (interval = 1 / 30) => ({
62
  bodies: new Map(),
63
  interval,
64
  at: 0,
65
  stamp: 0,
66
  drawAt: 0,
67
  drawnNow: 0,
68
  restarts: 0,
69
});
70
71
/**
72
 * A snapshot landed at `at` seconds. The gap since the last one is what the
73
 * render delay is built out of; a gap wilder than any real rate (a tab that
74
 * was in the background, a reconnect) says nothing about the rate and is
75
 * left out of it.
76
 *
77
 * Returns the time the snapshot is filed under, which is not the time it
78
 * landed. The server sends thirty a second, evenly; the network hands them
79
 * over in a rhythm of its own, a few milliseconds early or late, sometimes
80
 * two nearly together. A sample filed under its arrival time carries that
81
 * wobble into the walk between samples — a body drawn between a sample that
82
 * came early and one that came late moves slow, then fast — and into the
83
 * age of your own snapshot, which is how far ahead your own keys are
84
 * carried. So arrivals are filed on an even clock instead: one interval
85
 * after the last, pulled a tenth of the way toward when they really came so
86
 * it never drifts. A rate is not a rhythm; this is the rhythm.
87
 */
88
export function arrive(timeline, at) {
89
  const gap = at - timeline.at;
90
  const steady = timeline.at > 0 && gap > 1 / 120 && gap < 0.5;
91
  if (steady) timeline.interval += (gap - timeline.interval) * INTERVAL_PULL;
92
  const expected = timeline.stamp + timeline.interval;
93
  const off = at - expected;
94
  // Within an interval of the beat, keep the beat; further out (a pause, a
95
  // reconnect, the rate itself changing) the arrival is the only clock there is.
96
  let stamp = at;
97
  if (timeline.at > 0 && gap < 0.5 && Math.abs(off) < timeline.interval) {
98
    stamp = expected + off * STAMP_PULL;
99
    // Arrivals that keep landing on the same side of the beat mean the beat
100
    // is wrong, not late: trim the interval by a little of the lateness.
101
    timeline.interval += off * INTERVAL_TRIM;
102
  }
103
  // A sample is never filed before the one before it, and never more than
104
  // half a beat before it actually came: a burst of snapshots (a tab coming
105
  // back from the background) must not walk the clock off into the future.
106
  if (stamp <= timeline.stamp) stamp = timeline.stamp + timeline.interval / 4;
107
  stamp = Math.max(Math.min(stamp, at + timeline.interval / 2), timeline.stamp);
108
  timeline.at = at;
109
  timeline.stamp = stamp;
110
  return stamp;
111
}
112
113
/** How far behind the present this timeline is drawn, in seconds. */
114
export const delay = (timeline) => clamp(timeline.interval * LAG, LAG_MIN, LAG_MAX);
115
116
/**
117
 * The moment being drawn, given the clock now.
118
 *
119
 * Not simply `now - delay`: the delay follows the measured rate, and a delay
120
 * that moves between frames moves the moment being drawn with it, so a
121
 * frame after a late snapshot drew twelve milliseconds of motion and the
122
 * next nineteen — the beat of the wire showing through as a stutter. The
123
 * moment drawn now advances at real time, and creeps toward where the delay
124
 * says it should be at no more than two percent of that: a speed nobody can
125
 * see. Only a clock a quarter second off — a tab that was in the background,
126
 * the first frame — is started again where it belongs.
127
 */
128
export function renderTime(timeline, now) {
129
  const want = now - delay(timeline);
130
  const dt = now - timeline.drawnNow;
131
  timeline.drawnNow = now;
132
  const ahead = timeline.drawAt + dt;
133
  if (timeline.drawAt === 0 || dt <= 0 || dt > RESYNC || Math.abs(want - ahead) > RESYNC) {
134
    timeline.drawAt = want;
135
    return want;
136
  }
137
  timeline.drawAt = ahead + clamp(want - ahead, -dt * SLEW, dt * SLEW);
138
  return timeline.drawAt;
139
}
140
141
const mark = (at, body) => ({ t: at, x: body.x, y: body.y, angle: body.angle });
142
143
/**
144
 * What the server just said about one body. Out-of-order samples are
145
 * dropped; a body that has been away, or that has moved further than anyone
146
 * can run, starts fresh rather than sliding across the map to its new place.
147
 *
148
 * The drawn position lives on the track — one object per body, so a caller
149
 * may hold the answer it was given while asking about somebody else.
150
 */
151
export function feed(timeline, id, body, at) {
152
  const track = timeline.bodies.get(id);
153
  if (!track) {
154
    timeline.bodies.set(id, { marks: [mark(at, body)], out: { x: 0, y: 0, angle: 0 } });
155
    return timeline;
156
  }
157
  const last = track.marks[track.marks.length - 1];
158
  if (at <= last.t) return timeline;
159
  const gap = at - last.t;
160
  // Two samples filed close together are still a tick apart on the server,
161
  // so a body is allowed at least an interval's run between them.
162
  const jumped = Math.hypot(body.x - last.x, body.y - last.y) > TOP_SPEED * Math.max(gap, timeline.interval) + 4;
163
  if (gap > GONE || jumped) {
164
    track.marks.length = 0;
165
    timeline.restarts += 1;
166
  }
167
  track.marks.push(mark(at, body));
168
  if (track.marks.length > KEEP) track.marks.shift();
169
  return timeline;
170
}
171
172
/**
173
 * Where a body was at a moment, given the samples it has — ascending in
174
 * time, and at least one.
175
 *
176
 * Between two samples this is a straight walk from one to the other at the
177
 * server's own speed. Before the oldest it holds, because running backwards
178
 * to meet a moment we have no answer for would be inventing one. After the
179
 * newest it carries the last pair's velocity for one interval — no more,
180
 * ever — and then holds there.
181
 */
182
export function sample(marks, when, out = { x: 0, y: 0, angle: 0 }) {
183
  const last = marks[marks.length - 1];
184
  if (marks.length === 1 || when >= last.t) return beyond(marks, when, out);
185
  if (when <= marks[0].t) return at(marks[0], marks[0], 0, out);
186
  let i = marks.length - 2;
187
  while (i > 0 && marks[i].t > when) i -= 1;
188
  const from = marks[i];
189
  const to = marks[i + 1];
190
  const span = to.t - from.t;
191
  return at(from, to, span > 0 ? (when - from.t) / span : 1, out);
192
}
193
194
/** Past the newest sample: one interval of the last pair's velocity, held. */
195
function beyond(marks, when, out) {
196
  const last = marks[marks.length - 1];
197
  const before = marks[marks.length - 2];
198
  if (!before) return at(last, last, 0, out);
199
  const span = last.t - before.t;
200
  if (span <= 0) return at(last, last, 0, out);
201
  return at(before, last, clamp((when - before.t) / span, 1, 2), out);
202
}
203
204
/** A point `u` of the way from one sample to another; the turn is short. */
205
function at(from, to, u, out) {
206
  out.x = from.x + (to.x - from.x) * u;
207
  out.y = from.y + (to.y - from.y) * u;
208
  out.angle = from.angle + shortWay(from.angle, to.angle) * u;
209
  return out;
210
}
211
212
/**
213
 * Where to draw a body out of a snapshot. One with no samples yet — the
214
 * first frame it is seen on — is drawn where the server says, which is the
215
 * only honest answer available.
216
 */
217
export function bodyAt(timeline, body, when, out = null) {
218
  const track = timeline.bodies.get(body.id);
219
  if (!track) return body;
220
  return sample(track.marks, when, out ?? track.out);
221
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts