Private
Public Access
1
0

fix game
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m29s

This commit is contained in:
Sebastian Unterschütz
2026-03-22 10:14:20 +01:00
parent 7a86a8596e
commit 1dc5005cf3
2 changed files with 92 additions and 28 deletions

View File

@@ -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()
}

View File

@@ -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,
}