spoint 0.1.59 → 0.1.61

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')
@@ -1032,30 +1033,24 @@ const client = new PhysicsNetworkClient({
1032
1033
  predictionEnabled: true,
1033
1034
  smoothInterpolation: true,
1034
1035
  onStateUpdate: (state) => {
1035
- const smoothState = client.getSmoothState()
1036
- for (const p of smoothState.players) {
1036
+ for (const p of state.players) {
1037
1037
  if (!playerMeshes.has(p.id)) createPlayerVRM(p.id)
1038
- const mesh = playerMeshes.get(p.id)
1039
- const feetOff = mesh?.userData?.feetOffset ?? 1.3
1040
- const tx = p.position[0], ty = p.position[1] - feetOff, tz = p.position[2]
1041
- const existingTarget = playerTargets.get(p.id)
1042
- if (existingTarget) { existingTarget.x = tx; existingTarget.y = ty; existingTarget.z = tz }
1043
- else playerTargets.set(p.id, { x: tx, y: ty, z: tz })
1044
- playerStates.set(p.id, p)
1045
- const dx = tx - mesh.position.x, dy = ty - mesh.position.y, dz = tz - mesh.position.z
1046
- if (!mesh.userData.initialized || dx * dx + dy * dy + dz * dz > 100) { mesh.position.set(tx, ty, tz); mesh.userData.initialized = true }
1047
1038
  }
1048
- for (const e of smoothState.entities) {
1039
+ for (const e of state.entities) {
1049
1040
  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])
1041
+ if (mesh && e.position) {
1042
+ const et = entityTargets.get(e.id)
1043
+ 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 }
1044
+ 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 })
1045
+ const dx = e.position[0] - mesh.position.x, dy = e.position[1] - mesh.position.y, dz = e.position[2] - mesh.position.z
1046
+ 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 }
1047
+ }
1052
1048
  if (!entityMeshes.has(e.id)) loadEntityModel(e.id, e)
1053
1049
  }
1054
- rebuildEntityHierarchy(smoothState.entities)
1055
1050
  latestState = state
