add music, better sync, particles
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
This commit is contained in:
308
websocket.go
Normal file
308
websocket.go
Normal file
@@ -0,0 +1,308 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user