fix game
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 8m20s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 8m20s
This commit is contained in:
@@ -104,7 +104,22 @@ func (g *Game) UpdateGame() {
|
|||||||
// Kamera hart setzen
|
// Kamera hart setzen
|
||||||
g.camX = targetCam
|
g.camX = targetCam
|
||||||
|
|
||||||
// --- 6. PARTIKEL UPDATEN ---
|
// --- 6. CORRECTION OFFSET ABKLINGEN ---
|
||||||
|
// Der visuelle Offset sorgt dafür dass Server-Korrekturen sanft und unsichtbar sind.
|
||||||
|
// Decay: 0.85 pro Frame → ~5 Frames zum Halbieren bei 60fps (≈80ms)
|
||||||
|
const correctionDecay = 0.85
|
||||||
|
g.predictionMutex.Lock()
|
||||||
|
g.correctionOffsetX *= correctionDecay
|
||||||
|
g.correctionOffsetY *= correctionDecay
|
||||||
|
if g.correctionOffsetX*g.correctionOffsetX < 0.09 {
|
||||||
|
g.correctionOffsetX = 0
|
||||||
|
}
|
||||||
|
if g.correctionOffsetY*g.correctionOffsetY < 0.09 {
|
||||||
|
g.correctionOffsetY = 0
|
||||||
|
}
|
||||||
|
g.predictionMutex.Unlock()
|
||||||
|
|
||||||
|
// --- 7. PARTIKEL UPDATEN ---
|
||||||
g.UpdateParticles(1.0 / 60.0) // Delta time: ~16ms
|
g.UpdateParticles(1.0 / 60.0) // Delta time: ~16ms
|
||||||
|
|
||||||
// --- 7. PARTIKEL SPAWNEN (State Changes Detection) ---
|
// --- 7. PARTIKEL SPAWNEN (State Changes Detection) ---
|
||||||
@@ -335,12 +350,12 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
vy := p.VY
|
vy := p.VY
|
||||||
onGround := p.OnGround
|
onGround := p.OnGround
|
||||||
|
|
||||||
// Für lokalen Spieler: Verwende Client-Prediction Position
|
// Für lokalen Spieler: Verwende Client-Prediction + visuellen Korrektur-Offset
|
||||||
// Die Reconciliation wird in ReconcileWithServer() (connection_*.go) gemacht
|
// correctionOffset sorgt dafür dass Server-Korrekturen sanft aussehen
|
||||||
if p.Name == g.playerName {
|
if p.Name == g.playerName {
|
||||||
g.predictionMutex.Lock()
|
g.predictionMutex.Lock()
|
||||||
posX = g.predictedX
|
posX = g.predictedX + g.correctionOffsetX
|
||||||
posY = g.predictedY
|
posY = g.predictedY + g.correctionOffsetY
|
||||||
g.predictionMutex.Unlock()
|
g.predictionMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,9 +98,14 @@ type Game struct {
|
|||||||
lastRecvSeq uint32 // Letzte empfangene Server-Sequenznummer (für Out-of-Order-Erkennung)
|
lastRecvSeq uint32 // Letzte empfangene Server-Sequenznummer (für Out-of-Order-Erkennung)
|
||||||
lastInputTime time.Time // Letzter Input-Send (für 20 TPS Throttling)
|
lastInputTime time.Time // Letzter Input-Send (für 20 TPS Throttling)
|
||||||
|
|
||||||
// Smooth Correction
|
// Smooth Correction (Debug-Info)
|
||||||
correctionX float64 // Verbleibende Korrektur in X
|
correctionX float64 // Letzte Korrektur-Magnitude X
|
||||||
correctionY float64 // Verbleibende Korrektur in Y
|
correctionY float64 // Letzte Korrektur-Magnitude Y
|
||||||
|
|
||||||
|
// Visueller Korrektur-Offset: Physics springt sofort, Display folgt sanft
|
||||||
|
// display_pos = predicted + correctionOffset (blendet harte Korrekturen aus)
|
||||||
|
correctionOffsetX float64
|
||||||
|
correctionOffsetY float64
|
||||||
|
|
||||||
// Particle System
|
// Particle System
|
||||||
particles []Particle
|
particles []Particle
|
||||||
@@ -866,50 +871,25 @@ func (g *Game) SendInputWithSequence(input InputState) {
|
|||||||
|
|
||||||
myID := g.getMyPlayerID()
|
myID := g.getMyPlayerID()
|
||||||
|
|
||||||
// Inputs als einzelne Commands senden
|
// Kompletten Input-State in einer einzigen Nachricht senden.
|
||||||
if input.Left {
|
// Vorteile gegenüber mehreren Event-Nachrichten:
|
||||||
|
// - Kein stuck-Input durch verlorene oder falsch sortierte Pakete
|
||||||
|
// - Server hat immer den vollständigen Zustand nach einem Paket
|
||||||
|
// - Weniger Nachrichten pro Frame
|
||||||
g.publishInput(game.ClientInput{
|
g.publishInput(game.ClientInput{
|
||||||
PlayerID: myID,
|
PlayerID: myID,
|
||||||
Type: "LEFT_DOWN",
|
Type: "STATE",
|
||||||
Sequence: input.Sequence,
|
Sequence: input.Sequence,
|
||||||
|
InputLeft: input.Left,
|
||||||
|
InputRight: input.Right,
|
||||||
|
InputJump: input.Jump,
|
||||||
|
InputDown: input.Down,
|
||||||
|
InputJoyX: input.JoyX,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
if input.Right {
|
|
||||||
g.publishInput(game.ClientInput{
|
|
||||||
PlayerID: myID,
|
|
||||||
Type: "RIGHT_DOWN",
|
|
||||||
Sequence: input.Sequence,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if input.Jump {
|
|
||||||
g.publishInput(game.ClientInput{
|
|
||||||
PlayerID: myID,
|
|
||||||
Type: "JUMP",
|
|
||||||
Sequence: input.Sequence,
|
|
||||||
})
|
|
||||||
// Jump Sound abspielen
|
|
||||||
g.audio.PlayJump()
|
|
||||||
}
|
|
||||||
if input.Down {
|
|
||||||
g.publishInput(game.ClientInput{
|
|
||||||
PlayerID: myID,
|
|
||||||
Type: "DOWN",
|
|
||||||
Sequence: input.Sequence,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn weder Links noch Rechts, sende STOP
|
// Jump-Sound lokal abspielen
|
||||||
if !input.Left && !input.Right {
|
if input.Jump {
|
||||||
g.publishInput(game.ClientInput{
|
g.audio.PlayJump()
|
||||||
PlayerID: myID,
|
|
||||||
Type: "LEFT_UP",
|
|
||||||
Sequence: input.Sequence,
|
|
||||||
})
|
|
||||||
g.publishInput(game.ClientInput{
|
|
||||||
PlayerID: myID,
|
|
||||||
Type: "RIGHT_UP",
|
|
||||||
Sequence: input.Sequence,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,37 +128,48 @@ func (g *Game) ReconcileWithServer(serverState game.PlayerState) {
|
|||||||
g.correctionX = diffX
|
g.correctionX = diffX
|
||||||
g.correctionY = diffY
|
g.correctionY = diffY
|
||||||
|
|
||||||
// === NEUE KORREKTUR-LOGIK MIT TOLERANZ ===
|
// === SMOOTH CORRECTION MIT VISUELLEM OFFSET ===
|
||||||
|
//
|
||||||
|
// Strategie: Physics springt sofort zur richtigen Position (korrekte Physik),
|
||||||
|
// aber der angezeigte Wert (predictedX/Y + correctionOffsetX/Y) springt NICHT –
|
||||||
|
// er driftet sanft über ~80ms zur Physics-Position hin.
|
||||||
|
//
|
||||||
|
// Dadurch sehen Spieler keinen "Pushback/Teleport" mehr, selbst wenn der Server
|
||||||
|
// eine größere Korrektur schickt.
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Toleranzen für Client-Abweichungen
|
acceptTolerance = 400.0 // ~20px: keine Korrektur
|
||||||
acceptTolerance = 400.0 // 20px - Client-Wert wird akzeptiert, keine Korrektur
|
criticalTolerance = 2500.0 // ~50px: sanfte Korrektur
|
||||||
warnTolerance = 2500.0 // 50px - Warnung, nur noch Monitoring
|
|
||||||
criticalTolerance = 10000.0 // 100px - kritisch (Teleport/Respawn/Desync)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if dist < acceptTolerance {
|
if dist < acceptTolerance {
|
||||||
// Bei kleinen Abweichungen (<20px): Client-Position akzeptieren, KEINE Korrektur
|
// Kleine Abweichung (<20px): Client-Position beibehalten, kein Eingriff nötig
|
||||||
// Server nimmt Client-Wert an
|
|
||||||
// (Client-seitige Prediction bleibt unverändert)
|
|
||||||
} else if dist < warnTolerance {
|
|
||||||
// Bei mittleren Abweichungen (20-50px): Nur Monitoring, KEINE Korrektur
|
|
||||||
// Der Unterschied wird toleriert - Client-Position wird akzeptiert
|
|
||||||
// Server überwacht weiterhin, greift aber nicht ein
|
|
||||||
} else if dist < criticalTolerance {
|
} else if dist < criticalTolerance {
|
||||||
// Bei großen Abweichungen (50-100px): Sanfte Korrektur
|
// Mittlere Abweichung (20-50px): sanfte Physics-Korrektur (25%)
|
||||||
interpFactor := 0.3 // Nur 30% Korrektur
|
// Visueller Offset kompensiert die Bewegung → kein sichtbarer Sprung
|
||||||
g.predictedX += diffX * interpFactor
|
factor := 0.25
|
||||||
g.predictedY += diffY * interpFactor
|
corrX := diffX * factor
|
||||||
|
corrY := diffY * factor
|
||||||
|
g.correctionOffsetX -= corrX // Offset gegenläufig zur Physics-Korrektur
|
||||||
|
g.correctionOffsetY -= corrY
|
||||||
|
g.predictedX += corrX
|
||||||
|
g.predictedY += corrY
|
||||||
g.correctionCount++
|
g.correctionCount++
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Bei kritischen Abweichungen (>100px): Sofort korrigieren (Teleport/Respawn/Desync)
|
// Große Abweichung (>50px): Physics springt komplett zur Replay-Position.
|
||||||
|
// Visueller Offset hält das Bild an der alten Stelle – Spieler sieht keinen Teleport.
|
||||||
|
g.correctionOffsetX += g.predictedX - replayX // = -(replayX - predictedX) = -diffX
|
||||||
|
g.correctionOffsetY += g.predictedY - replayY
|
||||||
g.predictedX = replayX
|
g.predictedX = replayX
|
||||||
g.predictedY = replayY
|
g.predictedY = replayY
|
||||||
g.correctionCount++
|
g.correctionCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Velocity und Ground Status vom Server übernehmen
|
// Velocity und Ground-State immer vom Replay übernehmen.
|
||||||
|
// Replay-Werte kommen aus korrekter Server-State + Pending-Input-Replay
|
||||||
|
// und sind physikalisch genauer als reine Client-Prediction über Zeit.
|
||||||
g.predictedVX = replayVX
|
g.predictedVX = replayVX
|
||||||
g.predictedVY = replayVY
|
g.predictedVY = replayVY
|
||||||
g.predictedGround = replayGround
|
g.predictedGround = replayGround
|
||||||
|
|||||||
@@ -66,11 +66,19 @@ type LoginPayload struct {
|
|||||||
|
|
||||||
// Input vom Spieler während des Spiels
|
// Input vom Spieler während des Spiels
|
||||||
type ClientInput struct {
|
type ClientInput struct {
|
||||||
Type string `json:"type"` // "JUMP", "START", "LEFT_DOWN", "RIGHT_DOWN", "SET_TEAM_NAME", etc.
|
Type string `json:"type"` // "STATE", "START", "SET_TEAM_NAME", etc.
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
PlayerID string `json:"player_id"`
|
PlayerID string `json:"player_id"`
|
||||||
Sequence uint32 `json:"sequence"` // Sequenznummer für Client Prediction
|
Sequence uint32 `json:"sequence"` // Sequenznummer für Client Prediction
|
||||||
TeamName string `json:"team_name,omitempty"` // Für SET_TEAM_NAME Input
|
TeamName string `json:"team_name,omitempty"` // Für SET_TEAM_NAME Input
|
||||||
|
|
||||||
|
// Vollständiger Input-State (für TYPE="STATE")
|
||||||
|
// Sendet den kompletten Zustand in einer Nachricht, verhindert stuck-Inputs durch Paketverlust
|
||||||
|
InputLeft bool `json:"input_left,omitempty"`
|
||||||
|
InputRight bool `json:"input_right,omitempty"`
|
||||||
|
InputJump bool `json:"input_jump,omitempty"`
|
||||||
|
InputDown bool `json:"input_down,omitempty"`
|
||||||
|
InputJoyX float64 `json:"input_joy_x,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JoinRequest struct {
|
type JoinRequest struct {
|
||||||
|
|||||||
@@ -146,17 +146,16 @@ func (gw *Gateway) HandleWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
continue // Ignoriere böswilligen Input
|
continue // Ignoriere böswilligen Input
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔒 SECURITY: Setze IMMER die korrekten IDs (überschreibe Client-Werte)
|
// 🔒 SECURITY: Alle Input-Felder übernehmen, aber IDs immer vom Server setzen
|
||||||
input := game.ClientInput{
|
// Remarshal des raw-Objekts in ClientInput um alle Felder (inkl. STATE-Felder) zu übernehmen
|
||||||
Type: inputType,
|
inputBytes, _ := json.Marshal(raw)
|
||||||
RoomID: roomID, // Server setzt den Raum (nicht Client!)
|
var input game.ClientInput
|
||||||
PlayerID: playerID, // Server setzt die Player-ID (nicht Client!)
|
json.Unmarshal(inputBytes, &input)
|
||||||
}
|
|
||||||
|
|
||||||
// Sequence-Nummer vom Client übernehmen (für Client Prediction)
|
// Security-kritische Felder vom Server überschreiben (nie Client-Werten vertrauen)
|
||||||
if seq, ok := raw["sequence"].(float64); ok {
|
input.Type = inputType
|
||||||
input.Sequence = uint32(seq)
|
input.RoomID = roomID // Server setzt den Raum
|
||||||
}
|
input.PlayerID = playerID // Server setzt die Player-ID
|
||||||
|
|
||||||
bytes, _ := json.Marshal(input)
|
bytes, _ := json.Marshal(input)
|
||||||
gw.NC.Publish(fmt.Sprintf("game.room.%s.input", roomID), bytes)
|
gw.NC.Publish(fmt.Sprintf("game.room.%s.input", roomID), bytes)
|
||||||
|
|||||||
@@ -276,16 +276,49 @@ func (r *Room) HandleInput(input game.ClientInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch input.Type {
|
switch input.Type {
|
||||||
case "JUMP":
|
case "STATE":
|
||||||
p.InputJump = true // Setze Jump-Flag für Physik-Engine
|
// Vollständigen Input-State atomisch setzen – verhindert stuck-Inputs durch
|
||||||
|
// Paketverlust oder Reihenfolge-Probleme bei Event-basierten Nachrichten.
|
||||||
|
// Out-of-Order-Schutz: nur neuere States übernehmen
|
||||||
|
if input.Sequence <= p.LastInputSeq {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Richtung: JoyX hat Vorrang vor digitalen Tasten
|
||||||
|
if input.InputJoyX != 0 {
|
||||||
|
p.InputX = input.InputJoyX
|
||||||
|
} else if input.InputLeft && !input.InputRight {
|
||||||
|
p.InputX = -1
|
||||||
|
} else if input.InputRight && !input.InputLeft {
|
||||||
|
p.InputX = 1
|
||||||
|
} else {
|
||||||
|
p.InputX = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump/Down: einmal setzen, nie löschen (Physics-Tick macht das)
|
||||||
|
if input.InputJump {
|
||||||
|
p.InputJump = true
|
||||||
// Double Jump spezial-Logik (außerhalb der Physik-Engine)
|
// Double Jump spezial-Logik (außerhalb der Physik-Engine)
|
||||||
if !p.OnGround && p.HasDoubleJump && !p.DoubleJumpUsed {
|
if !p.OnGround && p.HasDoubleJump && !p.DoubleJumpUsed {
|
||||||
p.VY = -config.JumpVelocity
|
p.VY = -config.JumpVelocity
|
||||||
p.DoubleJumpUsed = true
|
p.DoubleJumpUsed = true
|
||||||
log.Printf("⚡ %s verwendet Double Jump!", p.Name)
|
log.Printf("⚡ %s verwendet Double Jump!", p.Name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if input.InputDown {
|
||||||
|
p.InputDown = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy-Events (Rückwärtskompatibilität, werden vom neuen Client nicht mehr gesendet)
|
||||||
|
case "JUMP":
|
||||||
|
p.InputJump = true
|
||||||
|
if !p.OnGround && p.HasDoubleJump && !p.DoubleJumpUsed {
|
||||||
|
p.VY = -config.JumpVelocity
|
||||||
|
p.DoubleJumpUsed = true
|
||||||
|
log.Printf("⚡ %s verwendet Double Jump!", p.Name)
|
||||||
|
}
|
||||||
case "DOWN":
|
case "DOWN":
|
||||||
p.InputDown = true // Setze Down-Flag für Fast Fall
|
p.InputDown = true
|
||||||
case "LEFT_DOWN":
|
case "LEFT_DOWN":
|
||||||
p.InputX = -1
|
p.InputX = -1
|
||||||
case "LEFT_UP":
|
case "LEFT_UP":
|
||||||
@@ -298,6 +331,7 @@ func (r *Room) HandleInput(input game.ClientInput) {
|
|||||||
if p.InputX == 1 {
|
if p.InputX == 1 {
|
||||||
p.InputX = 0
|
p.InputX = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
case "START":
|
case "START":
|
||||||
if input.PlayerID == r.HostID && r.Status == "LOBBY" {
|
if input.PlayerID == r.HostID && r.Status == "LOBBY" {
|
||||||
r.StartCountdown()
|
r.StartCountdown()
|
||||||
|
|||||||
Reference in New Issue
Block a user