1056
1051
  if (!firstSnapshotReceived) {
1057
1052
  firstSnapshotReceived = true
1058
- for (const e of smoothState.entities) {
1053
+ for (const e of state.entities) {
1059
1054
  if (e.model && !entityMeshes.has(e.id)) firstSnapshotEntityPending.add(e.id)
1060
1055
  }
1061
1056
  checkAllLoaded()
@@ -1064,7 +1059,7 @@ const client = new PhysicsNetworkClient({
1064
1059
  onPlayerJoined: (id) => { if (!playerMeshes.has(id)) createPlayerVRM(id) },
1065
1060
  onPlayerLeft: (id) => removePlayerMesh(id),
1066
1061
  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) },
1062
+ 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
1063
  onWorldDef: (wd) => {
1069
1064
  loadingMgr.setStage('SERVER_SYNC')
1070
1065
  worldConfig = wd
@@ -1461,7 +1456,27 @@ function animate(timestamp) {
1461
1456
  const frameDt = smoothDt
1462
1457
  fpsFrames++
1463
1458
  if (now - fpsLast >= 1000) { fpsDisplay = fpsFrames; fpsFrames = 0; fpsLast = now }
1464
- const lerpFactor = 1.0 - Math.exp(-16.0 * frameDt)
1459
+ const rttMs = client.getRTT?.() || 0
1460
+ const lerpConstant = rttMs > 100 ? 24.0 : 16.0
1461
+ const lerpFactor = 1.0 - Math.exp(-lerpConstant * frameDt)
1462
+ const smoothState = client.getSmoothState()
1463
+ for (const p of smoothState.players) {
1464
+ if (!playerMeshes.has(p.id)) continue
1465
+ const mesh = playerMeshes.get(p.id)
1466
+ const feetOff = mesh?.userData?.feetOffset ?? 1.3
1467
+ const tx = p.position[0], ty = p.position[1] - feetOff, tz = p.position[2]
1468
+ const existingTarget = playerTargets.get(p.id)
1469
+ if (existingTarget) { existingTarget.x = tx; existingTarget.y = ty; existingTarget.z = tz }
1470
+ else playerTargets.set(p.id, { x: tx, y: ty, z: tz })
1471
+ playerStates.set(p.id, p)
1472
+ if (!mesh.userData.initialized) { mesh.position.set(tx, ty, tz); mesh.userData.initialized = true }
1473
+ }
1474
+ for (const e of smoothState.entities) {
1475
+ const mesh = entityMeshes.get(e.id)
1476
+ if (mesh && e.position) mesh.position.set(e.position[0], e.position[1], e.position[2])
1477
+ if (mesh && e.rotation) mesh.quaternion.set(e.rotation[0], e.rotation[1], e.rotation[2], e.rotation[3])
1478
+ }
1479
+ if (smoothState.entities.length > 0) rebuildEntityHierarchy(smoothState.entities)
1465
1480
  playerTargets.forEach((target, id) => {
1466
1481
  const mesh = playerMeshes.get(id)
1467
1482
  if (!mesh) return
@@ -1496,6 +1511,18 @@ function animate(timestamp) {
1496
1511
  if (features?._headBone) features._headBone.rotation.x = -(ps.lookPitch || 0) * 0.6
1497
1512
  }
1498
1513
  })
1514
+ entityTargets.forEach((target, id) => {
1515
+ const mesh = entityMeshes.get(id)
1516
+ if (!mesh) return
1517
+ mesh.position.x += (target.x - mesh.position.x) * lerpFactor
1518
+ mesh.position.y += (target.y - mesh.position.y) * lerpFactor
1519
+ mesh.position.z += (target.z - mesh.position.z) * lerpFactor
1520
+ mesh.quaternion.x += (target.rx - mesh.quaternion.x) * lerpFactor
1521
+ mesh.quaternion.y += (target.ry - mesh.quaternion.y) * lerpFactor
1522
+ mesh.quaternion.z += (target.rz - mesh.quaternion.z) * lerpFactor
1523
+ mesh.quaternion.w += (target.rw - mesh.quaternion.w) * lerpFactor
1524
+ mesh.quaternion.normalize()
1525
+ })
1499
1526
  entityMeshes.forEach((mesh) => {
1500
1527
  if (mesh.userData.spin) mesh.rotation.y += mesh.userData.spin * frameDt
1501
1528
  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.61",
4
4
  "description": "Physics and netcode SDK for multiplayer game servers",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,207 +1,131 @@
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(400, this.rtt + this.jitter * 3 + 150)
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
+ const alpha = instant > this.rtt ? 0.5 : 0.1
115
+ this.rtt = this.rtt * (1 - alpha) + instant * alpha
116
+ this.targetDelay = Math.min(250, this.baseDelay + this.rtt * 0.5 + this.jitter * 2)
200
117
  }
201
-
118
+
119
+ getBufferHealth() { return this.buffer.length }
120
+ getRTT() { return this.rtt }
121
+ getJitter() { return this.jitter }
122
+ getTargetDelay() { return this.targetDelay }
123
+
202
124
  clear() {
203
125
  this.buffer = []
204
126
  this.lastServerTime = 0
205
127
  this.lastClientTime = 0
206
128
  }
207
- }
129
+ }
130
+
131
+ 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
  }
@@ -2,35 +2,22 @@ export class ReconciliationEngine {
2
2
  constructor(config = {}) {
3
3
  this.correctionThreshold = config.correctionThreshold || 0.01
4
4
  this.correctionSpeed = config.correctionSpeed || 0.5
5
- this.lastReconcileTime = 0
6
- this.reconcileInterval = config.reconcileInterval || 100
7
5
  }
8
6
 
9
7
  reconcile(serverState, localState, tick) {
10
- const now = Date.now()
11
- if (now - this.lastReconcileTime < this.reconcileInterval) {
12
- return { needsCorrection: false, correction: null }
13
- }
14
-
15
- this.lastReconcileTime = now
16
-
17
8
  const divergence = this.calculateDivergence(serverState, localState)
18
-
19
9
  if (divergence < this.correctionThreshold) {
20
10
  return { needsCorrection: false, divergence }
21
11
  }
22
-
23
12
  const correction = this.generateCorrection(serverState, localState)
24
13
  return { needsCorrection: true, correction, divergence }
25
14
  }
26
15
 
27
16
  calculateDivergence(serverState, localState) {
28
17
  if (!serverState || !localState) return 0
29
-
30
18
  const dx = serverState.position[0] - localState.position[0]
31
19
  const dy = serverState.position[1] - localState.position[1]
32
20
  const dz = serverState.position[2] - localState.position[2]
33
-
34
21
  return Math.sqrt(dx * dx + dy * dy + dz * dz)
35
22
  }
36
23
 
@@ -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,95 @@ 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
11
+ }
12
+ this.entityKalmanConfig = config.entityKalman || {
13
+ positionQ: 2.0, velocityQ: 4.0, positionR: 0.01, velocityR: 0.5
13
14
  }
14
-
15
- this.lastFrameTime = Date.now()
16
15
  this.localPlayerId = null
17
16
  this.predictionEnabled = config.predictionEnabled !== false
18
- this.extrapolationLimit = config.extrapolationLimit || 100
19
- }
20
-
21
- setLocalPlayer(id) {
22
- this.localPlayerId = id
17
+ this._lastDisplayTime = 0
23
18
  }
24
-
19
+
20
+ setLocalPlayer(id) { this.localPlayerId = id }
21
+
25
22
  addSnapshot(snapshot) {
26
23
  this.jitterBuffer.addSnapshot(snapshot)
24
+ const now = Date.now()
25
+ for (const p of snapshot.players || []) {
26
+ if (p.id === this.localPlayerId && this.predictionEnabled) continue
27
+ let filter = this.playerFilters.get(p.id)
28
+ if (!filter) {
29
+ filter = new KalmanFilter3D(this.playerKalmanConfig)
30
+ this.playerFilters.set(p.id, filter)
31
+ }
32
+ filter.update(p.position, p.velocity, now)
33
+ }
34
+ for (const e of snapshot.entities || []) {
35
+ let filter = this.entityFilters.get(e.id)
36
+ if (!filter) {
37
+ filter = new KalmanFilter3D(this.entityKalmanConfig)
38
+ this.entityFilters.set(e.id, filter)
39
+ }
40
+ filter.update(e.position, null, now)
41
+ }
27
42
  }
28
-
43
+
29
44
  getDisplayState(now = Date.now()) {
30
45
  const snapshot = this.jitterBuffer.getSnapshotToRender(now)
31
46
  if (!snapshot) return { players: [], entities: [] }
32
-
33
- const dt = Math.min((now - this.lastFrameTime) / 1000, 0.1)
34
- this.lastFrameTime = now
35
-
47
+
48
+ const dt = this._lastDisplayTime > 0 ? Math.min((now - this._lastDisplayTime) / 1000, 0.1) : 0
49
+ this._lastDisplayTime = now
50
+
36
51
  const displayPlayers = []
37
52
  for (const player of snapshot.players || []) {
38
53
  if (player.id === this.localPlayerId && this.predictionEnabled) {
39
54
  displayPlayers.push(player)
40
55
  continue
41
56
  }
42
-
43
- const smoothed = this._smoothPlayer(player, dt)
44
- displayPlayers.push(smoothed)
57
+ const filter = this.playerFilters.get(player.id)
58
+ if (filter && dt > 0) {
59
+ const predicted = filter.predict(dt)
60
+ displayPlayers.push({ ...player, position: predicted.position, velocity: predicted.velocity })
61
+ } else {
62
+ displayPlayers.push(player)
63
+ }
45
64
  }
46
-
65
+
47
66
  const displayEntities = []
48
67
  for (const entity of snapshot.entities || []) {
49
- const smoothed = this._smoothEntity(entity, dt)
50
- displayEntities.push(smoothed)
68
+ const filter = this.entityFilters.get(entity.id)
69
+ if (filter && dt > 0) {
70
+ const predicted = filter.predict(dt)
71
+ displayEntities.push({ ...entity, position: predicted.position })
72
+ } else {
73
+ displayEntities.push(entity)
74
+ }
51
75
  }
52
-
76
+
53
77
  return { players: displayPlayers, entities: displayEntities }
54
78
  }
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
-
79
+
80
+ removePlayer(id) { this.playerFilters.delete(id) }
81
+ removeEntity(id) { this.entityFilters.delete(id) }
82
+
83
+ updateRTT(pingTime, pongTime) { this.jitterBuffer.updateRTT(pingTime, pongTime) }
84
+ getRTT() { return this.jitterBuffer.getRTT() }
85
+ getJitter() { return this.jitterBuffer.getJitter() }
86
+ getTargetDelay() { return this.jitterBuffer.getTargetDelay() }
87
+ getBufferHealth() { return this.jitterBuffer.getBufferHealth() }
88
+
116
89
  reset() {
117
90
  this.jitterBuffer.clear()
118
91
  this.playerFilters.clear()
119
92
  this.entityFilters.clear()
93
+ this._lastDisplayTime = 0
120
94
  }
121
-
95
+
122
96
  setConfig(config) {
123
- if (config.kalman) {
124
- this.kalmanConfig = { ...this.kalmanConfig, ...config.kalman }
125
- }
97
+ if (config.playerKalman) this.playerKalmanConfig = { ...this.playerKalmanConfig, ...config.playerKalman }
98
+ if (config.entityKalman) this.entityKalmanConfig = { ...this.entityKalmanConfig, ...config.entityKalman }
126
99
  }
127
- }
100
+ }