This is a proof of concept for a UDP game server
  • Go 80.2%
  • Shell 15.8%
  • JavaScript 2.5%
  • HTML 1.5%
Find a file
Jesper Saastamoinen 4dcae3735b chore(docs): add k6 task
Signed-off-by: Jesper Saastamoinen <jespersaastamoinen@gmail.com>
2026-10-14 22:08:40 +02:00
.forgejo/workflows chore(ci): update benchmark workflow with job summary 2026-09-19 21:38:34 +02:00
benchmarks/results feat: fix bottlenecks (#21) 2026-10-08 22:19:06 +02:00
client feat: add spatial hashing for bullet collision (#20) 2026-10-04 21:08:38 +02:00
config feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00
database feat: fix bottlenecks (#21) 2026-10-08 22:19:06 +02:00
docs feat: make the queue generic (#25) 2026-10-12 20:38:51 +02:00
k6 chore(docs): add k6 task 2026-10-14 22:08:40 +02:00
login-client feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00
login-web feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00
loginserver feat: make the queue generic (#25) 2026-10-12 20:38:51 +02:00
network feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00
scripts feat: add queue to loginserver (#23) 2026-10-12 12:09:12 +02:00
server feat: fix bottlenecks (#21) 2026-10-08 22:19:06 +02:00
tasks/k6 chore(docs): add k6 task 2026-10-14 22:08:40 +02:00
.gitignore feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00
build-client.sh fix: update build scripts for build dir 2026-09-19 01:01:21 +02:00
build-windows-client.sh fix: update build scripts for build dir 2026-09-19 01:01:21 +02:00
game-server feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00
go.mod feat: fix bottlenecks (#21) 2026-10-08 22:19:06 +02:00
go.sum feat: fix bottlenecks (#21) 2026-10-08 22:19:06 +02:00
go.work feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00
go.work.sum feat: clean up code (#16) 2026-09-27 15:05:41 +02:00
login-server-bin feat: add queue to loginserver (#23) 2026-10-12 12:09:12 +02:00
start.sh feature/login-architecture (#18) 2026-10-04 16:30:19 +02:00

Game Server Architecture

Overview

This is a multiplayer online game system consisting of three main components:

Component Directory Description
Game Server game/ Main game logic, physics, and world management
Login Server loginserver/ User authentication and session management
Game Client client/ Player-facing application (Ebitengine GUI + headless mode)
Login Web UI login-web/ Web-based login interface

The project uses a Go workspace (go.work) with separate modules for each component, enabling independent builds and dependency management.

Project Structure

game/
├── go.work                     # Go workspace definition
├── go.mod                      # Main module dependencies
├── .env                        # Environment configuration
│
├── server/                     # Game server
│   ├── main.go                 # Entry point, config loading
│   ├── server.go               # Server struct, UDP listener, DB init
│   ├── run.go                  # Main event loop with tickers
│   ├── game.go                 # 60Hz game loop (tick, update, state)
│   ├── player.go               # Player entity, input handling, movement
│   ├── bullet.go               # Bullet entity, physics, lifetime
│   ├── world.go                # World view/culling for players
│   ├── cleanup.go              # Stale player cleanup
│   ├── debug.go                # Debug utilities
│   └── network.go              # UDP receive/send, packet dispatch
│
├── config/
│   └── config.go               # Configuration loading with build-time overrides
│
├── database/
│   ├── database.go             # SQLite connection, initialization, WAL mode
│   ├── player_repo.go          # Player persistence
│   └── ip_address_repo.go      # IP address tracking
│
├── network/
│   ├── packets.go              # Packet structs (Input, Player, Bullet, State)
│   ├── packets_test.go         # Packet serialization tests
│   ├── constants.go            # World dimensions, protocol constants
│   └── protocol/               # Wire protocol implementation
│       ├── constants.go        # Protocol version, packet types
│       ├── header.go           # Packet header encode/decode
│       ├── auth.go             # Auth packet handling
│       ├── input.go            # Input packet serialization
│       ├── shoot.go            # Shoot packet handling
│       └── state.go            # Game state serialization
│
├── loginserver/                # Login server module
│   ├── main.go                 # Entry point, HTTP server setup
│   ├── api/server.go           # REST API endpoints
│   ├── auth/                   # Authentication logic
│   └── database/               # SQLite repositories
│
├── client/                     # Game client module
│   └── ...                     # Client implementation
│
├── build/                      # Pre-built client binaries
├── data/                       # Game server database (game.db)
├── docs/                       # Documentation (this file)
└── scripts/                    # Build and utility scripts

Game Server

The game server is the core of the system, handling real-time game logic, player state, and bullet physics.

Entry Point & Configuration

server/main.go → config.Load(cwd) → NewServer(conf) → server.Start()

The server resolves its configuration relative to the current working directory at startup, enabling portable execution from any location. Configuration sources (in priority order):

  1. Environment variables (e.g., DATABASE_PATH, SERVER_ADDRESS, SERVER_PORT)
  2. .env file in the working directory
  3. Build-time defaults via -ldflags -X
// Build-time overrides (overridable at compile time)
var ServerAddress = "127.0.0.1"
var ServerPort = "9000"
var MaxConnections = 10000
var DatabasePath = "data/game.db"
var LoginServerAddress = "127.0.0.1"
var LoginServerPort = "8081"

Server Lifecycle

  1. Startup (server.Start()):

    • Resolves UDP address and binds listener on config.ServerPort
    • Opens SQLite database at config.DatabasePath with WAL mode
    • Enters main event loop via run()
  2. Main Loop (run()):

    • Continuously receives UDP packets via receiveInput()
    • Runs three periodic tasks via select/ticker pattern:
      • Session cleanup (every 1 second) — removes stale players
      • Logging (every 5 seconds) — logs player count and stats
      • Auth validation (every 5 minutes) — validates all player sessions against login server
  3. Game Loop (gameLoop() — runs in background goroutine at 60Hz):

    • Updates all player positions based on received input
    • Updates bullet trajectories and lifetimes
    • Sends world state to all connected players

Game State Model

Entity Key Fields Management
Player ID, position (x, y), health, session ID Created on first packet, cleaned up if stale
Bullet ID, position, velocity, owner, lifetime Spawned on shoot, removed on expiry or collision

Players are stored in a thread-safe map protected by sync.RWMutex. The server tracks nextPlayerID and nextBulletID for unique identifiers.

World Culling

The server uses a WorldView to only send each player the portion of the game world they can see. This reduces network bandwidth by:

  • Storing player positions with 0.1 precision (multiplied by 10, stored as uint16 in wire format)
  • Filtering bullets and players outside the viewer's range
  • Only including visible entities in state packets

Database

The game server uses SQLite via modernc.org/sqlite (pure Go implementation, no CGO required):

// WAL mode, busy timeout, foreign keys
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
PRAGMA foreign_keys=ON;

The database persists:

  • Players — registration data, last known position, timestamps
  • IP addresses — connection tracking for rate limiting / abuse prevention

Session Validation

Every 5 minutes, the server validates active player sessions by calling the login server's HTTP API:

func (s *Server) validateSessionHTTP(sessionID string) (bool, error)
func (s *Server) validateAllSessions()

Players with invalid or expired sessions are disconnected. This provides security against session hijacking and ensures only authenticated players remain in the game.

Login Server

The login server handles user authentication, registration, and session management.

Architecture

main.go → API Server (REST) → Auth Module → SQLite Repositories

REST API Endpoints

Endpoint Method Description
/api/login POST Authenticate user, returns session token
/api/register POST Register new user account
/api/validate POST Validate session token
/api/logout POST Invalidate session token
/health GET Health check endpoint

Session Management

  • Sessions are stored in SQLite (loginserver/data/login.db)
  • Automatic cleanup runs every 5 minutes to remove expired sessions
  • CORS headers allow requests from localhost and 127.0.0.1

Configuration

The login server follows the same pattern as the game server: environment variable overrides with .env file support and build-time defaults.

Client

The game client supports two modes:

GUI Mode

Built with Ebitengine (formerly Ebiten), providing a 2D game rendering surface. Handles:

  • Rendering game state (players, bullets, world)
  • Mouse input for aiming/shooting
  • Network packet serialization and deserialization
  • Account management (login, register)

Headless Mode

A CLI-based mode for automated testing, bots, or server-side simulation without graphical rendering.

Network Layer

The client connects to the game server via UDP and:

  • Sends Input packets with mouse coordinates (uint16) for shooting direction
  • Receives State packets containing player positions, bullets, and game tick
  • Handles Ping/Pong for latency estimation
  • Performs Auth handshake on connection

Network Protocol

Transport

  • Protocol: UDP
  • Endianness: Little-endian
  • Protocol Version: 1

Packet Header (8 bytes)

Offset Field Type Size
0 Version uint16 2 bytes
2 Packet Type uint16 2 bytes
4 Sequence Number uint16 2 bytes
6 Server Tick uint32 4 bytes

Packet Types

Type Direction Description
Input Client → Server Player movement and shoot commands with mouse coordinates
State Server → Client World state: all visible players, bullets, current tick
Ping Either Latency check
Pong Either Ping response
Auth Client → Server Authentication handshake
AuthResponse Server → Client Auth result (accept/reject)

Wire Format Details

  • Player positions: Encoded with 0.1 precision (world position × 10, stored as uint16)
  • Mouse coordinates: Stored as uint16 (screen resolution)
  • Bullet data: Position, velocity direction, owner player ID, remaining lifetime

Build System

Build-Time Configuration

All major components support build-time overrides via Go's -ldflags -X flag:

go build -ldflags "-X game/config.ServerAddress=0.0.0.0 -X game/config.ServerPort=9001" ./server/

This allows a single binary to be configured differently across environments without modifying source code.

Scripts

Script Purpose
start.sh Start both game server and login server
build-client.sh Build client binaries for current platform
build-windows-client.sh Cross-compile Windows client from Linux

Pre-built Binaries

Client binaries are stored in build/ directory with versioned subdirectories (e.g., v0.1.0/).

Running the System

Prerequisites

  • Go 1.26+
  • SQLite (pure Go driver used, no system SQLite needed)

Start All Services

# From project root
./start.sh

Start Individual Components

# Game server
go run ./server/

# Login server
cd loginserver && go run .

# Client (GUI mode)
cd client && go run .

# Client (headless mode)
cd client && go run . -headless

Configuration

Create a .env file in the working directory:

SERVER_ADDRESS=0.0.0.0
SERVER_PORT=9000
MAX_CONNECTIONS_PER_IP=10000
DATABASE_PATH=data/game.db
LOGIN_SERVER_ADDRESS=127.0.0.1
LOGIN_SERVER_PORT=8081

All values can also be passed as environment variables directly, which take precedence over .env and build-time defaults.