Add Docker, Kubernetes configurations, and CI workflows for deployment. Integrate Gin server for API, WebSocket support, and static file hosting. Refactor WebSocket gateway to use Gin router.
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Binaries
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
server
|
||||
client
|
||||
|
||||
# Test files
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Development
|
||||
.git/
|
||||
.github/
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Temporary
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Dependencies (werden neu gebaut)
|
||||
vendor/
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Cleanup Environment
|
||||
on: [delete]
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
# Nur ausführen, wenn ein Branch gelöscht wurde (keine Tags)
|
||||
if: github.event.ref_type == 'branch'
|
||||
|
||||
steps:
|
||||
# 1. Variablen berechnen (MIT FIX FÜR REFS/HEADS & MAIN-CHECK)
|
||||
- name: Prepare Variables
|
||||
run: |
|
||||
# Repo Name klein (z.B. "it232abschied")
|
||||
REPO_LOWER=$(echo "${{ gitea.repository }}" | cut -d'/' -f2 | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Branch Name aus Event (z.B. "refs/heads/feature-x")
|
||||
RAW_REF="${{ github.event.ref }}"
|
||||
# "refs/heads/" entfernen
|
||||
BRANCH_CLEAN=${RAW_REF#refs/heads/}
|
||||
# Kleinschreiben & Sonderzeichen
|
||||
BRANCH_LOWER=$(echo "$BRANCH_CLEAN" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
|
||||
|
||||
# Logik synchron zum Deploy:
|
||||
# Main/Master -> Namespace ist nur der Repo-Name
|
||||
# Anderes -> Namespace ist Repo-Branch
|
||||
if [ "$BRANCH_LOWER" = "main" ] || [ "$BRANCH_LOWER" = "master" ]; then
|
||||
TARGET_NS="${REPO_LOWER}"
|
||||
IS_MAIN="true"
|
||||
else
|
||||
TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}"
|
||||
IS_MAIN="false"
|
||||
fi
|
||||
|
||||
echo "DEBUG: Clean Branch: $BRANCH_LOWER"
|
||||
echo "DEBUG: Target NS: $TARGET_NS"
|
||||
|
||||
echo "TARGET_NS=$TARGET_NS" >> $GITHUB_ENV
|
||||
echo "IS_MAIN=$IS_MAIN" >> $GITHUB_ENV
|
||||
|
||||
# 2. Sicherheits-Check: Niemals Main/Master löschen!
|
||||
# Wir prüfen jetzt die Variable IS_MAIN, statt den Namen hart zu codieren
|
||||
- name: Protect Main
|
||||
if: env.IS_MAIN == 'true'
|
||||
run: |
|
||||
echo "❌ ABBRUCH: Der Produktions-Namespace ${{ env.TARGET_NS }} darf nicht gelöscht werden!"
|
||||
exit 1
|
||||
|
||||
# 3. Kubectl einrichten
|
||||
- name: Setup Kubectl
|
||||
run: |
|
||||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl
|
||||
mv kubectl /usr/local/bin/
|
||||
|
||||
mkdir -p $HOME/.kube
|
||||
echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
|
||||
chmod 600 $HOME/.kube/config
|
||||
|
||||
# Der Trick für interne Kommunikation
|
||||
sed -i 's|server: https://.*:6443|server: https://kubernetes.default.svc:443|g' $HOME/.kube/config
|
||||
|
||||
# 4. Namespace löschen
|
||||
- name: Delete Namespace
|
||||
run: |
|
||||
echo "🗑️ Lösche Namespace: ${{ env.TARGET_NS }}"
|
||||
# Wir löschen den Namespace ohne zu warten (async), das geht schneller
|
||||
kubectl delete namespace ${{ env.TARGET_NS }} --ignore-not-found --wait=false
|
||||
@@ -0,0 +1,128 @@
|
||||
name: Dynamic Branch Deploy
|
||||
on: [push]
|
||||
|
||||
env:
|
||||
REGISTRY: git.zb-server.de
|
||||
# WICHTIG: Deine echte Haupt-Domain
|
||||
BASE_DOMAIN: escape-from-school.de
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 1. Code auschecken
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# 2. Variablen vorbereiten (MIT HAUPT-DOMAIN LOGIK)
|
||||
- name: Prepare Environment Variables
|
||||
id: prep
|
||||
run: |
|
||||
# 1. Repo und Branch Namen säubern
|
||||
# Voller Pfad für Docker Image (z.B. "user/escape-teacher")
|
||||
FULL_IMAGE_PATH=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Nur der Projektname für K8s (z.B. "escape-teacher")
|
||||
REPO_LOWER=$(echo "$FULL_IMAGE_PATH" | cut -d'/' -f2)
|
||||
|
||||
# Branch Name säubern (Sonderzeichen zu Bindestrichen)
|
||||
BRANCH_LOWER=$(echo "${{ gitea.ref_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
|
||||
|
||||
# 2. Logik: Ist es der Haupt-Branch?
|
||||
if [ "$BRANCH_LOWER" = "main" ] || [ "$BRANCH_LOWER" = "master" ]; then
|
||||
# PRODUKTION:
|
||||
# URL ist direkt die Domain (ohne Subdomain)
|
||||
APP_URL="${{ env.BASE_DOMAIN }}"
|
||||
# Namespace ist nur der Projektname (ohne Branch-Suffix)
|
||||
TARGET_NS="${REPO_LOWER}"
|
||||
echo "Mode: PRODUCTION (Root Domain)"
|
||||
else
|
||||
# ENTWICKLUNG:
|
||||
# URL ist repo-branch.domain.de
|
||||
APP_URL="${REPO_LOWER}-${BRANCH_LOWER}.${{ env.BASE_DOMAIN }}"
|
||||
# Namespace ist repo-branch
|
||||
TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}"
|
||||
echo "Mode: DEVELOPMENT (Subdomain)"
|
||||
fi
|
||||
|
||||
# Image Tag (Commit Hash)
|
||||
IMAGE_TAG="${{ gitea.sha }}"
|
||||
|
||||
# Debug Ausgabe
|
||||
echo "DEBUG: Branch: $BRANCH_LOWER"
|
||||
echo "DEBUG: Namespace: $TARGET_NS"
|
||||
echo "DEBUG: URL: $APP_URL"
|
||||
|
||||
# In Gitea Actions Environment schreiben
|
||||
echo "FULL_IMAGE_PATH=$FULL_IMAGE_PATH" >> $GITHUB_ENV
|
||||
echo "REPO_NAME=$REPO_LOWER" >> $GITHUB_ENV
|
||||
echo "TARGET_NS=$TARGET_NS" >> $GITHUB_ENV
|
||||
echo "APP_URL=$APP_URL" >> $GITHUB_ENV
|
||||
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||
|
||||
# 3. Kaniko Build
|
||||
- name: Build and Push with Kaniko
|
||||
uses: aevea/action-kaniko@v0.12.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||
image: ${{ env.FULL_IMAGE_PATH }}
|
||||
tag: ${{ env.IMAGE_TAG }}
|
||||
cache: true
|
||||
extra_args: --skip-tls-verify-pull --insecure
|
||||
|
||||
# 4. Setup Kubectl (Interner Trick)
|
||||
- name: Setup Kubectl
|
||||
run: |
|
||||
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl
|
||||
mv kubectl /usr/local/bin/
|
||||
|
||||
mkdir -p $HOME/.kube
|
||||
echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
|
||||
chmod 600 $HOME/.kube/config
|
||||
|
||||
# Internal DNS Trick (für Kommunikation innerhalb des Clusters)
|
||||
sed -i 's|server: https://.*:6443|server: https://kubernetes.default.svc:443|g' $HOME/.kube/config
|
||||
|
||||
# 5. Deploy to Kubernetes
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
# Namespace erstellen (falls nicht existiert)
|
||||
kubectl create namespace ${{ env.TARGET_NS }} --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Vollen Image Pfad bauen
|
||||
FULL_IMAGE_URL="${{ env.REGISTRY }}/${{ env.FULL_IMAGE_PATH }}:${{ env.IMAGE_TAG }}"
|
||||
|
||||
# 1. Ingress anpassen (Hier wird die URL eingesetzt!)
|
||||
sed -i "s|\${APP_URL}|${{ env.APP_URL }}|g" k8s/ingress.yaml
|
||||
|
||||
# 2. App Deployment anpassen (Image)
|
||||
sed -i "s|\${IMAGE_NAME}|$FULL_IMAGE_URL|g" k8s/app.yaml
|
||||
|
||||
# Anwenden
|
||||
echo "Deploying Resources to Namespace: ${{ env.TARGET_NS }}"
|
||||
kubectl apply -f k8s/pvc.yaml -n ${{ env.TARGET_NS }}
|
||||
kubectl apply -f k8s/redis.yaml -n ${{ env.TARGET_NS }}
|
||||
kubectl apply -f k8s/app.yaml -n ${{ env.TARGET_NS }}
|
||||
kubectl apply -f k8s/ingress.yaml -n ${{ env.TARGET_NS }}
|
||||
|
||||
# HPA (Autoscaling) nur für Main/Master Branch aktivieren
|
||||
# Wir vergleichen den Namespace mit dem Repo-Namen
|
||||
# Wenn Namespace == RepoName, dann sind wir im Main Branch
|
||||
if [ "${{ env.TARGET_NS }}" == "${{ env.REPO_NAME }}" ]; then
|
||||
echo "Main Branch detected: Applying HPA (Autoscaling)..."
|
||||
kubectl apply -f k8s/hpa.yaml -n ${{ env.TARGET_NS }}
|
||||
else
|
||||
echo "Feature Branch: Skipping HPA."
|
||||
# Optional: HPA löschen, falls es versehentlich da ist
|
||||
kubectl delete hpa escape-game-hpa -n ${{ env.TARGET_NS }} --ignore-not-found
|
||||
fi
|
||||
|
||||
# Force Update (damit das neue Image sicher geladen wird)
|
||||
kubectl rollout restart deployment/escape-game -n ${{ env.TARGET_NS }}
|
||||
|
||||
# 6. Summary
|
||||
- name: Summary
|
||||
run: echo "🚀 Deployed successfully to https://${{ env.APP_URL }}"
|
||||
@@ -51,14 +51,6 @@ Thumbs.db
|
||||
.env
|
||||
config.local.json
|
||||
|
||||
# Generated assets (if you want to regenerate them)
|
||||
# Uncomment if assets should be generated, not committed
|
||||
# /cmd/client/web/assets/*.png
|
||||
# /cmd/client/web/assets/assets.json
|
||||
|
||||
# Keep chunks but ignore generated ones if needed
|
||||
# /cmd/client/web/assets/chunks/*.json
|
||||
|
||||
# Debug files
|
||||
debug
|
||||
__debug_bin
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# Stage 1: Builder
|
||||
FROM golang:1.25.5-alpine AS builder
|
||||
|
||||
# Build-Dependencies
|
||||
RUN apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Dependencies cachen
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Source Code kopieren
|
||||
COPY . .
|
||||
|
||||
# Server binary bauen
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o server ./cmd/server
|
||||
|
||||
# WASM Client bauen
|
||||
RUN GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client
|
||||
|
||||
# Stage 2: Production Image
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates curl tzdata && \
|
||||
addgroup -g 1000 appuser && \
|
||||
adduser -D -u 1000 -G appuser appuser
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Binary und Web-Dateien vom Builder kopieren
|
||||
COPY --from=builder /app/server .
|
||||
COPY --from=builder /app/cmd/client/web ./cmd/client/web
|
||||
|
||||
# User wechseln
|
||||
USER appuser
|
||||
|
||||
# Port für HTTP/WebSocket
|
||||
EXPOSE 8080
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
CMD ["./server"]
|
||||
@@ -0,0 +1,296 @@
|
||||
# 🏃 Escape From Teacher
|
||||
|
||||
Ein Endless-Runner-Spiel entwickelt als Schulprojekt. Renne vor dem Lehrer davon, sammle Münzen, nutze Power-Ups und klettere an Wänden!
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 📋 Inhaltsverzeichnis
|
||||
|
||||
- [Features](#-features)
|
||||
- [Technologie-Stack](#-technologie-stack)
|
||||
- [Installation](#-installation)
|
||||
- [Spielmodi](#-spielmodi)
|
||||
- [Entwicklung](#-entwicklung)
|
||||
- [Architektur](#-architektur)
|
||||
- [Tools](#-tools)
|
||||
- [Lizenz](#-lizenz)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Gameplay
|
||||
- 🏃 **Endless Runner**: Unendliches Spiel mit prozedural generierten Levels
|
||||
- 🧗 **Wall Climbing**: Klettere an Wänden hoch für neue Strategien
|
||||
- 💰 **Coins & Power-Ups**: Sammle Münzen und nutze Power-Ups wie Double Jump und Godmode
|
||||
- 🎯 **Hindernisse**: Weiche bewegenden Plattformen und Objekten aus
|
||||
- ⚡ **Dynamische Schwierigkeit**: Das Spiel wird mit der Zeit schneller
|
||||
|
||||
### Modi
|
||||
- 👤 **Solo-Modus**: Spiele alleine und sammle Highscores
|
||||
- 👥 **Coop-Modus**: Spiele mit Freunden im Team
|
||||
- Team-Namen für Leaderboard
|
||||
- Multiplizierter Score basierend auf lebenden Spielern
|
||||
- Host-Controls für Lobby-Management
|
||||
|
||||
### Features
|
||||
- 🏆 **Leaderboard**: Redis-basiertes globales Leaderboard mit Proof-Codes
|
||||
- 🎮 **Cross-Platform**: Web (WASM) und Desktop (Native)
|
||||
- 🎨 **Level Editor**: Erstelle eigene Levels mit dem integrierten Level Builder
|
||||
- 🔊 **Audio**: Musik und Sound-Effekte mit Lautstärkeregelung
|
||||
- 📱 **Mobile Support**: Touch-Controls für mobile Geräte
|
||||
|
||||
## 🛠 Technologie-Stack
|
||||
|
||||
### Backend
|
||||
- **Go 1.25+**: Server und Game-Logik
|
||||
- **NATS**: Message Broker für Echtzeit-Kommunikation (Legacy)
|
||||
- **WebSocket**: Direkte WebSocket-Verbindungen für niedrige Latenz
|
||||
- **Redis**: Persistentes Leaderboard mit Proof-Code-Validierung
|
||||
|
||||
### Frontend
|
||||
- **Go + Ebiten**: Desktop-Client (Native)
|
||||
- **Go + WASM**: Web-Client kompiliert zu WebAssembly
|
||||
- **HTML/CSS/JavaScript**: Web-UI und Menüs
|
||||
|
||||
### Architektur
|
||||
- **Client-Side Prediction**: Flüssiges Gameplay trotz Netzwerk-Latenz
|
||||
- **Server-Authoritative**: Server validiert alle Spielaktionen
|
||||
- **Deterministische Physik**: Gleiche Physik auf Client und Server
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
```bash
|
||||
# Go 1.25 oder höher
|
||||
go version
|
||||
|
||||
# Redis für Leaderboard
|
||||
redis-server --version
|
||||
```
|
||||
|
||||
### Repository klonen
|
||||
|
||||
```bash
|
||||
git clone https://git.zb-server.de/ZB-Server/EscapeFromTeacher.git
|
||||
cd EscapeFromTeacher
|
||||
```
|
||||
|
||||
### Assets generieren
|
||||
|
||||
Die Assets müssen aus den Raw-Dateien generiert werden:
|
||||
|
||||
```bash
|
||||
# Asset-Builder starten (GUI)
|
||||
go run ./cmd/builder
|
||||
|
||||
# Im GUI: Assets laden, anpassen und speichern
|
||||
# Dies erstellt cmd/client/web/assets/assets.json und die PNG-Dateien
|
||||
```
|
||||
|
||||
### Server starten
|
||||
|
||||
```bash
|
||||
# Redis starten (Terminal 1)
|
||||
redis-server
|
||||
|
||||
# Server starten (Terminal 2)
|
||||
go run ./cmd/server
|
||||
```
|
||||
|
||||
Server läuft auf `http://localhost:8080`
|
||||
|
||||
### Clients starten
|
||||
|
||||
#### Web-Client (WASM)
|
||||
|
||||
```bash
|
||||
# WASM kompilieren
|
||||
GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client
|
||||
|
||||
# Web-Server starten (z.B. mit Python)
|
||||
cd cmd/client/web
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Browser öffnen: http://localhost:8000
|
||||
```
|
||||
|
||||
#### Desktop-Client (Native)
|
||||
|
||||
```bash
|
||||
go run ./cmd/client
|
||||
```
|
||||
|
||||
## 🎮 Spielmodi
|
||||
|
||||
### Solo-Modus
|
||||
- Starte direkt aus dem Hauptmenü
|
||||
- Automatischer Room-Erstellen
|
||||
- Score basiert auf zurückgelegter Distanz
|
||||
- Eigener Highscore im Leaderboard
|
||||
|
||||
### Coop-Modus
|
||||
1. **Raum erstellen** (Host):
|
||||
- "RAUM ERSTELLEN" klicken
|
||||
- Team-Namen festlegen (optional)
|
||||
- Raum-Code mit Freunden teilen
|
||||
|
||||
2. **Raum beitreten**:
|
||||
- Raum-Code eingeben
|
||||
- "RAUM BEITRETEN" klicken
|
||||
|
||||
3. **Gameplay**:
|
||||
- Host startet das Spiel wenn alle bereit sind
|
||||
- Team-Score = Distanz × Anzahl lebender Spieler
|
||||
- Beispiel: 100 Tiles mit 3 Spielern = 300 Punkte pro Spieler
|
||||
|
||||
### Steuerung
|
||||
|
||||
**Desktop:**
|
||||
- `Leertaste` / `W` / `↑`: Springen
|
||||
- `A` / `←`: Links bewegen
|
||||
- `D` / `→`: Rechts bewegen
|
||||
- `S` / `↓`: Schneller fallen
|
||||
|
||||
**Mobile:**
|
||||
- Touch-Buttons auf dem Bildschirm
|
||||
|
||||
**Wall Climbing:**
|
||||
- Laufe gegen eine Wand
|
||||
- Halte Richtungstaste gedrückt zum Hochklettern
|
||||
- Lasse los zum Herunterrutschen
|
||||
|
||||
## 🔧 Entwicklung
|
||||
|
||||
### Projekt-Struktur
|
||||
|
||||
```
|
||||
EscapeFromTeacher/
|
||||
├── cmd/
|
||||
│ ├── server/ # Game Server
|
||||
│ ├── client/ # Desktop & WASM Client
|
||||
│ │ └── web/ # Web Assets & HTML
|
||||
│ ├── builder/ # Asset Builder Tool
|
||||
│ └── levelbuilder/ # Level Editor
|
||||
├── pkg/
|
||||
│ ├── game/ # Gemeinsame Game-Logik
|
||||
│ ├── server/ # Server-spezifische Logik
|
||||
│ └── config/ # Konfiguration
|
||||
└── assets_raw/ # Rohe Asset-Dateien
|
||||
```
|
||||
|
||||
### Neue Features entwickeln
|
||||
|
||||
1. **Server-Logik** in `pkg/server/`
|
||||
2. **Game-Logik** (gemeinsam) in `pkg/game/`
|
||||
3. **Client-Rendering** in `cmd/client/`
|
||||
4. **Web-UI** in `cmd/client/web/`
|
||||
|
||||
### Build-Befehle
|
||||
|
||||
```bash
|
||||
# Server
|
||||
go build -o server ./cmd/server
|
||||
|
||||
# Desktop-Client
|
||||
go build -o client ./cmd/client
|
||||
|
||||
# WASM-Client
|
||||
GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client
|
||||
|
||||
# Level Builder
|
||||
go build -o levelbuilder ./cmd/levelbuilder
|
||||
```
|
||||
|
||||
## 🧰 Tools
|
||||
|
||||
### Level Builder
|
||||
|
||||
Erstelle eigene Levels mit dem visuellen Editor:
|
||||
|
||||
```bash
|
||||
go run ./cmd/levelbuilder
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Drag & Drop von Assets
|
||||
- Bewegende Plattformen konfigurieren
|
||||
- Chunk-basiertes Level-Design
|
||||
- JSON-Export in `cmd/client/web/assets/chunks/`
|
||||
|
||||
### Asset Builder
|
||||
|
||||
Konvertiere Raw-Assets zu optimierten Spiel-Assets:
|
||||
|
||||
```bash
|
||||
go run ./cmd/builder
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- PNG-Kompression und -Skalierung
|
||||
- Hitbox-Editor
|
||||
- Asset-Metadaten (Typ, Offsets, etc.)
|
||||
- Generiert `assets.json`
|
||||
|
||||
## 📐 Architektur
|
||||
|
||||
### Client-Server Kommunikation
|
||||
|
||||
```
|
||||
Client Server
|
||||
| |
|
||||
|------ WebSocket ------->|
|
||||
| (Input Events) |
|
||||
| |
|
||||
|<----- Broadcast --------|
|
||||
| (Game State) |
|
||||
| |
|
||||
|-- Score Submission ---->|
|
||||
| |
|
||||
|<--- Proof Code ---------|
|
||||
```
|
||||
|
||||
### Game Loop
|
||||
|
||||
**Server (60 FPS):**
|
||||
1. Verarbeite Inputs von allen Clients
|
||||
2. Update Physik (Gravity, Kollision, etc.)
|
||||
3. Update Map (Chunk spawning/despawning)
|
||||
4. Broadcast Game State an alle Clients
|
||||
|
||||
**Client:**
|
||||
1. Sende lokale Inputs an Server
|
||||
2. Empfange Game State vom Server
|
||||
3. Render Game State
|
||||
4. (Optional) Client-Side Prediction für lokalen Spieler
|
||||
|
||||
### Score-System
|
||||
|
||||
**Solo:**
|
||||
- Score = Zurückgelegte Distanz (in Tiles)
|
||||
- Coins: +200 Punkte
|
||||
|
||||
**Coop:**
|
||||
- Distanz-Score akkumuliert pro Tick
|
||||
- Pro Tick: `Score += Anzahl lebender Spieler`
|
||||
- Beispiel: 3 Spieler → +3 Punkte/Tick (180 Punkte/Sekunde)
|
||||
- Coins werden geteilt: Alle Spieler bekommen +200
|
||||
|
||||
## 📄 Lizenz
|
||||
|
||||
Dies ist ein **Schulprojekt**. Kommerzielle Nutzung und Veränderung des Quellcodes sind ausdrücklich untersagt. Alle Rechte liegen bei den Urhebern.
|
||||
|
||||
**Projektleitung & Code:** Sebastian Unterschütz
|
||||
**Musik & Sound Design:** Max E.
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- **Ebiten**: Go 2D Game Engine
|
||||
- **NATS**: Message Broker
|
||||
- **Redis**: In-Memory Database
|
||||
- **Go WASM**: WebAssembly Support
|
||||
|
||||
---
|
||||
|
||||
**Entwickelt als Abschlussprojekt für IT232 (2025/2026)**
|
||||
@@ -0,0 +1,88 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
// SetupGinServer erstellt und konfiguriert den Gin-Server
|
||||
func SetupGinServer(ec *nats.EncodedConn, port string) *gin.Engine {
|
||||
// Production mode für bessere Performance
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
// Logging Middleware (bereits in gin.Default())
|
||||
// Custom Logger für bessere Übersicht
|
||||
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return "🌐 " + param.TimeStamp.Format("2006-01-02 15:04:05") +
|
||||
" | " + param.Method +
|
||||
" | " + string(rune(param.StatusCode)) +
|
||||
" | " + param.Latency.String() +
|
||||
" | " + param.ClientIP +
|
||||
" | " + param.Path + "\n"
|
||||
}))
|
||||
|
||||
// Recovery Middleware
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
// Health Check Endpoint
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"service": "EscapeFromTeacher",
|
||||
"version": "1.0.0",
|
||||
})
|
||||
})
|
||||
|
||||
// Metrics Endpoint (optional für Kubernetes)
|
||||
r.GET("/metrics", func(c *gin.Context) {
|
||||
mu.RLock()
|
||||
roomCount := len(rooms)
|
||||
playerCount := len(playerSessions)
|
||||
mu.RUnlock()
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"rooms": roomCount,
|
||||
"players": playerCount,
|
||||
})
|
||||
})
|
||||
|
||||
// WebSocket Endpoint
|
||||
r.GET("/ws", func(c *gin.Context) {
|
||||
handleWebSocketGin(c.Writer, c.Request, ec)
|
||||
})
|
||||
|
||||
// Static Files - Serve Web Client
|
||||
r.Static("/assets", "./cmd/client/web/assets")
|
||||
r.StaticFile("/", "./cmd/client/web/index.html")
|
||||
r.StaticFile("/index.html", "./cmd/client/web/index.html")
|
||||
r.StaticFile("/game.js", "./cmd/client/web/game.js")
|
||||
r.StaticFile("/style.css", "./cmd/client/web/style.css")
|
||||
r.StaticFile("/wasm_exec.js", "./cmd/client/web/wasm_exec.js")
|
||||
r.StaticFile("/main.wasm", "./cmd/client/web/main.wasm")
|
||||
r.StaticFile("/background.jpg", "./cmd/client/web/background.jpg")
|
||||
|
||||
// 404 Handler
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "Route not found",
|
||||
})
|
||||
})
|
||||
|
||||
log.Printf("🚀 Gin-Server konfiguriert auf Port %s", port)
|
||||
log.Printf("📁 Statische Dateien: ./cmd/client/web/")
|
||||
log.Printf("🌐 WebSocket Endpoint: /ws")
|
||||
log.Printf("❤️ Health Check: /health")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// handleWebSocketGin verwaltet WebSocket-Verbindungen über Gin
|
||||
func handleWebSocketGin(w http.ResponseWriter, r *http.Request, ec *nats.EncodedConn) {
|
||||
// Verwende die handleWebSocket Funktion aus websocket_gateway.go
|
||||
handleWebSocket(w, r, ec)
|
||||
}
|
||||
+5
-5
@@ -172,11 +172,11 @@ func main() {
|
||||
|
||||
log.Println("✅ Server bereit. Warte auf Spieler...")
|
||||
|
||||
// 9. WEBSOCKET-GATEWAY STARTEN (für Browser-Clients)
|
||||
go StartWebSocketGateway("8080", ec)
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
// 9. GIN-SERVER STARTEN (statische Dateien + WebSocket)
|
||||
router := SetupGinServer(ec, "8080")
|
||||
if err := router.Run(":8080"); err != nil {
|
||||
log.Fatal("❌ Gin-Server Fehler:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadServerAssets(w *game.World) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: escape-game
|
||||
labels:
|
||||
app: escape-game
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: escape-game
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: escape-game
|
||||
spec:
|
||||
containers:
|
||||
- name: server
|
||||
image: ${IMAGE_NAME}
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "1000m"
|
||||
volumeMounts:
|
||||
- name: assets
|
||||
mountPath: /root/cmd/client/web/assets
|
||||
volumes:
|
||||
- name: assets
|
||||
persistentVolumeClaim:
|
||||
claimName: game-assets-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: escape-game
|
||||
labels:
|
||||
app: escape-game
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: escape-game
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
clientIP:
|
||||
timeoutSeconds: 3600
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: escape-game-hpa
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: escape-game
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: game-ingress
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web, websecure
|
||||
traefik.ingress.kubernetes.io/router.middlewares: gitea-redirect-https@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- ${APP_URL}
|
||||
secretName: game-tls-secret
|
||||
rules:
|
||||
- host: ${APP_URL}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: escape-game
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: game-assets-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
@@ -0,0 +1,75 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: redis-data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
ports:
|
||||
- port: 6379
|
||||
selector:
|
||||
app: redis
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
initContainers:
|
||||
- name: fix-permissions
|
||||
image: busybox
|
||||
command: ["sh", "-c", "chown -R 999:999 /data"]
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m" # 0.05 CPU Cores
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "1000m" # 0.5 CPU Cores
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["redis-cli", "ping"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: redis-data
|
||||
Reference in New Issue
Block a user