- Go 80.2%
- Shell 15.8%
- JavaScript 2.5%
- HTML 1.5%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| benchmarks/results | ||
| client | ||
| config | ||
| database | ||
| docs | ||
| k6 | ||
| login-client | ||
| login-web | ||
| loginserver | ||
| network | ||
| scripts | ||
| server | ||
| tasks/k6 | ||
| .gitignore | ||
| build-client.sh | ||
| build-windows-client.sh | ||
| game-server | ||
| go.mod | ||
| go.sum | ||
| go.work | ||
| go.work.sum | ||
| login-server-bin | ||
| start.sh | ||
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):
- Environment variables (e.g.,
DATABASE_PATH,SERVER_ADDRESS,SERVER_PORT) .envfile in the working directory- 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
-
Startup (
server.Start()):- Resolves UDP address and binds listener on
config.ServerPort - Opens SQLite database at
config.DatabasePathwith WAL mode - Enters main event loop via
run()
- Resolves UDP address and binds listener on
-
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
- Continuously receives UDP packets via
-
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
uint16in 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
localhostand127.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.