spoint 0.1.59 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/app.js CHANGED
@@ -686,6 +686,7 @@ const entityGroups = new Map()
686
686
  const appModules = new Map()
687
687
  const entityAppMap = new Map()
688
688
  const playerTargets = new Map()
689
+ const entityTargets = new Map()
689
690
  let inputHandler = null
690
691
  const uiRoot = document.getElementById('ui-root')
691
692
  const clickPrompt = document.getElementById('click-prompt')
@@ -1047,8 +1048,13 @@ const client = new PhysicsNetworkClient({
1047
1048
  }
1048
1049
  for (const e of smoothState.entities) {
1049
1050
  const mesh = entityMeshes.get(e.id)
1050
- if (mesh && e.position) mesh.position.set(e.position[0], e.position[1], e.position[2])
1051
- if (mesh && e.rotation) mesh.quaternion.set(e.rotation[0], e.rotation[1], e.rotation[2], e.rotation[3])
1051
+ if (mesh && e.position) {
1052
+ const et = entityTargets.get(e.id)
1053
+ if (et) { et.x = e.position[0]; et.y = e.position[1]; et.z = e.position[2]; et.rx = e.rotation?.[0] || 0; et.ry = e.rotation?.[1] || 0; et.rz = e.rotation?.[2] || 0; et.rw = e.rotation?.[3] || 1 }
1054
+ else entityTargets.set(e.id, { x: e.position[0], y: e.position[1], z: e.position[2], rx: e.rotation?.[0] || 0, ry: e.rotation?.[1] || 0, rz: e.rotation?.[2] || 0, rw: e.rotation?.[3] || 1 })
1055
+ const dx = e.position[0] - mesh.position.x, dy = e.position[1] - mesh.position.y, dz = e.position[2] - mesh.position.z
1056
+ if (!mesh.userData.entInit || dx * dx + dy * dy + dz * dz > 100) { mesh.position.set(e.position[0], e.position[1], e.position[2]); if (e.rotation) mesh.quaternion.set(e.rotation[0], e.rotation[1], e.rotation[2], e.rotation[3]); mesh.userData.entInit = true }
1057
+ }
1052
1058
  if (!entityMeshes.has(e.id)) loadEntityModel(e.id, e)
1053
1059
  }
1054
1060
  rebuildEntityHierarchy(smoothState.entities)
@@ -1064,7 +1070,7 @@ const client = new PhysicsNetworkClient({
1064
1070
  onPlayerJoined: (id) => { if (!playerMeshes.has(id)) createPlayerVRM(id) },
1065
1071
  onPlayerLeft: (id) => removePlayerMesh(id),
1066
1072
  onEntityAdded: (id, state) => loadEntityModel(id, state),
1067
- onEntityRemoved: (id) => { const m = entityMeshes.get(id); if (m) { scene.remove(m); m.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() }); entityMeshes.delete(id) }; pendingLoads.delete(id) },
1073
+ onEntityRemoved: (id) => { const m = entityMeshes.get(id); if (m) { scene.remove(m); m.traverse(c => { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose() }); entityMeshes.delete(id) }; entityTargets.delete(id); pendingLoads.delete(id) },
1068
1074
  onWorldDef: (wd) => {
1069
1075
  loadingMgr.setStage('SERVER_SYNC')
1070
1076
  worldConfig = wd
@@ -1496,6 +1502,18 @@ function animate(timestamp) {
1496
1502
  if (features?._headBone) features._headBone.rotation.x = -(ps.lookPitch || 0) * 0.6
1497
1503
  }
1498
1504
  })
