Private
Public Access
1
0

Implement core game functionalities: client prediction, coin collection, scoring, game state synchronization, and player management.

This commit is contained in:
Sebastian Unterschütz
2026-01-01 16:46:39 +01:00
parent 4b2995846e
commit 5e6b8a2304
9 changed files with 902 additions and 203 deletions

View File

@@ -3,6 +3,7 @@ package main
import (
"fmt"
"image/color"
"log"
"math"
"github.com/hajimehoshi/ebiten/v2"
@@ -24,53 +25,47 @@ func (g *Game) UpdateGame() {
// --- 2. TOUCH INPUT HANDLING ---
g.handleTouchInput()
// --- 3. INPUTS ZUSAMMENFÜHREN & SENDEN ---
if g.connected {
// A) BEWEGUNG (Links/Rechts)
// Joystick auswerten (-1 bis 1)
joyDir := 0.0
if g.joyActive {
diffX := g.joyStickX - g.joyBaseX
if diffX < -20 {
joyDir = -1
} // Nach Links gezogen
if diffX > 20 {
joyDir = 1
} // Nach Rechts gezogen
// --- 3. INPUT STATE ERSTELLEN ---
joyDir := 0.0
if g.joyActive {
diffX := g.joyStickX - g.joyBaseX
if diffX < -20 {
joyDir = -1
}
// Senden: Keyboard ODER Joystick
if keyLeft || joyDir == -1 {
g.SendCommand("LEFT_DOWN")
} else if keyRight || joyDir == 1 {
g.SendCommand("RIGHT_DOWN")
} else {
// Wenn weder Links noch Rechts gedrückt ist, senden wir STOP.
g.SendCommand("LEFT_UP")
g.SendCommand("RIGHT_UP")
}
// B) NACH UNTEN (Fast Fall)
// Joystick weit nach unten gezogen?
isJoyDown := false
if g.joyActive && (g.joyStickY-g.joyBaseY) > 40 {
isJoyDown = true
}
if keyDown || isJoyDown {
g.SendCommand("DOWN")
}
// C) SPRINGEN
// Keyboard ODER Touch-Button
if keyJump || g.btnJumpActive {
g.SendCommand("JUMP")
g.btnJumpActive = false // Reset (Tap to jump)
if diffX > 20 {
joyDir = 1
}
}
// --- 4. KAMERA LOGIK ---
isJoyDown := g.joyActive && (g.joyStickY-g.joyBaseY) > 40
// Input State zusammenbauen
input := InputState{
Sequence: g.inputSequence,
Left: keyLeft || joyDir == -1,
Right: keyRight || joyDir == 1,
Jump: keyJump || g.btnJumpActive,
Down: keyDown || isJoyDown,
}
g.btnJumpActive = false
// --- 4. CLIENT PREDICTION ---
if g.connected {
// 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)
g.ApplyInput(input)
// Input an Server senden
g.SendInputWithSequence(input)
}
// --- 5. KAMERA LOGIK ---
g.stateMutex.Lock()
defer g.stateMutex.Unlock()
@@ -176,9 +171,15 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
for _, activeChunk := range g.gameState.WorldChunks {
chunkDef, exists := g.world.ChunkLibrary[activeChunk.ChunkID]
if !exists {
log.Printf("⚠️ Chunk '%s' nicht in Library gefunden!", activeChunk.ChunkID)
continue
}
// DEBUG: Chunk-Details loggen (nur einmal)
if len(chunkDef.Objects) == 0 {
log.Printf("⚠️ Chunk '%s' hat 0 Objekte! Width=%d", activeChunk.ChunkID, chunkDef.Width)
}
for _, obj := range chunkDef.Objects {
// Asset zeichnen
g.DrawAsset(screen, obj.AssetID, activeChunk.X+obj.X, obj.Y)
@@ -186,20 +187,36 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
}
// 3. Spieler
// MyID ohne Lock holen (wir haben bereits den stateMutex)
myID := ""
for id, p := range g.gameState.Players {
g.DrawAsset(screen, "player", p.X, p.Y)
if p.Name == g.playerName {
myID = id
break
}
}
for id, p := range g.gameState.Players {
// Für lokalen Spieler: Verwende vorhergesagte Position
posX, posY := p.X, p.Y
if id == myID && g.connected {
posX = g.predictedX
posY = g.predictedY
}
g.DrawAsset(screen, "player", posX, posY)
// Name Tag
name := p.Name
if name == "" {
name = id
}
text.Draw(screen, name, basicfont.Face7x13, int(p.X-g.camX), int(p.Y-25), ColText)
text.Draw(screen, name, basicfont.Face7x13, int(posX-g.camX), int(posY-25), ColText)
// DEBUG: Rote Hitbox
if def, ok := g.world.Manifest.Assets["player"]; ok {
hx := float32(p.X + def.DrawOffX + def.Hitbox.OffsetX - g.camX)
hy := float32(p.Y + def.DrawOffY + def.Hitbox.OffsetY)
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)
}
}
@@ -211,6 +228,25 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
} else if g.gameState.Status == "RUNNING" {
dist := fmt.Sprintf("Distance: %.0f m", g.camX/64.0)
text.Draw(screen, dist, basicfont.Face7x13, ScreenWidth-150, 30, ColText)
// Score anzeigen
for _, p := range g.gameState.Players {
if p.Name == g.playerName {
scoreStr := fmt.Sprintf("Score: %d", p.Score)
text.Draw(screen, scoreStr, basicfont.Face7x13, ScreenWidth-150, 50, ColText)
break
}
}
} else if g.gameState.Status == "GAMEOVER" {
// Game Over Screen mit allen Scores
text.Draw(screen, "GAME OVER", basicfont.Face7x13, ScreenWidth/2-50, 100, color.RGBA{255, 0, 0, 255})
y := 150
for _, p := range g.gameState.Players {
scoreMsg := fmt.Sprintf("%s: %d pts", p.Name, p.Score)
text.Draw(screen, scoreMsg, basicfont.Face7x13, ScreenWidth/2-80, y, color.White)
y += 20
}
}
// 5. DEBUG: TODES-LINIE
@@ -282,6 +318,9 @@ func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY fl
if img != nil {
op := &ebiten.DrawImageOptions{}
// Filter für bessere Skalierung (besonders bei großen Sprites)
op.Filter = ebiten.FilterLinear
// Skalieren
op.GeoM.Scale(def.Scale, def.Scale)
@@ -291,8 +330,11 @@ func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY fl
screenY+def.DrawOffY,
)
// Farbe anwenden
op.ColorScale.ScaleWithColor(def.Color.ToRGBA())
// Farbe anwenden (nur wenn explizit gesetzt)
// Wenn Color leer ist (R=G=B=A=0), nicht anwenden (Bild bleibt original)
if def.Color.R != 0 || def.Color.G != 0 || def.Color.B != 0 || def.Color.A != 0 {
op.ColorScale.ScaleWithColor(def.Color.ToRGBA())
}
screen.DrawImage(img, op)
} else {