Integrate shared physics engine for player movement and collision handling, refine 20 TPS gameplay logic, and enhance client prediction with server-reconciliation updates.
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m51s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m51s
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"image/color"
|
||||
"log"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
@@ -71,45 +72,18 @@ func (g *Game) UpdateGame() {
|
||||
}
|
||||
g.btnJumpActive = false
|
||||
|
||||
// --- 4. CLIENT PREDICTION ---
|
||||
// --- 4. INPUT SENDEN (MIT CLIENT PREDICTION) ---
|
||||
if g.connected {
|
||||
g.predictionMutex.Lock()
|
||||
// Sequenznummer erhöhen
|
||||
g.inputSequence++
|
||||
input.Sequence = g.inputSequence
|
||||
|
||||
// Input speichern für später Reconciliation
|
||||
g.pendingInputs[input.Sequence] = input
|
||||
|
||||
// Lokale Physik sofort anwenden (Prediction)
|
||||
// Lokale Prediction ausführen für sofortiges Feedback
|
||||
g.ApplyInput(input)
|
||||
|
||||
// Sanfte Korrektur anwenden (langsamer bei 20 TPS für weniger Jitter)
|
||||
const smoothingFactor = 0.15 // Reduziert für 20 TPS (war 0.4 bei 60 TPS)
|
||||
if g.correctionX != 0 || g.correctionY != 0 {
|
||||
g.predictedX += g.correctionX * smoothingFactor
|
||||
g.predictedY += g.correctionY * smoothingFactor
|
||||
|
||||
g.correctionX *= (1.0 - smoothingFactor)
|
||||
g.correctionY *= (1.0 - smoothingFactor)
|
||||
|
||||
// Korrektur beenden wenn sehr klein
|
||||
if g.correctionX*g.correctionX+g.correctionY*g.correctionY < 1.0 {
|
||||
g.correctionX = 0
|
||||
g.correctionY = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Landing Detection für Partikel
|
||||
if !g.lastGroundState && g.predictedGround {
|
||||
// Gerade gelandet! Partikel direkt unter dem Spieler (an den Füßen)
|
||||
// Füße sind bei: Y + DrawOffY + Hitbox.OffsetY + Hitbox.H
|
||||
// = Y - 231 + 42 + 184 = Y - 5
|
||||
feetY := g.predictedY - 231 + 42 + 184
|
||||
centerX := g.predictedX - 56 + 68 + 73/2
|
||||
g.SpawnLandingParticles(centerX, feetY)
|
||||
}
|
||||
g.lastGroundState = g.predictedGround
|
||||
// Input für History speichern (für Server-Reconciliation)
|
||||
g.pendingInputs[input.Sequence] = input
|
||||
|
||||
g.predictionMutex.Unlock()
|
||||
|
||||
@@ -231,7 +205,7 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
||||
|
||||
// In WASM: HTML Game Over Screen anzeigen
|
||||
if !g.scoreSubmitted {
|
||||
g.submitScore() // submitScore() setzt g.scoreSubmitted intern
|
||||
g.submitScore() // submitScore() setzt g.scoreSubmitted intern
|
||||
g.sendGameOverToJS(myScore) // Zeigt HTML Game Over Screen
|
||||
}
|
||||
|
||||
@@ -262,23 +236,26 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
||||
backgroundID = "background1"
|
||||
}
|
||||
|
||||
// Hintergrundbild zeichnen (skaliert auf Bildschirmgröße)
|
||||
// Hintergrundbild zeichnen (skaliert auf tatsächliche Canvas-Größe)
|
||||
if bgImg, exists := g.assetsImages[backgroundID]; exists && bgImg != nil {
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
|
||||
// Skalierung berechnen, um Bildschirm zu füllen
|
||||
// Tatsächliche Canvas-Größe verwenden (nicht nur ScreenWidth/Height)
|
||||
canvasW, canvasH := screen.Size()
|
||||
bgW, bgH := bgImg.Size()
|
||||
scaleX := float64(ScreenWidth) / float64(bgW)
|
||||
scaleY := float64(ScreenHeight) / float64(bgH)
|
||||
|
||||
// Skalierung berechnen, um Canvas komplett zu füllen
|
||||
scaleX := float64(canvasW) / float64(bgW)
|
||||
scaleY := float64(canvasH) / float64(bgH)
|
||||
scale := math.Max(scaleX, scaleY) // Größere Skalierung verwenden, um zu füllen
|
||||
|
||||
op.GeoM.Scale(scale, scale)
|
||||
|
||||
// Zentrieren
|
||||
// Zentrieren auf Canvas
|
||||
scaledW := float64(bgW) * scale
|
||||
scaledH := float64(bgH) * scale
|
||||
offsetX := (float64(ScreenWidth) - scaledW) / 2
|
||||
offsetY := (float64(ScreenHeight) - scaledH) / 2
|
||||
offsetX := (float64(canvasW) - scaledW) / 2
|
||||
offsetY := (float64(canvasH) - scaledH) / 2
|
||||
op.GeoM.Translate(offsetX, offsetY)
|
||||
|
||||
screen.DrawImage(bgImg, op)
|
||||
@@ -336,26 +313,22 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
||||
g.DrawAsset(screen, mp.AssetID, mp.X, mp.Y)
|
||||
}
|
||||
|
||||
// 3. Spieler
|
||||
// MyID ohne Lock holen (wir haben bereits den stateMutex)
|
||||
myID := ""
|
||||
for id, p := range g.gameState.Players {
|
||||
if p.Name == g.playerName {
|
||||
myID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
// 2.6 DEBUG: Basis-Boden-Collider visualisieren (GRÜN) - UNTER dem Gras bis tief in die Erde
|
||||
vector.StrokeRect(screen, float32(-g.camX), float32(540), 10000, float32(5000), float32(2), color.RGBA{0, 255, 0, 255}, false)
|
||||
|
||||
// 3. Spieler
|
||||
for id, p := range g.gameState.Players {
|
||||
// Für lokalen Spieler: Verwende vorhergesagte Position
|
||||
posX, posY := p.X, p.Y
|
||||
vy := p.VY
|
||||
onGround := p.OnGround
|
||||
if id == myID && g.connected {
|
||||
|
||||
// Für lokalen Spieler: Verwende Client-Prediction Position
|
||||
// Die Reconciliation wird in ReconcileWithServer() (connection_*.go) gemacht
|
||||
if p.Name == g.playerName {
|
||||
g.predictionMutex.Lock()
|
||||
posX = g.predictedX
|
||||
posY = g.predictedY
|
||||
vy = g.predictedVY
|
||||
onGround = g.predictedGround
|
||||
g.predictionMutex.Unlock()
|
||||
}
|
||||
|
||||
// Wähle Sprite basierend auf Sprung-Status
|
||||
@@ -385,82 +358,98 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
||||
}
|
||||
text.Draw(screen, name, basicfont.Face7x13, int(posX-g.camX), int(posY-25), ColText)
|
||||
|
||||
// DEBUG: Rote Hitbox
|
||||
// HITBOX VISUALISIERUNG (IMMER SICHTBAR)
|
||||
if def, ok := g.world.Manifest.Assets["player"]; ok {
|
||||
// Spieler-Hitbox (ROT)
|
||||
hx := float32(posX + def.DrawOffX + def.Hitbox.OffsetX - g.camX)
|
||||
hy := float32(posY + def.DrawOffY + def.Hitbox.OffsetY)
|
||||
vector.StrokeRect(screen, hx, hy, float32(def.Hitbox.W), float32(def.Hitbox.H), 2, color.RGBA{255, 0, 0, 255}, false)
|
||||
vector.StrokeRect(screen, hx, hy, float32(def.Hitbox.W), float32(def.Hitbox.H), 3, color.RGBA{255, 0, 0, 255}, false)
|
||||
|
||||
// Spieler-Position als Punkt (GELB)
|
||||
vector.DrawFilledCircle(screen, float32(posX-g.camX), float32(posY), 5, color.RGBA{255, 255, 0, 255}, false)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. UI Status
|
||||
// 4. UI Status (Canvas-relativ)
|
||||
canvasW, canvasH := screen.Size()
|
||||
|
||||
if g.gameState.Status == "COUNTDOWN" {
|
||||
msg := fmt.Sprintf("GO IN: %d", g.gameState.TimeLeft)
|
||||
text.Draw(screen, msg, basicfont.Face7x13, ScreenWidth/2-40, ScreenHeight/2, color.RGBA{255, 255, 0, 255})
|
||||
text.Draw(screen, msg, basicfont.Face7x13, canvasW/2-40, canvasH/2, color.RGBA{255, 255, 0, 255})
|
||||
} else if g.gameState.Status == "RUNNING" {
|
||||
// Score/Distance Anzeige mit grauem Hintergrund (oben rechts)
|
||||
dist := fmt.Sprintf("Distance: %.0f m", g.camX/64.0)
|
||||
text.Draw(screen, dist, basicfont.Face7x13, ScreenWidth-150, 30, ColText)
|
||||
|
||||
// Score anzeigen
|
||||
scoreStr := fmt.Sprintf("Score: %d", myScore)
|
||||
text.Draw(screen, scoreStr, basicfont.Face7x13, ScreenWidth-150, 50, ColText)
|
||||
|
||||
// Berechne Textbreiten für dynamische Box-Größe
|
||||
distLen := len(dist) * 7 // ~7px pro Zeichen
|
||||
scoreLen := len(scoreStr) * 7
|
||||
maxWidth := distLen
|
||||
if scoreLen > maxWidth {
|
||||
maxWidth = scoreLen
|
||||
}
|
||||
|
||||
boxWidth := float32(maxWidth + 20) // 10px Padding links/rechts
|
||||
boxHeight := float32(50)
|
||||
boxX := float32(canvasW) - boxWidth - 10 // 10px vom rechten Rand
|
||||
boxY := float32(10) // 10px vom oberen Rand
|
||||
|
||||
// Grauer halbtransparenter Hintergrund
|
||||
vector.DrawFilledRect(screen, boxX, boxY, boxWidth, boxHeight, color.RGBA{60, 60, 60, 200}, false)
|
||||
vector.StrokeRect(screen, boxX, boxY, boxWidth, boxHeight, 2, color.RGBA{100, 100, 100, 255}, false)
|
||||
|
||||
// Text (zentriert in Box)
|
||||
textX := int(boxX) + 10
|
||||
text.Draw(screen, dist, basicfont.Face7x13, textX, int(boxY)+22, color.RGBA{255, 255, 255, 255})
|
||||
text.Draw(screen, scoreStr, basicfont.Face7x13, textX, int(boxY)+40, color.RGBA{255, 215, 0, 255})
|
||||
|
||||
// Spectator Overlay wenn tot
|
||||
if isDead {
|
||||
// Halbtransparenter roter Overlay
|
||||
vector.DrawFilledRect(screen, 0, 0, ScreenWidth, 80, color.RGBA{150, 0, 0, 180}, false)
|
||||
text.Draw(screen, "☠ DU BIST TOT - SPECTATOR MODE ☠", basicfont.Face7x13, ScreenWidth/2-140, 30, color.White)
|
||||
text.Draw(screen, fmt.Sprintf("Dein Final Score: %d", myScore), basicfont.Face7x13, ScreenWidth/2-90, 55, color.RGBA{255, 255, 0, 255})
|
||||
// Halbtransparenter roter Overlay (volle Canvas-Breite)
|
||||
vector.DrawFilledRect(screen, 0, 0, float32(canvasW), 80, color.RGBA{150, 0, 0, 180}, false)
|
||||
text.Draw(screen, "☠ DU BIST TOT - SPECTATOR MODE ☠", basicfont.Face7x13, canvasW/2-140, 30, color.White)
|
||||
text.Draw(screen, fmt.Sprintf("Dein Final Score: %d", myScore), basicfont.Face7x13, canvasW/2-90, 55, color.RGBA{255, 255, 0, 255})
|
||||
}
|
||||
}
|
||||
|
||||
// 5. DEBUG: TODES-LINIE
|
||||
vector.StrokeLine(screen, 0, 0, 0, float32(ScreenHeight), 10, color.RGBA{255, 0, 0, 128}, false)
|
||||
text.Draw(screen, "! DEATH ZONE !", basicfont.Face7x13, 10, ScreenHeight/2, color.RGBA{255, 0, 0, 255})
|
||||
// 5. DEBUG: TODES-LINIE (volle Canvas-Höhe)
|
||||
vector.StrokeLine(screen, 0, 0, 0, float32(canvasH), 10, color.RGBA{255, 0, 0, 128}, false)
|
||||
text.Draw(screen, "! DEATH ZONE !", basicfont.Face7x13, 10, canvasH/2, color.RGBA{255, 0, 0, 255})
|
||||
|
||||
// 6. PARTIKEL RENDERN (vor UI)
|
||||
g.RenderParticles(screen)
|
||||
|
||||
// 7. TOUCH CONTROLS OVERLAY (nur wenn Tastatur nicht benutzt wurde)
|
||||
if !g.keyboardUsed {
|
||||
// A) Joystick Base (dunkelgrau und durchsichtig)
|
||||
baseCol := color.RGBA{80, 80, 80, 50} // Dunkelgrau und durchsichtig
|
||||
vector.DrawFilledCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), 60, baseCol, false)
|
||||
vector.StrokeCircle(screen, float32(g.joyBaseX), float32(g.joyBaseY), 60, 2, color.RGBA{100, 100, 100, 100}, false)
|
||||
// 7. DEBUG OVERLAY (F3 zum Umschalten)
|
||||
if g.showDebug {
|
||||
g.drawDebugOverlay(screen)
|
||||
}
|
||||
|
||||
// B) Joystick Knob (dunkelgrau, außer wenn aktiv)
|
||||
knobCol := color.RGBA{100, 100, 100, 80} // Dunkelgrau und durchsichtig
|
||||
// 8. TOUCH CONTROLS OVERLAY (nur wenn Tastatur nicht benutzt wurde)
|
||||
if !g.keyboardUsed {
|
||||
canvasW, canvasH := screen.Size()
|
||||
|
||||
// A) Joystick Base (unten links, relativ zu Canvas)
|
||||
joyX := 150.0
|
||||
joyY := float64(canvasH) - 150.0
|
||||
baseCol := color.RGBA{80, 80, 80, 50}
|
||||
vector.DrawFilledCircle(screen, float32(joyX), float32(joyY), 60, baseCol, false)
|
||||
vector.StrokeCircle(screen, float32(joyX), float32(joyY), 60, 2, color.RGBA{100, 100, 100, 100}, false)
|
||||
|
||||
// B) Joystick Knob (relativ zu Base, nicht zu Canvas)
|
||||
knobCol := color.RGBA{100, 100, 100, 80}
|
||||
if g.joyActive {
|
||||
knobCol = color.RGBA{100, 255, 100, 120} // Grün wenn aktiv, aber auch durchsichtig
|
||||
knobCol = color.RGBA{100, 255, 100, 120}
|
||||
}
|
||||
vector.DrawFilledCircle(screen, float32(g.joyStickX), float32(g.joyStickY), 30, knobCol, false)
|
||||
|
||||
// C) Jump Button (Rechts, ausgeblendet bei Tastatur-Nutzung)
|
||||
jumpX := float32(ScreenWidth - 150)
|
||||
jumpY := float32(ScreenHeight - 150)
|
||||
// C) Jump Button (unten rechts, relativ zu Canvas)
|
||||
jumpX := float32(canvasW) - 150
|
||||
jumpY := float32(canvasH) - 150
|
||||
vector.DrawFilledCircle(screen, jumpX, jumpY, 50, color.RGBA{255, 0, 0, 50}, false)
|
||||
vector.StrokeCircle(screen, jumpX, jumpY, 50, 2, color.RGBA{255, 0, 0, 100}, false)
|
||||
text.Draw(screen, "JUMP", basicfont.Face7x13, int(jumpX)-15, int(jumpY)+5, color.RGBA{255, 255, 255, 150})
|
||||
}
|
||||
|
||||
// 8. DEBUG INFO (Oben Links)
|
||||
myPosStr := "N/A"
|
||||
for _, p := range g.gameState.Players {
|
||||
myPosStr = fmt.Sprintf("X:%.0f Y:%.0f", p.X, p.Y)
|
||||
break
|
||||
}
|
||||
|
||||
debugMsg := fmt.Sprintf(
|
||||
"FPS: %.2f\nState: %s\nPlayers: %d\nCamX: %.0f\nPos: %s",
|
||||
ebiten.CurrentFPS(),
|
||||
g.gameState.Status,
|
||||
len(g.gameState.Players),
|
||||
g.camX,
|
||||
myPosStr,
|
||||
)
|
||||
|
||||
vector.DrawFilledRect(screen, 10, 10, 200, 90, color.RGBA{0, 0, 0, 180}, false)
|
||||
text.Draw(screen, debugMsg, basicfont.Face7x13, 20, 30, color.White)
|
||||
}
|
||||
|
||||
// --- ASSET HELPER ---
|
||||
@@ -476,8 +465,10 @@ func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY fl
|
||||
screenX := worldX - g.camX
|
||||
screenY := worldY
|
||||
|
||||
// Optimierung: Nicht zeichnen, wenn komplett außerhalb
|
||||
if screenX < -200 || screenX > ScreenWidth+200 {
|
||||
// Optimierung: Nicht zeichnen, wenn komplett außerhalb (Canvas-Breite verwenden)
|
||||
// Großzügiger Culling-Bereich für früheres Spawning (800px statt 200px)
|
||||
canvasW, _ := screen.Size()
|
||||
if screenX < -800 || screenX > float64(canvasW)+800 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -519,3 +510,90 @@ func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY fl
|
||||
}
|
||||
}
|
||||
|
||||
// drawDebugOverlay zeigt Performance und Network Stats (F3 zum Umschalten)
|
||||
func (g *Game) drawDebugOverlay(screen *ebiten.Image) {
|
||||
// Hintergrund (halbtransparent)
|
||||
vector.DrawFilledRect(screen, 10, 80, 350, 170, color.RGBA{0, 0, 0, 180}, false)
|
||||
vector.StrokeRect(screen, 10, 80, 350, 170, 2, color.RGBA{255, 255, 0, 255}, false)
|
||||
|
||||
y := 95
|
||||
lineHeight := 15
|
||||
|
||||
// Titel
|
||||
text.Draw(screen, "=== DEBUG INFO (F3) ===", basicfont.Face7x13, 20, y, color.RGBA{255, 255, 0, 255})
|
||||
y += lineHeight + 5
|
||||
|
||||
// FPS
|
||||
fpsColor := color.RGBA{0, 255, 0, 255}
|
||||
if g.currentFPS < 15 {
|
||||
fpsColor = color.RGBA{255, 0, 0, 255}
|
||||
} else if g.currentFPS < 30 {
|
||||
fpsColor = color.RGBA{255, 165, 0, 255}
|
||||
}
|
||||
text.Draw(screen, fmt.Sprintf("FPS: %.1f", g.currentFPS), basicfont.Face7x13, 20, y, fpsColor)
|
||||
y += lineHeight
|
||||
|
||||
// Server Update Latenz
|
||||
updateAge := time.Since(g.lastUpdateTime).Milliseconds()
|
||||
latencyColor := color.RGBA{0, 255, 0, 255}
|
||||
if updateAge > 200 {
|
||||
latencyColor = color.RGBA{255, 0, 0, 255}
|
||||
} else if updateAge > 100 {
|
||||
latencyColor = color.RGBA{255, 165, 0, 255}
|
||||
}
|
||||
text.Draw(screen, fmt.Sprintf("Update Age: %dms", updateAge), basicfont.Face7x13, 20, y, latencyColor)
|
||||
y += lineHeight
|
||||
|
||||
// Network Stats
|
||||
text.Draw(screen, fmt.Sprintf("Total Updates: %d", g.totalUpdates), basicfont.Face7x13, 20, y, color.White)
|
||||
y += lineHeight
|
||||
|
||||
oooColor := color.RGBA{0, 255, 0, 255}
|
||||
if g.outOfOrderCount > 10 {
|
||||
oooColor = color.RGBA{255, 165, 0, 255}
|
||||
}
|
||||
if g.outOfOrderCount > 50 {
|
||||
oooColor = color.RGBA{255, 0, 0, 255}
|
||||
}
|
||||
text.Draw(screen, fmt.Sprintf("Out-of-Order: %d", g.outOfOrderCount), basicfont.Face7x13, 20, y, oooColor)
|
||||
y += lineHeight
|
||||
|
||||
// Packet Loss Rate
|
||||
if g.totalUpdates > 0 {
|
||||
lossRate := float64(g.outOfOrderCount) / float64(g.totalUpdates+g.outOfOrderCount) * 100
|
||||
lossColor := color.RGBA{0, 255, 0, 255}
|
||||
if lossRate > 10 {
|
||||
lossColor = color.RGBA{255, 0, 0, 255}
|
||||
} else if lossRate > 5 {
|
||||
lossColor = color.RGBA{255, 165, 0, 255}
|
||||
}
|
||||
text.Draw(screen, fmt.Sprintf("Loss Rate: %.1f%%", lossRate), basicfont.Face7x13, 20, y, lossColor)
|
||||
y += lineHeight
|
||||
}
|
||||
|
||||
// Client Prediction Stats
|
||||
text.Draw(screen, fmt.Sprintf("Pending Inputs: %d", g.pendingInputCount), basicfont.Face7x13, 20, y, color.White)
|
||||
y += lineHeight
|
||||
|
||||
corrColor := color.RGBA{0, 255, 0, 255}
|
||||
if g.correctionCount > 100 {
|
||||
corrColor = color.RGBA{255, 165, 0, 255}
|
||||
}
|
||||
if g.correctionCount > 500 {
|
||||
corrColor = color.RGBA{255, 0, 0, 255}
|
||||
}
|
||||
text.Draw(screen, fmt.Sprintf("Corrections: %d", g.correctionCount), basicfont.Face7x13, 20, y, corrColor)
|
||||
y += lineHeight
|
||||
|
||||
// Current Correction Magnitude
|
||||
corrMag := math.Sqrt(g.correctionX*g.correctionX + g.correctionY*g.correctionY)
|
||||
if corrMag > 0.1 {
|
||||
text.Draw(screen, fmt.Sprintf("Corr Mag: %.1f", corrMag), basicfont.Face7x13, 20, y, color.RGBA{255, 165, 0, 255})
|
||||
} else {
|
||||
text.Draw(screen, "Corr Mag: 0.0", basicfont.Face7x13, 20, y, color.RGBA{0, 255, 0, 255})
|
||||
}
|
||||
y += lineHeight
|
||||
|
||||
// Server Sequence
|
||||
text.Draw(screen, fmt.Sprintf("Server Seq: %d", g.lastRecvSeq), basicfont.Face7x13, 20, y, color.White)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user