Skip to content

Commit e62e98a

Browse files
committed
fix: lock ball to player sprite during possession to prevent visual trailing
When a player possesses the ball, the server sets ball velocity to 0 and places it at an offset from the player. On the client, the player sprite is predicted ahead via dead reckoning, but the ball was lerping slowly toward the stale server position — causing it to visibly trail behind. Now computes the server-side offset (ball pos - player pos) and applies it to the local predicted sprite position, keeping the ball locked to the player visually.
1 parent d043548 commit e62e98a

File tree

1 file changed

+26
-3
lines changed

1 file changed

+26
-3
lines changed

client/src/scenes/MultiplayerScene.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,13 +577,36 @@ export class MultiplayerScene extends BaseGameScene {
577577
if (this.ball.x == null || this.ball.y == null || isNaN(this.ball.x) || isNaN(this.ball.y)) {
578578
this.ball.x = serverBall.x
579579
this.ball.y = serverBall.y
580+
} else if (serverBall.possessedBy) {
581+
// Ball is possessed — lock it to the possessing player's SPRITE position
582+
// instead of lerping toward the server ball position. The server places
583+
// the ball at player + direction * POSSESSION_BALL_OFFSET, but the player
584+
// sprite is predicted ahead of the server (dead reckoning / client prediction).
585+
// Lerping would make the ball visibly trail the player.
586+
const possessorSprite = this.players.get(serverBall.possessedBy)
587+
if (possessorSprite) {
588+
// Use the server ball position relative to the server player position
589+
// to compute the offset, then apply it to the local sprite position.
590+
const possessorState = state.players?.get(serverBall.possessedBy)
591+
if (possessorState) {
592+
const offsetX = serverBall.x - possessorState.x
593+
const offsetY = serverBall.y - possessorState.y
594+
this.ball.x = possessorSprite.x + offsetX
595+
this.ball.y = possessorSprite.y + offsetY
596+
} else {
597+
this.ball.x = serverBall.x
598+
this.ball.y = serverBall.y
599+
}
600+
} else {
601+
this.ball.x = serverBall.x
602+
this.ball.y = serverBall.y
603+
}
580604
} else {
581-
// Dead reckoning: extrapolate ball using last known velocity to fill gaps between patches.
582-
// Skip when ball is possessed — velocity is stale and the ball tracks the player instead.
605+
// Ball is free — dead reckoning extrapolation + lerp
583606
let targetX = this.lastBallServerX
584607
let targetY = this.lastBallServerY
585608

586-
if (!serverBall.possessedBy && this.lastBallStateReceivedAt > 0) {
609+
if (this.lastBallStateReceivedAt > 0) {
587610
const dtS = Math.min((now - this.lastBallStateReceivedAt) / 1000, 0.1) // cap at 100ms
588611
// Integrate ball friction: 0.98 per 60Hz physics step
589612
const frictionScale = Math.pow(GAME_CONFIG.BALL_FRICTION, dtS * 60)

0 commit comments

Comments
 (0)