1505
+ entityTargets.forEach((target, id) => {
1506
+ const mesh = entityMeshes.get(id)
1507
+ if (!mesh) return
1508
+ mesh.position.x += (target.x - mesh.position.x) * lerpFactor
1509
+ mesh.position.y += (target.y - mesh.position.y) * lerpFactor
1510
+ mesh.position.z += (target.z - mesh.position.z) * lerpFactor
1511
+ mesh.quaternion.x += (target.rx - mesh.quaternion.x) * lerpFactor
1512
+ mesh.quaternion.y += (target.ry - mesh.quaternion.y) * lerpFactor
1513
+ mesh.quaternion.z += (target.rz - mesh.quaternion.z) * lerpFactor
1514
+ mesh.quaternion.w += (target.rw - mesh.quaternion.w) * lerpFactor
1515
+ mesh.quaternion.normalize()
1516
+ })
1499
1517
  entityMeshes.forEach((mesh) => {
1500
1518
  if (mesh.userData.spin) mesh.rotation.y += mesh.userData.spin * frameDt
1501
1519
  if (mesh.userData.hover) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spoint",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,207 +1,130 @@
1
1
  export class JitterBuffer {
2
2
  constructor(config = {}) {
3
- this.maxSize = config.maxSize || 32
4
- this.maxAge = config.maxAge || 200
3
+ this.maxSize = config.maxSize || 64
5
4
  this.minBufferSize = config.minBufferSize || 2
6
- this.targetDelay = config.targetDelay || 50
7
-
5
+ this.baseDelay = config.baseDelay || 30
6
+
8
7
  this.buffer = []
9
- this.lastProcessTime = 0
10
8
  this.lastServerTime = 0
11
- this.lastTick = 0
9
+ this.lastClientTime = 0
12
10
  this.rtt = config.initialRtt || 50
13
11
  this.rttVariance = 0
14
- this.clockDelta = 0
15
- this.clockDeltaVariance = 0
16
- this.lastClientTime = 0
12
+ this.jitter = 0
13
+ this.targetDelay = this.baseDelay
17
14
  }
18
-
15
+
19
16
  addSnapshot(snapshot) {
20
17
  const now = Date.now()
21
- const clientTime = now
22
18
  const serverTime = snapshot.timestamp || now
23
-
24
- if (this.lastServerTime > 0) {
19
+
20
+ if (this.lastServerTime > 0 && this.lastClientTime > 0) {
25
21
  const serverDelta = serverTime - this.lastServerTime
26
- const clientDelta = clientTime - this.lastClientTime
27
-
22
+ const clientDelta = now - this.lastClientTime
28
23
  if (serverDelta > 0 && clientDelta > 0) {
29
- const instantClockDelta = clientDelta - serverDelta
30
- this.clockDeltaVariance = this.clockDeltaVariance * 0.9 + Math.abs(instantClockDelta - this.clockDelta) * 0.1
31
- this.clockDelta = this.clockDelta * 0.9 + instantClockDelta * 0.1
24
+ const instantJitter = Math.abs(clientDelta - serverDelta)
25
+ this.jitter = this.jitter * 0.9 + instantJitter * 0.1
32
26
  }
33
27
  }
34
-
28
+
35
29
  this.lastServerTime = serverTime
36
- this.lastClientTime = clientTime
37
-
38
- this.buffer.push({
39
- snapshot,
40
- clientTime,
41
- serverTime,
42
- tick: snapshot.tick || 0
43
- })
44
-
30
+ this.lastClientTime = now
31
+
32
+ this.buffer.push({ snapshot, clientTime: now, serverTime, tick: snapshot.tick || 0 })
45
33
  this.buffer.sort((a, b) => a.tick - b.tick)
46
-
47
- while (this.buffer.length > this.maxSize) {
48
- this.buffer.shift()
49
- }
50
-
51
- this._pruneOld(now)
52
- }
53
-
54
- _pruneOld(now) {
55
- const cutoff = now - this.maxAge
56
- while (this.buffer.length > 0 && this.buffer[0].clientTime < cutoff) {
57
- this.buffer.shift()
58
- }
34
+
35
+ while (this.buffer.length > this.maxSize) this.buffer.shift()
36
+
37
+ const maxAge = Math.max(300, this.rtt + this.jitter * 3)
38
+ const cutoff = now - maxAge
39
+ while (this.buffer.length > 0 && this.buffer[0].clientTime < cutoff) this.buffer.shift()
59
40
  }
60
-
41
+
61
42
  getSnapshotToRender(now = Date.now()) {
43
+ if (this.buffer.length === 0) return null
62
44
  if (this.buffer.length < this.minBufferSize) {
63
- if (this.buffer.length === 0) return null
64
45
  return this.buffer[this.buffer.length - 1].snapshot
65
46
  }
66
-
47
+
67
48
  const renderTime = now - this.targetDelay
68
-
69
- let newest = this.buffer[this.buffer.length - 1]
70
- let oldest = this.buffer[0]
71
-
72
- if (renderTime >= newest.clientTime) {
73
- return newest.snapshot
74
- }
75
-
76
- if (renderTime <= oldest.clientTime) {
77
- return oldest.snapshot
78
- }
79
-
49
+ const newest = this.buffer[this.buffer.length - 1]
50
+ const oldest = this.buffer[0]
51
+
52
+ if (renderTime >= newest.clientTime) return newest.snapshot
53
+ if (renderTime <= oldest.clientTime) return oldest.snapshot
54
+
80
55
  for (let i = 0; i < this.buffer.length - 1; i++) {
81
56
  const curr = this.buffer[i]
82
57
  const next = this.buffer[i + 1]
83
-
84
58
  if (renderTime >= curr.clientTime && renderTime <= next.clientTime) {
85
59
  const range = next.clientTime - curr.clientTime
86
60
  if (range === 0) return curr.snapshot
87
-
88
61
  const alpha = (renderTime - curr.clientTime) / range
89
62
  return this._interpolateSnapshots(curr.snapshot, next.snapshot, alpha)
90
63
  }
91
64
  }
92
-
65
+
93
66
  return newest.snapshot
94
67
  }
95
-
68
+
96
69
  _interpolateSnapshots(older, newer, alpha) {
97
- const interpolated = {
98
- tick: Math.round(older.tick + (newer.tick - older.tick) * alpha),
99
- timestamp: older.timestamp + (newer.timestamp - older.timestamp) * alpha,
100
- players: [],
101
- entities: []
102
- }
103
-
104
- const olderPlayers = new Map()
105
- for (const p of older.players || []) {
106
- olderPlayers.set(p.id, p)
107
- }
108
-
70
+ const result = { tick: newer.tick, timestamp: newer.timestamp, players: [], entities: [] }
71
+
72
+ const oldP = new Map()
73
+ for (const p of older.players || []) oldP.set(p.id, p)
109
74
  for (const np of newer.players || []) {
110
- const op = olderPlayers.get(np.id)
75
+ const op = oldP.get(np.id)
111
76
  if (op) {
112
- interpolated.players.push(this._interpolatePlayer(op, np, alpha))
77
+ result.players.push({
78
+ id: np.id,
79
+ position: [_l(op.position[0], np.position[0], alpha), _l(op.position[1], np.position[1], alpha), _l(op.position[2], np.position[2], alpha)],
80
+ rotation: np.rotation,
81
+ velocity: [_l(op.velocity?.[0] || 0, np.velocity?.[0] || 0, alpha), _l(op.velocity?.[1] || 0, np.velocity?.[1] || 0, alpha), _l(op.velocity?.[2] || 0, np.velocity?.[2] || 0, alpha)],
82
+ onGround: np.onGround, health: np.health, inputSequence: np.inputSequence,
83
+ crouch: np.crouch,
84
+ lookPitch: _l(op.lookPitch || 0, np.lookPitch || 0, alpha),
85
+ lookYaw: _l(op.lookYaw || 0, np.lookYaw || 0, alpha)
86
+ })
113
87
  } else {
114
- interpolated.players.push({ ...np })
88
+ result.players.push({ ...np })
115
89
  }
116
90
  }
117
-
118
- const olderEntities = new Map()
119
- for (const e of older.entities || []) {
120
- olderEntities.set(e.id, e)
121
- }
122
-
91
+
92
+ const oldE = new Map()
93
+ for (const e of older.entities || []) oldE.set(e.id, e)
123
94
  for (const ne of newer.entities || []) {
124
- const oe = olderEntities.get(ne.id)
95
+ const oe = oldE.get(ne.id)
125
96
  if (oe) {
126
- interpolated.entities.push(this._interpolateEntity(oe, ne, alpha))
97
+ result.entities.push({
98
+ id: ne.id, model: ne.model,
99
+ position: [_l(oe.position[0], ne.position[0], alpha), _l(oe.position[1], ne.position[1], alpha), _l(oe.position[2], ne.position[2], alpha)],
100
+ rotation: [_l(oe.rotation[0], ne.rotation[0], alpha), _l(oe.rotation[1], ne.rotation[1], alpha), _l(oe.rotation[2], ne.rotation[2], alpha), _l(oe.rotation[3], ne.rotation[3], alpha)],
101
+ bodyType: ne.bodyType, custom: ne.custom
102
+ })
127
103
  } else {
128
- interpolated.entities.push({ ...ne })
104
+ result.entities.push({ ...ne })
129
105
  }
130
106
  }
131
-
132
- return interpolated
133
- }
134
-
135
- _interpolatePlayer(older, newer, alpha) {
136
- return {
137
- id: newer.id,
138
- position: [
139
- this._lerp(older.position[0], newer.position[0], alpha),
140
- this._lerp(older.position[1], newer.position[1], alpha),
141
- this._lerp(older.position[2], newer.position[2], alpha)
142
- ],
143
- rotation: newer.rotation,
144
- velocity: [
145
- this._lerp(older.velocity?.[0] || 0, newer.velocity?.[0] || 0, alpha),
146
- this._lerp(older.velocity?.[1] || 0, newer.velocity?.[1] || 0, alpha),
147
- this._lerp(older.velocity?.[2] || 0, newer.velocity?.[2] || 0, alpha)
148
- ],
149
- onGround: newer.onGround,
150
- health: this._lerp(older.health || 100, newer.health || 100, alpha),
151
- inputSequence: newer.inputSequence,
152
- crouch: newer.crouch,
153
- lookPitch: this._lerp(older.lookPitch || 0, newer.lookPitch || 0, alpha),
154
- lookYaw: this._lerp(older.lookYaw || 0, newer.lookYaw || 0, alpha)
155
- }
156
- }
157
-
158
- _interpolateEntity(older, newer, alpha) {
159
- return {
160
- id: newer.id,
161
- model: newer.model,
162
- position: [
163
- this._lerp(older.position[0], newer.position[0], alpha),
164
- this._lerp(older.position[1], newer.position[1], alpha),
165
- this._lerp(older.position[2], newer.position[2], alpha)
166
- ],
167
- rotation: [
168
- this._lerp(older.rotation[0], newer.rotation[0], alpha),
169
- this._lerp(older.rotation[1], newer.rotation[1], alpha),
170
- this._lerp(older.rotation[2], newer.rotation[2], alpha),
171
- this._lerp(older.rotation[3], newer.rotation[3], alpha)
172
- ],
173
- bodyType: newer.bodyType,
174
- custom: newer.custom
175
- }
107
+
108
+ return result
176
109
  }
177
-
178
- _lerp(a, b, t) {
179
- return a + (b - a) * t
180
- }
181
-
110
+
182
111
  updateRTT(pingTime, pongTime) {
183
- const instantRtt = pongTime - pingTime
184
- this.rttVariance = this.rttVariance * 0.75 + Math.abs(instantRtt - this.rtt) * 0.25
185
- this.rtt = this.rtt * 0.875 + instantRtt * 0.125
186
-
187
- this.targetDelay = Math.min(100, Math.max(20, this.rtt / 2 + this.rttVariance))
188
- }
189
-
190
- getBufferHealth() {
191
- return this.buffer.length
192
- }
193
-
194
- getRTT() {
195
- return this.rtt
196
- }
197
-
198
- getClockDelta() {
199
- return this.clockDelta
112
+ const instant = pongTime - pingTime
113
+ this.rttVariance = this.rttVariance * 0.75 + Math.abs(instant - this.rtt) * 0.25
114
+ this.rtt = this.rtt * 0.875 + instant * 0.125
115
+ this.targetDelay = this.baseDelay + this.rtt * 0.5 + this.jitter * 2
200
116
  }
201
-
117
+
118
+ getBufferHealth() { return this.buffer.length }
119
+ getRTT() { return this.rtt }
120
+ getJitter() { return this.jitter }
121
+ getTargetDelay() { return this.targetDelay }
122
+
202
123
  clear() {
203
124
  this.buffer = []
204
125
  this.lastServerTime = 0
205
126
  this.lastClientTime = 0
206
127
  }
207
- }
128
+ }
129
+
130
+ function _l(a, b, t) { return a + (b - a) * t }
@@ -1,89 +1,92 @@
1
1
  export class KalmanFilter3D {
2
2
  constructor(config = {}) {
3
- this.processNoise = config.processNoise || 0.1
4
- this.measurementNoise = config.measurementNoise || 0.5
5
- this.uncertainty = config.uncertainty || 1.0
6
-
3
+ this.positionQ = config.positionQ ?? 2.0
4
+ this.velocityQ = config.velocityQ ?? 4.0
5
+ this.positionR = config.positionR ?? 0.01
6
+ this.velocityR = config.velocityR ?? 0.1
7
+
7
8
  this.x = [0, 0, 0]
8
9
  this.v = [0, 0, 0]
9
-
10
- this.P = [
11
- [this.uncertainty, 0, 0],
12
- [0, this.uncertainty, 0],
13
- [0, 0, this.uncertainty]
14
- ]
15
-
10
+
11
+ this.Pp = [1, 1, 1]
12
+ this.Pv = [1, 1, 1]
13
+
16
14
  this.initialized = false
15
+ this._prevPos = null
16
+ this._lastUpdateMs = 0
17
17
  }
18
-
19
- init(position, velocity = [0, 0, 0]) {
18
+
19
+ init(position, velocity = null, now = Date.now()) {
20
20
  this.x = [...position]
21
- this.v = [...velocity]
21
+ this.v = velocity ? [...velocity] : [0, 0, 0]
22
+ this._prevPos = [...position]
23
+ this._lastUpdateMs = now
22
24
  this.initialized = true
23
25
  }
24
-
26
+
25
27
  predict(dt) {
26
- if (!this.initialized) return { position: this.x, velocity: this.v }
27
-
28
+ if (!this.initialized || dt <= 0) return { position: [...this.x], velocity: [...this.v] }
29
+
28
30
  for (let i = 0; i < 3; i++) {
29
31
  this.x[i] += this.v[i] * dt
32
+ this.Pp[i] += this.positionQ * dt
33
+ this.Pv[i] += this.velocityQ * dt
30
34
  }
31
-
32
- const q = this.processNoise * dt * dt
33
- for (let i = 0; i < 3; i++) {
34
- this.P[i][i] += q
35
- }
36
-
35
+
37
36
  return { position: [...this.x], velocity: [...this.v] }
38
37
  }
39
-
40
- update(measuredPosition, measuredVelocity = null) {
38
+
39
+ update(measuredPosition, measuredVelocity = null, now = Date.now()) {
41
40
  if (!this.initialized) {
42
- this.init(measuredPosition, measuredVelocity || [0, 0, 0])
41
+ this.init(measuredPosition, measuredVelocity, now)
43
42
  return { position: [...this.x], velocity: [...this.v] }
44
43
  }
45
-
46
- const R = this.measurementNoise
47
-
44
+
45
+ const elapsedMs = now - this._lastUpdateMs
46
+ if (elapsedMs < 1) return { position: [...this.x], velocity: [...this.v] }
47
+ const elapsed = elapsedMs / 1000
48
+ this._lastUpdateMs = now
49
+
50
+ for (let i = 0; i < 3; i++) {
51
+ this.x[i] += this.v[i] * elapsed
52
+ this.Pp[i] += this.positionQ * elapsed
53
+ this.Pv[i] += this.velocityQ * elapsed
54
+ }
55
+
48
56
  for (let i = 0; i < 3; i++) {
49
- const P = this.P[i][i]
50
- const K = P / (P + R)
51
-
52
- this.x[i] += K * (measuredPosition[i] - this.x[i])
53
-
57
+ const Kp = this.Pp[i] / (this.Pp[i] + this.positionR)
58
+ this.x[i] += Kp * (measuredPosition[i] - this.x[i])
59
+ this.Pp[i] = (1 - Kp) * this.Pp[i]
60
+
61
+ let measuredV
54
62
  if (measuredVelocity) {
55
- this.v[i] = measuredVelocity[i]
63
+ measuredV = measuredVelocity[i]
64
+ } else if (this._prevPos) {
65
+ measuredV = (measuredPosition[i] - this._prevPos[i]) / elapsed
66
+ } else {
67
+ measuredV = 0
56
68
  }
57
-
58
- this.P[i][i] = (1 - K) * P
69
+
70
+ const Kv = this.Pv[i] / (this.Pv[i] + this.velocityR)
71
+ this.v[i] += Kv * (measuredV - this.v[i])
72
+ this.Pv[i] = (1 - Kv) * this.Pv[i]
59
73
  }
60
-
74
+
75
+ this._prevPos = [...measuredPosition]
61
76
  return { position: [...this.x], velocity: [...this.v] }
62
77
  }
63
-
64
- getState() {
65
- return {
66
- position: [...this.x],
67
- velocity: [...this.v]
68
- }
69
- }
70
-
71
- setPosition(pos) {
72
- this.x = [...pos]
73
- }
74
-
75
- setVelocity(vel) {
76
- this.v = [...vel]
77
- }
78
-
78
+
79
+ getState() { return { position: [...this.x], velocity: [...this.v] } }
80
+ setPosition(pos) { this.x = [...pos]; this._prevPos = [...pos] }
81
+ setVelocity(vel) { this.v = [...vel] }
82
+
79
83
  reset(position = [0, 0, 0]) {
80
84
  this.x = [...position]
81
85
  this.v = [0, 0, 0]
82
- this.P = [
83
- [this.uncertainty, 0, 0],
84
- [0, this.uncertainty, 0],
85
- [0, 0, this.uncertainty]
86
- ]
86
+ this.Pp = [1, 1, 1]
87
+ this.Pv = [1, 1, 1]
88
+ this._prevPos = null
89
+ this._lastUpdateMs = 0
87
90
  this.initialized = false
88
91
  }
89
92
  }
@@ -94,7 +97,7 @@ export class SmoothStateTracker {
94
97
  this.maxAge = config.maxAge || 5000
95
98
  this.defaultConfig = config.filterConfig || {}
96
99
  }
97
-
100
+
98
101
  getFilter(id) {
99
102
  let filter = this.filters.get(id)
100
103
  if (!filter) {
@@ -103,23 +106,17 @@ export class SmoothStateTracker {
103
106
  }
104
107
  return filter
105
108
  }
106
-
109
+
107
110
  update(id, position, velocity, dt) {
108
111
  const filter = this.getFilter(id)
109
- filter.predict(dt)
110
112
  return filter.update(position, velocity)
111
113
  }
112
-
114
+
113
115
  predict(id, dt) {
114
116
  const filter = this.getFilter(id)
115
117
  return filter.predict(dt)
116
118
  }
117
-
118
- remove(id) {
119
- this.filters.delete(id)
120
- }
121
-
122
- clear() {
123
- this.filters.clear()
124
- }
119
+
120
+ remove(id) { this.filters.delete(id) }
121
+ clear() { this.filters.clear() }
125
122
  }
@@ -1,4 +1,4 @@
1
- import { KalmanFilter3D, SmoothStateTracker } from './KalmanFilter.js'
1
+ import { KalmanFilter3D } from './KalmanFilter.js'
2
2
  import { JitterBuffer } from './JitterBuffer.js'
3
3
 
4
4
  export class SmoothInterpolation {
@@ -6,122 +6,96 @@ export class SmoothInterpolation {
6
6
  this.jitterBuffer = new JitterBuffer(config.jitter || {})
7
7
  this.playerFilters = new Map()
8
8
  this.entityFilters = new Map()
9
- this.kalmanConfig = config.kalman || {
10
- processNoise: 0.08,
11
- measurementNoise: 0.3,
12
- uncertainty: 0.5
9
+ this.playerKalmanConfig = config.playerKalman || {
10
+ positionQ: 2.0, velocityQ: 4.0, positionR: 0.01, velocityR: 0.1
13
11
  }
14
-
15
- this.lastFrameTime = Date.now()
12
+ this.entityKalmanConfig = config.entityKalman || {
13
+ positionQ: 2.0, velocityQ: 4.0, positionR: 0.01, velocityR: 0.5
14
+ }
15
+
16
16
  this.localPlayerId = null
17
17
  this.predictionEnabled = config.predictionEnabled !== false
18
- this.extrapolationLimit = config.extrapolationLimit || 100
19
- }
20
-
21
- setLocalPlayer(id) {
22
- this.localPlayerId = id
18
+ this._lastDisplayTime = 0
23
19
  }
24
-
20
+
21
+ setLocalPlayer(id) { this.localPlayerId = id }
22
+
25
23
  addSnapshot(snapshot) {
26
24
  this.jitterBuffer.addSnapshot(snapshot)
25
+ const now = Date.now()
26
+ for (const p of snapshot.players || []) {
27
+ if (p.id === this.localPlayerId && this.predictionEnabled) continue
28
+ let filter = this.playerFilters.get(p.id)
29
+ if (!filter) {
30
+ filter = new KalmanFilter3D(this.playerKalmanConfig)
31
+ this.playerFilters.set(p.id, filter)
32
+ }
33
+ filter.update(p.position, p.velocity, now)
34
+ }
35
+ for (const e of snapshot.entities || []) {
36
+ let filter = this.entityFilters.get(e.id)
37
+ if (!filter) {
38
+ filter = new KalmanFilter3D(this.entityKalmanConfig)
39
+ this.entityFilters.set(e.id, filter)
40
+ }
41
+ filter.update(e.position, null, now)
42
+ }
27
43
  }
28
-
44
+
29
45
  getDisplayState(now = Date.now()) {
30
46
  const snapshot = this.jitterBuffer.getSnapshotToRender(now)
31
47
  if (!snapshot) return { players: [], entities: [] }
32
-
33
- const dt = Math.min((now - this.lastFrameTime) / 1000, 0.1)
34
- this.lastFrameTime = now
35
-
48
+
49
+ const dt = this._lastDisplayTime > 0 ? Math.min((now - this._lastDisplayTime) / 1000, 0.1) : 0
50
+ this._lastDisplayTime = now
51
+
36
52
  const displayPlayers = []
37
53
  for (const player of snapshot.players || []) {
38
54
  if (player.id === this.localPlayerId && this.predictionEnabled) {
39
55
  displayPlayers.push(player)
40
56
  continue
41
57
  }
42
-
43
- const smoothed = this._smoothPlayer(player, dt)
44
- displayPlayers.push(smoothed)
58
+ const filter = this.playerFilters.get(player.id)
59
+ if (filter && dt > 0) {
60
+ const predicted = filter.predict(dt)
61
+ displayPlayers.push({ ...player, position: predicted.position, velocity: predicted.velocity })
62
+ } else {
63
+ displayPlayers.push(player)
64
+ }
45
65
  }
46
-
66
+
47
67
  const displayEntities = []
48
68
  for (const entity of snapshot.entities || []) {
49
- const smoothed = this._smoothEntity(entity, dt)
50
- displayEntities.push(smoothed)
69
+ const filter = this.entityFilters.get(entity.id)
70
+ if (filter && dt > 0) {
71
+ const predicted = filter.predict(dt)
72
+ displayEntities.push({ ...entity, position: predicted.position })
73
+ } else {
74
+ displayEntities.push(entity)
75
+ }
51
76
  }
52
-
77
+
53
78
  return { players: displayPlayers, entities: displayEntities }
54
79
  }
55
-
56
- _smoothPlayer(player, dt) {
57
- let filter = this.playerFilters.get(player.id)
58
- if (!filter) {
59
- filter = new KalmanFilter3D(this.kalmanConfig)
60
- this.playerFilters.set(player.id, filter)
61
- }
62
-
63
- const state = filter.update(player.position, player.velocity)
64
-
65
- return {
66
- ...player,
67
- position: state.position,
68
- velocity: state.velocity
69
- }
70
- }
71
-
72
- _smoothEntity(entity, dt) {
73
- let filter = this.entityFilters.get(entity.id)
74
- if (!filter) {
75
- filter = new KalmanFilter3D(this.kalmanConfig)
76
- this.entityFilters.set(entity.id, filter)
77
- }
78
-
79
- const state = filter.update(entity.position)
80
-
81
- return {
82
- ...entity,
83
- position: state.position
84
- }
85
- }
86
-
87
- predictStep(dt) {
88
- for (const [id, filter] of this.playerFilters) {
89
- filter.predict(dt)
90
- }
91
- for (const [id, filter] of this.entityFilters) {
92
- filter.predict(dt)
93
- }
94
- }
95
-
96
- removePlayer(id) {
97
- this.playerFilters.delete(id)
98
- }
99
-
100
- removeEntity(id) {
101
- this.entityFilters.delete(id)
102
- }
103
-
104
- updateRTT(pingTime, pongTime) {
105
- this.jitterBuffer.updateRTT(pingTime, pongTime)
106
- }
107
-
108
- getRTT() {
109
- return this.jitterBuffer.getRTT()
110
- }
111
-
112
- getBufferHealth() {
113
- return this.jitterBuffer.getBufferHealth()
114
- }
115
-
80
+
81
+ removePlayer(id) { this.playerFilters.delete(id) }
82
+ removeEntity(id) { this.entityFilters.delete(id) }
83
+
84
+ updateRTT(pingTime, pongTime) { this.jitterBuffer.updateRTT(pingTime, pongTime) }
85
+ getRTT() { return this.jitterBuffer.getRTT() }
86
+ getJitter() { return this.jitterBuffer.getJitter() }
87
+ getTargetDelay() { return this.jitterBuffer.getTargetDelay() }
88
+ getBufferHealth() { return this.jitterBuffer.getBufferHealth() }
89
+
116
90
  reset() {
117
91
  this.jitterBuffer.clear()
118
92
  this.playerFilters.clear()
119
93
  this.entityFilters.clear()
94
+ this._lastDisplayTime = 0
120
95
  }
121
-
96
+
122
97
  setConfig(config) {
123
- if (config.kalman) {
124
- this.kalmanConfig = { ...this.kalmanConfig, ...config.kalman }
125
- }
98
+ if (config.playerKalman) this.playerKalmanConfig = { ...this.playerKalmanConfig, ...config.playerKalman }
99
+ if (config.entityKalman) this.entityKalmanConfig = { ...this.entityKalmanConfig, ...config.entityKalman }
126
100
  }
127
- }
101
+ }