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

forked from benjivers/val

workbenchthis machine
Files 4990f87c0167ca7cfa9c9a4a67fa43bcfef26454
146 lines · 6.2 KB · ~1.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.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
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts