fix(complement-race): fix ghost train position update lag and reload position reset
Fixed three critical multiplayer issues: 1. **Fixed interval restart bug**: Position broadcast interval was constantly restarting because useEffect depended on `compatibleState`, which changed on every position update. Now uses stable dependencies (`multiplayerState.gamePhase`, etc.) 2. **Increased broadcast frequency**: Changed from 200ms (5 Hz) to 100ms (10 Hz) for smoother ghost train movement during multiplayer races 3. **Fixed position reset on reload**: Client position now syncs from server's authoritative position when browser reloads, preventing trains from resetting to start of track Additional fixes: - Used refs for `sendMove` to prevent interval recreation - Removed unused imports (useEffect from GhostTrain, SteamTrainJourney) - Added strategic logging for position broadcast and reception 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b95fc1fdff
commit
ad78a65ed7
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import type { PlayerState } from '@/arcade-games/complement-race/types'
|
||||
import type { RailroadTrackGenerator } from '../../lib/RailroadTrackGenerator'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import { animated, useSpring } from '@react-spring/web'
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useMemo, useRef, useState } from 'react'
|
||||
import { useGameMode } from '@/contexts/GameModeContext'
|
||||
import { useUserProfile } from '@/contexts/UserProfileContext'
|
||||
import { useComplementRace } from '@/arcade-games/complement-race/Provider'
|
||||
|
|
|
|||
|
|
@ -318,6 +318,9 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
|||
const [clientPosition, setClientPosition] = useState(0)
|
||||
const [clientPressure, setClientPressure] = useState(0)
|
||||
|
||||
// Track if we've synced position from server (for reconnect/reload scenarios)
|
||||
const hasInitializedPositionRef = useRef(false)
|
||||
|
||||
// Ref to track latest position for broadcasting (avoids recreating interval on every position change)
|
||||
const clientPositionRef = useRef(clientPosition)
|
||||
|
||||
|
|
@ -326,6 +329,12 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
|||
const broadcastCountRef = useRef(0)
|
||||
const lastReceivedPositionsRef = useRef<Record<string, number>>({})
|
||||
|
||||
// Ref to hold sendMove so interval doesn't restart when sendMove changes
|
||||
const sendMoveRef = useRef(sendMove)
|
||||
useEffect(() => {
|
||||
sendMoveRef.current = sendMove
|
||||
}, [sendMove])
|
||||
|
||||
const [clientAIRacers, setClientAIRacers] = useState<
|
||||
Array<{
|
||||
id: string
|
||||
|
|
@ -452,16 +461,45 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
|||
clientAIRacers,
|
||||
])
|
||||
|
||||
// Sync client position from server on reconnect/reload (multiplayer only)
|
||||
useEffect(() => {
|
||||
// Only sync if:
|
||||
// 1. We haven't synced yet
|
||||
// 2. Game is active
|
||||
// 3. We're in sprint mode
|
||||
// 4. We have a local player with a position from server
|
||||
if (
|
||||
!hasInitializedPositionRef.current &&
|
||||
multiplayerState.gamePhase === 'playing' &&
|
||||
multiplayerState.config.style === 'sprint' &&
|
||||
localPlayerId
|
||||
) {
|
||||
const serverPosition = multiplayerState.players[localPlayerId]?.position
|
||||
if (serverPosition !== undefined && serverPosition > 0) {
|
||||
console.log(`[POSITION_SYNC] Restoring position from server: ${serverPosition.toFixed(1)}%`)
|
||||
setClientPosition(serverPosition)
|
||||
hasInitializedPositionRef.current = true
|
||||
}
|
||||
}
|
||||
|
||||
// Reset sync flag when game ends
|
||||
if (multiplayerState.gamePhase !== 'playing') {
|
||||
hasInitializedPositionRef.current = false
|
||||
}
|
||||
}, [multiplayerState.gamePhase, multiplayerState.config.style, multiplayerState.players, localPlayerId])
|
||||
|
||||
// Initialize game start time when game becomes active
|
||||
useEffect(() => {
|
||||
if (compatibleState.isGameActive && compatibleState.style === 'sprint') {
|
||||
if (gameStartTimeRef.current === 0) {
|
||||
gameStartTimeRef.current = Date.now()
|
||||
lastUpdateRef.current = Date.now()
|
||||
// Reset client state for new game
|
||||
setClientMomentum(10) // Start with gentle push
|
||||
setClientPosition(0)
|
||||
setClientPressure((10 / 100) * 150) // Initial pressure from starting momentum
|
||||
// Reset client state for new game (only if not restored from server)
|
||||
if (!hasInitializedPositionRef.current) {
|
||||
setClientMomentum(10) // Start with gentle push
|
||||
setClientPosition(0)
|
||||
setClientPressure((10 / 100) * 150) // Initial pressure from starting momentum
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Reset when game ends
|
||||
|
|
@ -589,13 +627,16 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
// Broadcast position to server for multiplayer ghost trains
|
||||
useEffect(() => {
|
||||
if (!compatibleState.isGameActive || compatibleState.style !== 'sprint' || !localPlayerId) {
|
||||
const isGameActive = multiplayerState.gamePhase === 'playing'
|
||||
const isSprint = multiplayerState.config.style === 'sprint'
|
||||
|
||||
if (!isGameActive || !isSprint || !localPlayerId) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[POS_BROADCAST] Starting position broadcast interval')
|
||||
|
||||
// Send position update every 200ms (reads from ref to avoid restarting interval)
|
||||
// Send position update every 100ms for smoother ghost trains (reads from refs to avoid restarting interval)
|
||||
const interval = setInterval(() => {
|
||||
const currentPos = clientPositionRef.current
|
||||
broadcastCountRef.current++
|
||||
|
|
@ -612,19 +653,19 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
|||
lastBroadcastLogRef.current = { position: currentPos, time: now }
|
||||
}
|
||||
|
||||
sendMove({
|
||||
sendMoveRef.current({
|
||||
type: 'UPDATE_POSITION',
|
||||
playerId: localPlayerId,
|
||||
userId: viewerId || '',
|
||||
data: { position: currentPos },
|
||||
} as ComplementRaceMove)
|
||||
}, 200)
|
||||
}, 100)
|
||||
|
||||
return () => {
|
||||
console.log(`[POS_BROADCAST] Stopping interval (sent ${broadcastCountRef.current} updates)`)
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [compatibleState.isGameActive, compatibleState.style, localPlayerId, viewerId, sendMove])
|
||||
}, [multiplayerState.gamePhase, multiplayerState.config.style, localPlayerId, viewerId])
|
||||
|
||||
// Keep lastLogRef for future debugging needs
|
||||
// (removed debug logging)
|
||||
|
|
|
|||
Loading…
Reference in New Issue