fix game
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m29s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m29s
This commit is contained in:
@@ -61,23 +61,72 @@ func (g *Game) UpdateGame() {
|
||||
// Down: Joystick nach unten ziehen ODER Down-Button
|
||||
isJoyDown := g.joyActive && (g.joyStickY-g.joyBaseY) > g.joyRadius*0.5
|
||||
|
||||
// Input State zusammenbauen
|
||||
input := InputState{
|
||||
Sequence: g.inputSequence,
|
||||
Left: keyLeft || joyDir < -0.1,
|
||||
Right: keyRight || joyDir > 0.1,
|
||||
Jump: keyJump || g.btnJumpActive,
|
||||
Down: keyDown || isJoyDown || g.btnDownActive,
|
||||
JoyX: joyDir,
|
||||
}
|
||||
wantsJump := keyJump || g.btnJumpActive
|
||||
g.btnJumpActive = false
|
||||
g.btnDownActive = false
|
||||
|
||||
// Jump Buffer: Sprung-Wunsch für bis zu 6 Physics-Frames (=300ms) speichern
|
||||
if wantsJump {
|
||||
g.jumpBufferFrames = 6
|
||||
}
|
||||
|
||||
// --- 4. INPUT SENDEN (MIT CLIENT PREDICTION) ---
|
||||
// Prediction läuft bei jedem Frame (60 TPS) für flüssige Darstellung.
|
||||
// Netz-Send wird auf ~20/sek gedrosselt um Server-Last zu begrenzen.
|
||||
if g.connected {
|
||||
// Wichtig: Prediction und Send müssen synchron laufen.
|
||||
// Nur wenn wir senden, speichern wir den Input in pendingInputs.
|
||||
if g.connected && time.Since(g.lastInputTime) >= 50*time.Millisecond {
|
||||
g.lastInputTime = time.Now()
|
||||
|
||||
// Coyote Time: War auf dem Boden, jetzt nicht mehr → Timer setzen
|
||||
g.predictionMutex.Lock()
|
||||
wasOnGround := g.predictedGround
|
||||
g.predictionMutex.Unlock()
|
||||
|
||||
// Coyote Time Countdown
|
||||
if g.coyoteFrames > 0 {
|
||||
g.coyoteFrames--
|
||||
}
|
||||
|
||||
// Jump Buffer Countdown
|
||||
if g.jumpBufferFrames > 0 {
|
||||
g.jumpBufferFrames--
|
||||
}
|
||||
|
||||
// Effektiven Jump bestimmen:
|
||||
// - Direkter Druck, ODER
|
||||
// - Jump-Buffer aktiv UND jetzt auf dem Boden (Sprung kurz vor Landung)
|
||||
g.predictionMutex.Lock()
|
||||
onGround := g.predictedGround
|
||||
g.predictionMutex.Unlock()
|
||||
|
||||
if wasOnGround && !onGround {
|
||||
// Gerade von Kante abgegangen → Coyote Time starten
|
||||
g.coyoteFrames = 4
|
||||
}
|
||||
|
||||
effectiveJump := wantsJump ||
|
||||
(g.jumpBufferFrames > 0 && onGround) ||
|
||||
(wantsJump && g.coyoteFrames > 0)
|
||||
|
||||
if effectiveJump {
|
||||
g.jumpBufferFrames = 0 // Buffer verbraucht
|
||||
}
|
||||
|
||||
// Input State zusammenbauen
|
||||
input := InputState{
|
||||
Sequence: g.inputSequence,
|
||||
Left: keyLeft || joyDir < -0.1,
|
||||
Right: keyRight || joyDir > 0.1,
|
||||
Jump: effectiveJump,
|
||||
Down: keyDown || isJoyDown,
|
||||
JoyX: joyDir,
|
||||
}
|
||||
|
||||
g.predictionMutex.Lock()
|
||||
// Position vor Physics-Step merken (für Interpolation)
|
||||
g.prevPredictedX = g.predictedX
|
||||
g.prevPredictedY = g.predictedY
|
||||
g.lastPhysicsTime = time.Now()
|
||||
|
||||
// Sequenznummer erhöhen
|
||||
g.inputSequence++
|
||||
input.Sequence = g.inputSequence
|
||||
@@ -88,9 +137,9 @@ func (g *Game) UpdateGame() {
|
||||
// Input für History speichern (für Server-Reconciliation)
|
||||
g.pendingInputs[input.Sequence] = input
|
||||
|
||||
// Cap: nie mehr als 120 unbestätigte Inputs ansammeln
|
||||
if len(g.pendingInputs) > 120 {
|
||||
oldest := g.inputSequence - 120
|
||||
// Cap: nie mehr als 60 unbestätigte Inputs ansammeln (~3 Sek bei 20/sek)
|
||||
if len(g.pendingInputs) > 60 {
|
||||
oldest := g.inputSequence - 60
|
||||
for seq := range g.pendingInputs {
|
||||
if seq < oldest {
|
||||
delete(g.pendingInputs, seq)
|
||||
@@ -100,25 +149,21 @@ func (g *Game) UpdateGame() {
|
||||
|
||||
g.predictionMutex.Unlock()
|
||||
|
||||
// Netz-Throttle: max 20 Inputs/Sek senden
|
||||
if time.Since(g.lastInputTime) >= 50*time.Millisecond {
|
||||
g.lastInputTime = time.Now()
|
||||
g.SendInputWithSequence(input)
|
||||
}
|
||||
g.SendInputWithSequence(input)
|
||||
}
|
||||
|
||||
// --- 5. KAMERA LOGIK ---
|
||||
// --- 5. KAMERA LOGIK (mit Smoothing) ---
|
||||
g.stateMutex.Lock()
|
||||
targetCam := g.gameState.ScrollX
|
||||
g.stateMutex.Unlock()
|
||||
|
||||
// Negative Kamera verhindern
|
||||
if targetCam < 0 {
|
||||
targetCam = 0
|
||||
}
|
||||
|
||||
// Kamera hart setzen
|
||||
g.camX = targetCam
|
||||
// Sanftes Kamera-Folgen: 20% pro Frame Richtung Ziel (bei 60fps ≈ 95ms Halbwertszeit)
|
||||
diff := targetCam - g.camX
|
||||
g.camX += diff * 0.2
|
||||
|
||||
// --- 6. CORRECTION OFFSET ABKLINGEN ---
|
||||
// Der visuelle Offset sorgt dafür dass Server-Korrekturen sanft und unsichtbar sind.
|
||||
@@ -386,12 +431,17 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
||||
vy := p.VY
|
||||
onGround := p.OnGround
|
||||
|
||||
// Für lokalen Spieler: Verwende Client-Prediction + visuellen Korrektur-Offset
|
||||
// correctionOffset sorgt dafür dass Server-Korrekturen sanft aussehen
|
||||
// Für lokalen Spieler: Client-Prediction mit Interpolation zwischen Physics-Steps
|
||||
// Physics läuft bei 20/sec, Draw bei 60fps → alpha interpoliert dazwischen
|
||||
if p.Name == g.playerName {
|
||||
g.predictionMutex.Lock()
|
||||
posX = g.predictedX + g.correctionOffsetX
|
||||
posY = g.predictedY + g.correctionOffsetY
|
||||
// Interpolations-Alpha: wie weit sind wir zwischen letztem und nächstem Physics-Step?
|
||||
alpha := float64(time.Since(g.lastPhysicsTime)) / float64(50*time.Millisecond)
|
||||
if alpha > 1 {
|
||||
alpha = 1
|
||||
}
|
||||
posX = g.prevPredictedX + (g.predictedX-g.prevPredictedX)*alpha + g.correctionOffsetX
|
||||
posY = g.prevPredictedY + (g.predictedY-g.prevPredictedY)*alpha + g.correctionOffsetY
|
||||
g.predictionMutex.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,17 @@ type Game struct {
|
||||
lastRecvSeq uint32 // Letzte empfangene Server-Sequenznummer (für Out-of-Order-Erkennung)
|
||||
lastInputTime time.Time // Letzter Input-Send (für 20 TPS Throttling)
|
||||
|
||||
// Interpolation (60fps Draw ↔ 20hz Physics)
|
||||
prevPredictedX float64 // Position vor letztem Physics-Step (für Interpolation)
|
||||
prevPredictedY float64
|
||||
lastPhysicsTime time.Time // Zeitpunkt des letzten Physics-Steps
|
||||
|
||||
// Jump Buffer: Sprung kurz vor Landung speichern → löst beim Aufkommen aus
|
||||
jumpBufferFrames int // Countdown in Physics-Frames (bei 0: kein Buffer)
|
||||
|
||||
// Coyote Time: Sprung kurz nach Abgang von Kante erlauben
|
||||
coyoteFrames int // Countdown in Physics-Frames
|
||||
|
||||
// Smooth Correction (Debug-Info)
|
||||
correctionX float64 // Letzte Korrektur-Magnitude X
|
||||
correctionY float64 // Letzte Korrektur-Magnitude Y
|
||||
@@ -176,6 +187,9 @@ func NewGame() *Game {
|
||||
fpsSampleTime: time.Now(),
|
||||
lastUpdateTime: time.Now(),
|
||||
|
||||
// Interpolation
|
||||
lastPhysicsTime: time.Now(),
|
||||
|
||||
joyBaseX: 150, joyBaseY: ScreenHeight - 150,
|
||||
joyStickX: 150, joyStickY: ScreenHeight - 150,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user