Private
Public Access
1
0

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

This commit is contained in:
Sebastian Unterschütz
2026-01-06 21:37:32 +01:00
parent 23d42d42e7
commit 023996229a
13 changed files with 685 additions and 251 deletions

View File

@@ -9,6 +9,7 @@ import (
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/config"
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/physics"
"github.com/nats-io/nats.go"
)
@@ -21,6 +22,8 @@ type ServerPlayer struct {
OnWall bool // Ist an einer Wand
OnMovingPlatform *MovingPlatform // Referenz zur Plattform auf der der Spieler steht
InputX float64 // -1 (Links), 0, 1 (Rechts)
InputJump bool // Sprung-Input (für Physik-Engine)
InputDown bool // Nach-Unten-Input (für Fast Fall)
LastInputSeq uint32 // Letzte verarbeitete Input-Sequenz
Score int
DistanceScore int // Score basierend auf zurückgelegter Distanz
@@ -122,6 +125,8 @@ func NewRoom(id string, nc *nats.Conn, w *game.World) *Room {
r.pDrawOffY = def.DrawOffY
r.pHitboxOffX = def.Hitbox.OffsetX
r.pHitboxOffY = def.Hitbox.OffsetY
log.Printf("🎮 Player Hitbox geladen: DrawOff=(%.1f, %.1f), HitboxOff=(%.1f, %.1f), Size=(%.1f, %.1f)",
r.pDrawOffX, r.pDrawOffY, r.pHitboxOffX, r.pHitboxOffY, r.pW, r.pH)
}
// Start-Chunk
@@ -163,7 +168,7 @@ func NewRoom(id string, nc *nats.Conn, w *game.World) *Room {
// --- MAIN LOOP ---
func (r *Room) RunLoop() {
// 60 Tick pro Sekunde
// 20 Tick pro Sekunde
ticker := time.NewTicker(time.Second / 20)
defer ticker.Stop()
@@ -172,6 +177,17 @@ func (r *Room) RunLoop() {
case <-r.stopChan:
return
case <-ticker.C:
r.Mutex.RLock()
status := r.Status
r.Mutex.RUnlock()
// Stoppe Updates wenn Spiel vorbei ist
if status == "GAMEOVER" {
r.Broadcast() // Ein letztes Mal broadcasten
time.Sleep(5 * time.Second) // Kurz warten damit Clients den GAMEOVER State sehen
return // Beende Loop
}
r.Update()
r.Broadcast()
}
@@ -196,7 +212,7 @@ func (r *Room) AddPlayer(id, name string) {
ID: id,
Name: name,
X: spawnX,
Y: 200,
Y: 400, // Spawn über dem Gras (Y=540), fällt dann auf den Boden
OnGround: false,
Score: 0,
IsAlive: true,
@@ -261,18 +277,15 @@ func (r *Room) HandleInput(input game.ClientInput) {
switch input.Type {
case "JUMP":
if p.OnGround {
p.VY = -config.JumpVelocity
p.OnGround = false
p.DoubleJumpUsed = false // Reset double jump on ground jump
} else if p.HasDoubleJump && !p.DoubleJumpUsed {
// Double Jump in der Luft
p.InputJump = true // Setze Jump-Flag für Physik-Engine
// Double Jump spezial-Logik (außerhalb der Physik-Engine)
if !p.OnGround && p.HasDoubleJump && !p.DoubleJumpUsed {
p.VY = -config.JumpVelocity
p.DoubleJumpUsed = true
log.Printf("⚡ %s verwendet Double Jump!", p.Name)
}
case "DOWN":
p.VY = config.FastFall
p.InputDown = true // Setze Down-Flag für Fast Fall
case "LEFT_DOWN":
p.InputX = -1
case "LEFT_UP":
@@ -355,44 +368,67 @@ func (r *Room) Update() {
continue
}
// X Bewegung
// Spieler bewegt sich relativ zum Scroll
// Scroll-Geschwindigkeit + Links/Rechts Bewegung
playerMovement := p.InputX * config.PlayerSpeed
currentSpeed := r.CurrentSpeed + playerMovement
nextX := p.X + currentSpeed
// === PHYSIK MIT GEMEINSAMER ENGINE (1:1 wie Client) ===
hitX, typeX := r.CheckCollision(nextX+r.pDrawOffX+r.pHitboxOffX, p.Y+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
if hitX {
if typeX == "wall" {
// Wand getroffen - kann klettern!
p.OnWall = true
// X-Position NICHT ändern (bleibt vor der Wand stehen)
} else if typeX == "obstacle" {
// Godmode prüfen
if p.HasGodMode && time.Now().Before(p.GodModeEndTime) {
// Mit Godmode - Obstacle wird zerstört, Spieler überlebt
p.X = nextX
// TODO: Obstacle aus colliders entfernen (benötigt Referenz zum Obstacle)
log.Printf("🛡️ %s zerstört Obstacle mit Godmode!", p.Name)
} else {
// Ohne Godmode - Spieler stirbt
p.X = nextX
r.KillPlayer(p)
continue
}
} else {
// Platform blockiert
p.OnWall = false
}
} else {
p.X = nextX
p.OnWall = false
// Physik-State vorbereiten
state := physics.PlayerPhysicsState{
X: p.X,
Y: p.Y,
VX: p.VX,
VY: p.VY,
OnGround: p.OnGround,
OnWall: p.OnWall,
}
// Physik-Input vorbereiten
physicsInput := physics.PhysicsInput{
InputX: p.InputX,
Jump: p.InputJump,
Down: p.InputDown,
}
// Kollisions-Checker vorbereiten
collisionChecker := &physics.ServerCollisionChecker{
CheckCollisionFunc: r.CheckCollision,
}
// Gemeinsame Physik anwenden (1:1 wie Client!)
physics.ApplyPhysics(&state, physicsInput, r.CurrentSpeed, collisionChecker, physics.DefaultPlayerConstants())
// Ergebnis zurückschreiben
p.X = state.X
p.Y = state.Y
p.VX = state.VX
p.VY = state.VY
p.OnGround = state.OnGround
p.OnWall = state.OnWall
// Input-Flags zurücksetzen für nächsten Tick
p.InputJump = false
p.InputDown = false
// Double Jump Reset wenn wieder am Boden
if p.OnGround {
p.DoubleJumpUsed = false
}
// === SERVER-SPEZIFISCHE LOGIK ===
// Obstacle-Kollision prüfen -> Spieler töten
hitObstacle, obstacleType := r.CheckCollision(
p.X+r.pDrawOffX+r.pHitboxOffX,
p.Y+r.pDrawOffY+r.pHitboxOffY,
r.pW,
r.pH,
)
if hitObstacle && obstacleType == "obstacle" {
r.KillPlayer(p)
continue
}
// Grenzen
if p.X > r.GlobalScrollX+1200 {
p.X = r.GlobalScrollX + 1200
if p.X > r.GlobalScrollX+2000 {
p.X = r.GlobalScrollX + 2000
} // Rechts Block
if p.X < r.GlobalScrollX-50 {
r.KillPlayer(p)
@@ -403,53 +439,11 @@ func (r *Room) Update() {
maxX = p.X
}
// Y Bewegung
// An der Wand: Reduzierte Gravität + Klettern mit InputX
if p.OnWall {
// Wandrutschen (langsame Fallgeschwindigkeit)
p.VY += config.Gravity * 0.3 // 30% Gravität an der Wand
if p.VY > config.WallSlideMax {
p.VY = config.WallSlideMax
}
// Hochklettern wenn nach oben gedrückt (InputX in Wandrichtung)
if p.InputX != 0 {
p.VY = -config.WallClimbSpeed
}
// Prüfe ob auf bewegender Plattform (für Platform-Mitbewegung)
if p.OnGround {
platform := r.CheckMovingPlatformLanding(p.X+r.pDrawOffX+r.pHitboxOffX, p.Y+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
p.OnMovingPlatform = platform
} else {
// Normal: Volle Gravität
p.VY += config.Gravity
if p.VY > config.MaxFall {
p.VY = config.MaxFall
}
}
nextY := p.Y + p.VY
hitY, typeY := r.CheckCollision(p.X+r.pDrawOffX+r.pHitboxOffX, nextY+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
if hitY {
if typeY == "wall" {
// An der Wand: Nicht töten, Position halten und klettern ermöglichen
p.VY = 0
p.OnWall = true
} else if typeY == "obstacle" {
// Obstacle - immer töten
p.Y = nextY
r.KillPlayer(p)
continue
} else {
// Platform blockiert
if p.VY > 0 {
p.OnGround = true
// Prüfe ob auf bewegender Plattform
platform := r.CheckMovingPlatformLanding(p.X+r.pDrawOffX+r.pHitboxOffX, nextY+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
p.OnMovingPlatform = platform
}
p.VY = 0
}
} else {
p.Y += p.VY
p.OnGround = false
p.OnMovingPlatform = nil
}
@@ -535,10 +529,6 @@ func (r *Room) CheckCollision(x, y, w, h float64) (bool, string) {
// 1. Statische Colliders (Chunks)
for _, c := range r.Colliders {
if game.CheckRectCollision(playerRect, c.Rect) {
log.Printf("🔴 COLLISION! Type=%s, Player: (%.1f, %.1f, %.1f x %.1f), Collider: (%.1f, %.1f, %.1f x %.1f)",
c.Type,
playerRect.OffsetX, playerRect.OffsetY, playerRect.W, playerRect.H,
c.Rect.OffsetX, c.Rect.OffsetY, c.Rect.W, c.Rect.H)
return true, c.Type
}
}
@@ -802,6 +792,7 @@ func (r *Room) Broadcast() {
CollectedPowerups: r.CollectedPowerups,
MovingPlatforms: make([]game.MovingPlatformSync, 0, len(r.MovingPlatforms)),
Sequence: r.sequence,
CurrentSpeed: r.CurrentSpeed,
}
for id, p := range r.Players {