Private
Public Access
1
0
Files
it232Abschied/websocket.go
Sebastian Unterschütz 669c783a06
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
add music, better sync, particles
2025-11-29 23:37:57 +01:00

309 lines
7.7 KiB
Go

package main
import (
"log"
"math"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
ServerTickRate = 50 * time.Millisecond
BufferAhead = 60 // Puffergröße (Zukunft)
SpawnXStart = 2000.0 // Spawn Abstand
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// Protokoll
type WSInputMsg struct {
Type string `json:"type"`
Input string `json:"input"`
Tick int `json:"tick"` // Optional: Client Timestamp für Ping
PosY float64 `json:"y"`
VelY float64 `json:"vy"`
}
type WSServerMsg struct {
Type string `json:"type"`
Obstacles []ActiveObstacle `json:"obstacles"`
Platforms []ActivePlatform `json:"platforms"`
ServerTick int `json:"serverTick"`
Score int `json:"score"`
PowerUps PowerUpState `json:"powerups"`
SessionID string `json:"sessionId"`
Ts int `json:"ts,omitempty"` // Für Pong
CurrentSpeed float64 `json:"currentSpeed"`
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
// 1. Session Init
sessionID := "ws-" + time.Now().Format("150405999")
err = rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"score": 0, // Startwert
"is_dead": 0,
"created_at": time.Now().Format("02.01.2006 15:04"),
}).Err()
if err != nil {
log.Println("Redis Init Fehler:", err)
return
}
rdb.Expire(ctx, "session:"+sessionID, 24*time.Hour)
// Session ID senden
conn.WriteJSON(WSServerMsg{Type: "init", SessionID: sessionID})
state := SimState{
SessionID: sessionID,
RNG: NewRNG(time.Now().UnixNano()),
Score: 0,
Ticks: 0,
PosY: PlayerYBase,
NextSpawnTick: 0,
}
// Channel größer machen, damit bei Lag nichts blockiert
inputChan := make(chan WSInputMsg, 100)
closeChan := make(chan struct{})
// LESE-ROUTINE
go func() {
defer close(closeChan)
for {
var msg WSInputMsg
if err := conn.ReadJSON(&msg); err != nil {
return
}
inputChan <- msg
}
}()
// GAME LOOP (High Performance)
ticker := time.NewTicker(ServerTickRate)
defer ticker.Stop()
// Input State
var pendingJump bool
var isCrouching bool
generatedHeadTick := 0
for {
select {
case <-closeChan:
return // Client weg
// WICHTIG: Wir verarbeiten Inputs hier NICHT einzeln,
// sondern sammeln sie im Default-Case (siehe unten),
// oder nutzen eine nicht-blockierende Schleife.
// Aber für einfache Logik reicht select.
// Um "Input Lag" zu verhindern, lesen wir den Channel leer:
case <-ticker.C:
// A. INPUTS VERARBEITEN (Alle die angekommen sind!)
// Wir loopen solange durch den Channel, bis er leer ist.
InputLoop:
for {
select {
case msg := <-inputChan:
if msg.Type == "input" {
if msg.Input == "JUMP" {
pendingJump = true
}
if msg.Input == "DUCK_START" {
isCrouching = true
}
if msg.Input == "DUCK_END" {
isCrouching = false
}
if msg.Input == "DEATH" {
state.IsDead = true
}
}
if msg.Type == "ping" {
// Sofort Pong zurück (Performance wichtig!)
conn.WriteJSON(WSServerMsg{Type: "pong", Ts: msg.Tick})
}
if msg.Type == "sync" {
diff := math.Abs(state.PosY - msg.PosY)
if diff < 100.0 {
state.PosY = msg.PosY
state.VelY = msg.VelY
}
}
if msg.Type == "debug" {
spd := calculateSpeed(state.Ticks)
conn.WriteJSON(WSServerMsg{
Type: "debug_sync",
ServerTick: state.Ticks,
Obstacles: state.Obstacles,
Platforms: state.Platforms,
Score: state.Score,
CurrentSpeed: spd,
})
log.Printf("🐞 Debug Snapshot an Client gesendet (Tick %d)", state.Ticks)
}
default:
// Channel leer, weiter zur Physik
break InputLoop
}
}
// B. LIVE SIMULATION (1 Tick)
// Jetzt simulieren wir genau EINEN Frame (16ms)
state.Ticks++
state.Score++ // Score wächst mit der Zeit
currentSpeed := calculateSpeed(state.Ticks)
updatePhysics(&state, pendingJump, isCrouching, currentSpeed)
pendingJump = false // Jump Trigger reset
checkCollisions(&state, isCrouching, currentSpeed)
if state.IsDead {
// Score Persistieren
rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
"score": state.Score,
"is_dead": 1,
})
rdb.Expire(ctx, "session:"+sessionID, 24*time.Hour)
conn.WriteJSON(WSServerMsg{Type: "dead", Score: state.Score})
return
}
moveWorld(&state, currentSpeed)
// C. STREAMING (Zukunft)
// Wir generieren nur, wenn der Puffer zur Neige geht
targetTick := state.Ticks + BufferAhead
var newObs []ActiveObstacle
var newPlats []ActivePlatform
// Um CPU zu sparen, generieren wir max 10 Ticks pro Frame nach
loops := 0
for generatedHeadTick < targetTick && loops < 10 {
generatedHeadTick++
loops++
o, p := generateFutureObjects(&state, generatedHeadTick, currentSpeed)
if len(o) > 0 {
newObs = append(newObs, o...)
state.Obstacles = append(state.Obstacles, o...)
}
if len(p) > 0 {
newPlats = append(newPlats, p...)
state.Platforms = append(state.Platforms, p...)
}
}
// D. SENDEN (Effizienz)
// Nur senden wenn Daten da sind ODER alle 15 Frames (Heartbeat/Score Sync)
if len(newObs) > 0 || len(newPlats) > 0 || state.Ticks%15 == 0 {
msg := WSServerMsg{
Type: "chunk",
ServerTick: state.Ticks,
Score: state.Score,
Obstacles: newObs,
Platforms: newPlats,
PowerUps: PowerUpState{
GodLives: state.GodLives, HasBat: state.HasBat, BootTicks: state.BootTicks,
},
}
conn.WriteJSON(msg)
}
}
}
}
// ... (generateFutureObjects bleibt gleich wie vorher) ...
func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstacle, []ActivePlatform) {
var createdObs []ActiveObstacle
var createdPlats []ActivePlatform
if s.NextSpawnTick == 0 {
s.NextSpawnTick = tick + 50
}
if tick >= s.NextSpawnTick {
spawnX := SpawnXStart
chunkCount := len(defaultConfig.Chunks)
if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
idx := int(s.RNG.NextRange(0, float64(chunkCount)))
chunk := defaultConfig.Chunks[idx]
for _, p := range chunk.Platforms {
createdPlats = append(createdPlats, ActivePlatform{X: spawnX + p.X, Y: p.Y, Width: p.Width, Height: p.Height})
}
for _, o := range chunk.Obstacles {
createdObs = append(createdObs, ActiveObstacle{ID: o.ID, Type: o.Type, X: spawnX + o.X, Y: o.Y, Width: o.Width, Height: o.Height})
}
width := chunk.TotalWidth
if width == 0 {
width = 2000
}
s.NextSpawnTick = tick + int(float64(width)/speed)
} else {
// Random Logic
gap := 400 + int(s.RNG.NextRange(0, 500))
s.NextSpawnTick = tick + int(float64(gap)/speed)
defs := defaultConfig.Obstacles
if len(defs) > 0 {
// Boss Check
isBoss := (tick % 1500) > 1200
var pool []ObstacleDef
for _, d := range defs {
if isBoss {
if d.ID == "principal" || d.ID == "trashcan" {
pool = append(pool, d)
}
} else {
if d.ID != "principal" {
pool = append(pool, d)
}
}
}
def := s.RNG.PickDef(pool)
if def != nil {
// RNG Calls to keep sync (optional now, but good practice)
if def.CanTalk && s.RNG.NextFloat() > 0.7 {
}
if def.Type == "powerup" && s.RNG.NextFloat() > 0.1 {
def = nil
}
if def != nil {
createdObs = append(createdObs, ActiveObstacle{
ID: def.ID, Type: def.Type, X: spawnX, Y: GroundY - def.Height - def.YOffset, Width: def.Width, Height: def.Height,
})
}
}
}
}
}
return createdObs, createdPlats
}