fix game
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
132
cmd/client/atlas.go
Normal file
132
cmd/client/atlas.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 2px Padding verhindert Linear-Filter-Bleeding an Sprite-Grenzen
|
||||||
|
atlasPadding = 2
|
||||||
|
// Maximale Atlas-Größe; 2048 deckt alle aktuellen Sprites locker ab
|
||||||
|
atlasMaxSize = 2048
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildAtlas packt alle Sprite-Bilder in eine einzige GPU-Textur (Texture Atlas).
|
||||||
|
// Ebiten erkennt zusammenhängende DrawImage-Calls auf derselben Quell-Textur
|
||||||
|
// und fasst sie in einen einzigen GPU-Draw-Call zusammen ("internal batching").
|
||||||
|
//
|
||||||
|
// Bilder vom Typ "background" und Bilder die größer als atlasMaxSize/2 sind
|
||||||
|
// bleiben als eigenständige Texturen und werden direkt in das Ergebnis übernommen.
|
||||||
|
//
|
||||||
|
// Alle anderen Sprites werden per Shelf-Packing-Algorithmus in den Atlas gepackt.
|
||||||
|
// Der Rückgabewert ist eine neue Map, in der jeder Eintrag entweder ein
|
||||||
|
// atlas.SubImage()-Ausschnitt oder das originale Einzelbild ist.
|
||||||
|
func buildAtlas(images map[string]*ebiten.Image, manifest game.AssetManifest) map[string]*ebiten.Image {
|
||||||
|
result := make(map[string]*ebiten.Image, len(images))
|
||||||
|
|
||||||
|
type spriteEntry struct {
|
||||||
|
id string
|
||||||
|
w int
|
||||||
|
h int
|
||||||
|
}
|
||||||
|
|
||||||
|
var topack []spriteEntry
|
||||||
|
standalone := make(map[string]*ebiten.Image)
|
||||||
|
|
||||||
|
for id, img := range images {
|
||||||
|
if img == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
def, ok := manifest.Assets[id]
|
||||||
|
if ok && def.Type == "background" {
|
||||||
|
standalone[id] = img
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w, h := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
|
if w > atlasMaxSize/2 || h > atlasMaxSize/2 {
|
||||||
|
standalone[id] = img
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
topack = append(topack, spriteEntry{id: id, w: w, h: h})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(topack) == 0 {
|
||||||
|
for id, img := range standalone {
|
||||||
|
result[id] = img
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shelf-Packing: nach Höhe absteigend sortieren → dichtere Packung
|
||||||
|
sort.Slice(topack, func(i, j int) bool {
|
||||||
|
return topack[i].h > topack[j].h
|
||||||
|
})
|
||||||
|
|
||||||
|
type placement struct {
|
||||||
|
id string
|
||||||
|
rect image.Rectangle
|
||||||
|
}
|
||||||
|
var placements []placement
|
||||||
|
|
||||||
|
curX := atlasPadding
|
||||||
|
curY := atlasPadding
|
||||||
|
shelfH := 0
|
||||||
|
atlasW := 0
|
||||||
|
atlasH := 0
|
||||||
|
|
||||||
|
for _, s := range topack {
|
||||||
|
pw := s.w + atlasPadding
|
||||||
|
ph := s.h + atlasPadding
|
||||||
|
|
||||||
|
// Neue Zeile (Shelf) wenn kein Platz mehr in aktueller Zeile
|
||||||
|
if curX+pw > atlasMaxSize {
|
||||||
|
curX = atlasPadding
|
||||||
|
curY += shelfH + atlasPadding
|
||||||
|
shelfH = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passt nicht mehr in den Atlas → als Standalone behalten
|
||||||
|
if curY+ph > atlasMaxSize {
|
||||||
|
standalone[s.id] = images[s.id]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r := image.Rect(curX, curY, curX+s.w, curY+s.h)
|
||||||
|
placements = append(placements, placement{id: s.id, rect: r})
|
||||||
|
|
||||||
|
if s.h > shelfH {
|
||||||
|
shelfH = s.h
|
||||||
|
}
|
||||||
|
if curX+pw > atlasW {
|
||||||
|
atlasW = curX + pw
|
||||||
|
}
|
||||||
|
if curY+s.h+atlasPadding > atlasH {
|
||||||
|
atlasH = curY + s.h + atlasPadding
|
||||||
|
}
|
||||||
|
curX += pw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atlas-Textur anlegen und Sprites hineinzeichnen
|
||||||
|
atlas := ebiten.NewImage(atlasW, atlasH)
|
||||||
|
for _, p := range placements {
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(float64(p.rect.Min.X), float64(p.rect.Min.Y))
|
||||||
|
atlas.DrawImage(images[p.id], op)
|
||||||
|
result[p.id] = atlas.SubImage(p.rect).(*ebiten.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, img := range standalone {
|
||||||
|
result[id] = img
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("🗺️ Texture Atlas: %dx%d px | %d Sprites gepackt | %d standalone",
|
||||||
|
atlasW, atlasH, len(placements), len(standalone))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -371,6 +371,19 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
effectiveCamX := g.camX / viewScale
|
effectiveCamX := g.camX / viewScale
|
||||||
g.RenderGround(screen, effectiveCamX)
|
g.RenderGround(screen, effectiveCamX)
|
||||||
|
|
||||||
|
// Prediction-Snapshot VOR dem stateMutex holen (verhindert Deadlock:
|
||||||
|
// DrawGame würde sonst stateMutex→predictionMutex halten, während
|
||||||
|
// ReconcileWithServer predictionMutex→stateMutex hält)
|
||||||
|
g.predictionMutex.Lock()
|
||||||
|
snapPrevX := g.prevPredictedX
|
||||||
|
snapPrevY := g.prevPredictedY
|
||||||
|
snapPredX := g.predictedX
|
||||||
|
snapPredY := g.predictedY
|
||||||
|
snapOffX := g.correctionOffsetX
|
||||||
|
snapOffY := g.correctionOffsetY
|
||||||
|
snapPhysTime := g.lastPhysicsTime
|
||||||
|
g.predictionMutex.Unlock()
|
||||||
|
|
||||||
// State Locken für Datenzugriff
|
// State Locken für Datenzugriff
|
||||||
g.stateMutex.Lock()
|
g.stateMutex.Lock()
|
||||||
defer g.stateMutex.Unlock()
|
defer g.stateMutex.Unlock()
|
||||||
@@ -434,15 +447,13 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
// Für lokalen Spieler: Client-Prediction mit Interpolation zwischen Physics-Steps
|
// Für lokalen Spieler: Client-Prediction mit Interpolation zwischen Physics-Steps
|
||||||
// Physics läuft bei 20/sec, Draw bei 60fps → alpha interpoliert dazwischen
|
// Physics läuft bei 20/sec, Draw bei 60fps → alpha interpoliert dazwischen
|
||||||
if p.Name == g.playerName {
|
if p.Name == g.playerName {
|
||||||
g.predictionMutex.Lock()
|
|
||||||
// Interpolations-Alpha: wie weit sind wir zwischen letztem und nächstem Physics-Step?
|
// Interpolations-Alpha: wie weit sind wir zwischen letztem und nächstem Physics-Step?
|
||||||
alpha := float64(time.Since(g.lastPhysicsTime)) / float64(50*time.Millisecond)
|
alpha := float64(time.Since(snapPhysTime)) / float64(50*time.Millisecond)
|
||||||
if alpha > 1 {
|
if alpha > 1 {
|
||||||
alpha = 1
|
alpha = 1
|
||||||
}
|
}
|
||||||
posX = g.prevPredictedX + (g.predictedX-g.prevPredictedX)*alpha + g.correctionOffsetX
|
posX = snapPrevX + (snapPredX-snapPrevX)*alpha + snapOffX
|
||||||
posY = g.prevPredictedY + (g.predictedY-g.prevPredictedY)*alpha + g.correctionOffsetY
|
posY = snapPrevY + (snapPredY-snapPrevY)*alpha + snapOffY
|
||||||
g.predictionMutex.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wähle Sprite basierend auf Sprung-Status
|
// Wähle Sprite basierend auf Sprung-Status
|
||||||
|
|||||||
Reference in New Issue
Block a user