Game Server Fundamentals

Learning Objectives

  • • Understand what makes game servers fundamentally different from web servers
  • • Learn the core game loop architecture and tick-based systems
  • • Explore different server architecture patterns for multiplayer games
  • • Master game state management and synchronization concepts
  • • Build your first tick-based game server with Go

Lesson 1.1: What Makes a Game Server Different?

⚠️ Common Misconception

At first glance, you might think: "Game servers are just web servers that handle multiplayer interactions." This assumption is dangerous and will lead to failed projects. Game servers are fundamentally different from web servers in critical ways.

Web Server vs Game Server

Web Server (HTTP-based)

Client Request → Process → Send Response → Done
- Request/response model
- Stateless (usually)
- Latency tolerance: 100-500ms OK
- Throughput-optimized
  • • User clicks button, waits for response
  • • Can handle thousands of requests per second
  • • Stateless: each request independent
  • • Caching and CDNs work well

Game Server (Real-time)

Continuous simulation → Broadcast state → Handle client input → Repeat
- Push model (server continuously sends state)
- Heavily stateful
- Latency criticality: <50ms matters
- Latency-optimized
  • • Server continuously updates all players
  • • Must maintain 60+ updates per second
  • • Stateful: everything affects everything
  • • Every millisecond counts

Real-Time Requirements

Latency Requirements

Web Apps
1-2 seconds OK
Games
50ms feels sluggish
FPS Shooters
<20ms for pros

Tick Rate & Frame Pacing

  • • Servers don't render frames, but they simulate at a fixed rate (tick rate)
  • • Common: 60 ticks/second = one update every 16.67ms
  • • Popular shooters: 128 ticks/second = 7.8ms per tick
  • • Every tick must complete on time, or players notice

Why Determinism Matters

  • • Game state must be consistent across all players
  • • Player A sees Player B at position X at time T
  • • Player B's client should agree with Player A's view
  • • One wrong calculation cascades to all players

The Illusion of Immediacy

Game servers create elaborate illusions to hide network delay:

  1. Client-side prediction: Feels instant locally
  2. Server verification: Ensures fairness
  3. Interpolation: Smooth other players' movement
  4. Lag compensation: Adjust hit detection for network delay

What Actually Happens

What players think:

  • • "I pressed the button, I shot immediately"
  • • "I see other players moving smoothly"
  • • "My hit registered when I aimed at them"

What actually happens:

  • • Input → Network delay (50-150ms) → Server processes → Other players download update → They see you
  • • Server must smooth out jumpy movement from packet loss
  • • Server must rewind time to check if your bullet actually hit

Lesson 1.2: Game Loop Basics

The Fundamental Game Loop

Every game server, from indie projects to AAA studios, follows this core pattern:

LOOP FOREVER:
  1. INPUT COLLECTION
     - Read from all connected clients
     - Parse input commands (movement, shooting, abilities)
     
  2. UPDATE (Game Logic)
     - Move players
     - Check collisions
     - Process abilities/effects
     - Update game state
     
  3. OUTPUT (Broadcast)
     - Serialize game state
     - Send to all affected players
     - Handle disconnections

This repeats at a fixed rate (e.g., 60 times per second).

Code Example: Basic Game Loop in Go

package main

import (
    "time"
)

type GameServer struct {
    tickRate      time.Duration  // How often to update (16.67ms for 60 ticks/sec)
    isRunning     bool
    players       map[int]*Player
    gameState     *GameState
}

type Player struct {
    ID            int
    Position      Vector2
    Velocity      Vector2
    Health        int
    InputBuffer   []PlayerInput  // Buffered inputs from this player
}

type Vector2 struct {
    X, Y float32
}

type GameState struct {
    Players     map[int]*Player
    Projectiles []Projectile
    World       *World
}

// The main game loop
func (g *GameServer) Start() {
    g.isRunning = true
    ticker := time.NewTicker(g.tickRate)
    defer ticker.Stop()
    
    for range ticker.C {  // Waits for next tick
        g.CollectInput()   // Step 1: Get client inputs
        g.Update()         // Step 2: Simulate this frame
        g.Broadcast()      // Step 3: Send updates to players
    }
}

func (g *GameServer) CollectInput() {
    // In reality, this would read from network buffers
    // For now, assume inputs already buffered from previous network reads
    for id, player := range g.players {
        if len(player.InputBuffer) > 0 {
            // Process oldest input first (FIFO)
            input := player.InputBuffer[0]
            player.InputBuffer = player.InputBuffer[1:]
            
            // Apply input to player state
            g.ApplyInput(id, input)
        }
    }
}

func (g *GameServer) Update() {
    startTime := time.Now()
    
    // 1. Update positions based on velocity
    for _, player := range g.players {
        player.Position.X += player.Velocity.X
        player.Position.Y += player.Velocity.Y
    }
    
    // 2. Check collisions with world
    g.UpdatePhysics()
    
    // 3. Update game logic (abilities, timers, etc)
    g.UpdateGameLogic()
    
    // 4. Process projectiles
    g.UpdateProjectiles()
    
    // Ensure tick completed in time
    elapsed := time.Since(startTime)
    if elapsed > g.tickRate {
        log.Printf("WARNING: Tick took %v, exceeds rate %v", elapsed, g.tickRate)
    }
}

func (g *GameServer) Broadcast() {
    // Create a snapshot of current game state
    snapshot := g.CreateSnapshot()
    
    // For each player, send only relevant data
    for _, player := range g.players {
        relevantState := g.FilterByRelevance(snapshot, player.ID)
        g.SendToPlayer(player.ID, relevantState)
    }
}

Tick Rate vs Frame Rate

Frame Rate (Client-side)

  • • Your game renders 60 FPS (or 144 FPS for competitive)
  • • This is graphics, doesn't affect server
  • • Can be higher than server tick rate

Tick Rate (Server-side)

  • • Server simulates 60 ticks/second (20ms per tick)
  • • Every tick, state updates and broadcasts to clients
  • • Clients interpolate between ticks for smooth rendering

Why Fixed Timestep?

  • Determinism: Same input always produces same output
  • Replay systems: Can record and replay by storing inputs + tick count
  • Fairness: All players experience same physics
  • Debugging: Deterministic = easier to find bugs

❌ Naive Approach (WRONG)

for {
    // Process whatever input arrived
    Update(variableTimeDelta)  // Inconsistent!
    Broadcast()
}

Why this fails: If network spikes, you skip multiple updates. Physics become unpredictable.

✅ Correct Approach

ticker := time.NewTicker(20 * time.Millisecond)  // Fixed 20ms

for range ticker.C {
    // Always process exactly 20ms of simulation
    Update(20.0 * time.Millisecond)
    Broadcast()
}

Always processes exactly 20ms of simulation, regardless of network conditions.

Lab 1: Build a Basic Tick-Based Game Loop

Objective

Build a simple game loop that runs at fixed 60 ticks/second, maintains player state, processes input, and tracks time per tick.

Requirements

  • • Implement `Start()` method with fixed tick rate
  • • Implement `Update()` to move players and apply friction
  • • Players should gradually stop (friction)
  • • Print state every tick
  • • Measure and report if any tick exceeds time budget

Starter Code

package main

import (
    "fmt"
    "time"
)

type Vector2 struct {
    X, Y float32
}

type Player struct {
    ID       int
    Position Vector2
    Velocity Vector2
    Health   int
}

type GameServer struct {
    tickRate time.Duration
    players  map[int]*Player
    tick     uint64
}

func NewGameServer() *GameServer {
    return &GameServer{
        tickRate: time.Duration(1000.0 / 60) * time.Millisecond, // 60 ticks/sec
        players:  make(map[int]*Player),
        tick:     0,
    }
}

func (g *GameServer) Start() {
    // TODO: Implement game loop
    // 1. Create ticker at g.tickRate
    // 2. Each tick: Update(), Broadcast()
    // 3. Track time per tick
}

func (g *GameServer) Update() {
    // TODO: 
    // 1. Move each player based on velocity
    // 2. Apply friction (reduce velocity by 10% each tick)
    // 3. Clamp position to world bounds [0, 1000]
}

func (g *GameServer) Broadcast() {
    fmt.Printf("[Tick %d] State: ", g.tick)
    for _, p := range g.players {
        fmt.Printf("P%d:(%.1f,%.1f) ", p.ID, p.Position.X, p.Position.Y)
    }
    fmt.Println()
}

// Add a player to the game
func (g *GameServer) AddPlayer(id int, x, y float32) {
    g.players[id] = &Player{
        ID:       id,
        Position: Vector2{x, y},
        Velocity: Vector2{0, 0},
        Health:   100,
    }
}

// Apply input (called from network handler)
func (g *GameServer) ApplyInput(playerID int, moveX, moveY float32) {
    if p, exists := g.players[playerID]; exists {
        p.Velocity.X = moveX * 5.0  // Scale input to velocity
        p.Velocity.Y = moveY * 5.0
    }
}

func main() {
    server := NewGameServer()
    
    // Add test players
    server.AddPlayer(1, 100, 100)
    server.AddPlayer(2, 200, 200)
    
    // Simulate input
    go func() {
        time.Sleep(100 * time.Millisecond)
        server.ApplyInput(1, 1, 0)    // Player 1 moves right
        time.Sleep(200 * time.Millisecond)
        server.ApplyInput(1, 0, 1)    // Player 1 moves down
    }()
    
    // Run for 5 seconds
    go server.Start()
    time.Sleep(5 * time.Second)
}

Expected Output

[Tick 0] State: P1:(100.0,100.0) P2:(200.0,200.0)
[Tick 3] State: P1:(101.5,100.0) P2:(200.0,200.0)
[Tick 5] State: P1:(103.7,100.0) P2:(200.0,200.0)
[Tick 7] State: P1:(105.6,100.0) P2:(200.0,200.0)
[Tick 10] State: P1:(107.2,100.0) P2:(200.0,200.0)
[Tick 12] State: P1:(108.5,100.0) P2:(200.0,200.0)
[Tick 15] State: P1:(109.6,100.0) P2:(200.0,200.0)
[Tick 18] State: P1:(110.4,100.0) P2:(200.0,200.0)
[Tick 20] State: P1:(111.0,100.0) P2:(200.0,200.0)
[Tick 22] State: P1:(111.4,100.0) P2:(200.0,200.0)
[Tick 25] State: P1:(111.6,100.0) P2:(200.0,200.0)
[Tick 27] State: P1:(111.7,100.0) P2:(200.0,200.0)
[Tick 30] State: P1:(111.7,100.0) P2:(200.0,200.0)
[Tick 32] State: P1:(111.6,100.0) P2:(200.0,200.0)
[Tick 35] State: P1:(111.4,100.0) P2:(200.0,200.0)
[Tick 37] State: P1:(111.1,100.0) P2:(200.0,200.0)
[Tick 40] State: P1:(110.7,100.0) P2:(200.0,200.0)
[Tick 42] State: P1:(110.2,100.0) P2:(200.0,200.0)
[Tick 45] State: P1:(109.6,100.0) P2:(200.0,200.0)
[Tick 47] State: P1:(108.9,100.0) P2:(200.0,200.0)
[Tick 50] State: P1:(108.1,100.0) P2:(200.0,200.0)
[Tick 52] State: P1:(107.2,100.0) P2:(200.0,200.0)
[Tick 55] State: P1:(106.2,100.0) P2:(200.0,200.0)
[Tick 57] State: P1:(105.1,100.0) P2:(200.0,200.0)
[Tick 60] State: P1:(103.9,100.0) P2:(200.0,200.0)
[Tick 62] State: P1:(102.6,100.0) P2:(200.0,200.0)
[Tick 65] State: P1:(101.2,100.0) P2:(200.0,200.0)
[Tick 67] State: P1:(99.7,100.0) P2:(200.0,200.0)
[Tick 70] State: P1:(98.1,100.0) P2:(200.0,200.0)
[Tick 72] State: P1:(96.4,100.0) P2:(200.0,200.0)
[Tick 75] State: P1:(94.6,100.0) P2:(200.0,200.0)
[Tick 77] State: P1:(92.7,100.0) P2:(200.0,200.0)
[Tick 80] State: P1:(90.7,100.0) P2:(200.0,200.0)
[Tick 82] State: P1:(88.6,100.0) P2:(200.0,200.0)
[Tick 85] State: P1:(86.4,100.0) P2:(200.0,200.0)
[Tick 87] State: P1:(84.1,100.0) P2:(200.0,200.0)
[Tick 90] State: P1:(81.7,100.0) P2:(200.0,200.0)
[Tick 92] State: P1:(79.2,100.0) P2:(200.0,200.0)
[Tick 95] State: P1:(76.6,100.0) P2:(200.0,200.0)
[Tick 97] State: P1:(73.9,100.0) P2:(200.0,200.0)
[Tick 100] State: P1:(71.1,100.0) P2:(200.0,200.0)
[Tick 102] State: P1:(68.2,100.0) P2:(200.0,200.0)
[Tick 105] State: P1:(65.2,100.0) P2:(200.0,200.0)
[Tick 107] State: P1:(62.1,100.0) P2:(200.0,200.0)
[Tick 110] State: P1:(58.9,100.0) P2:(200.0,200.0)
[Tick 112] State: P1:(55.6,100.0) P2:(200.0,200.0)
[Tick 115] State: P1:(52.2,100.0) P2:(200.0,200.0)
[Tick 117] State: P1:(48.7,100.0) P2:(200.0,200.0)
[Tick 120] State: P1:(45.1,100.0) P2:(200.0,200.0)
[Tick 122] State: P1:(41.4,100.0) P2:(200.0,200.0)
[Tick 125] State: P1:(37.6,100.0) P2:(200.0,200.0)
[Tick 127] State: P1:(33.7,100.0) P2:(200.0,200.0)
[Tick 130] State: P1:(29.7,100.0) P2:(200.0,200.0)
[Tick 132] State: P1:(25.6,100.0) P2:(200.0,200.0)
[Tick 135] State: P1:(21.4,100.0) P2:(200.0,200.0)
[Tick 137] State: P1:(17.1,100.0) P2:(200.0,200.0)
[Tick 140] State: P1:(12.7,100.0) P2:(200.0,200.0)
[Tick 142] State: P1:(8.2,100.0) P2:(200.0,200.0)
[Tick 145] State: P1:(3.6,100.0) P2:(200.0,200.0)
[Tick 147] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 150] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 152] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 155] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 157] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 160] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 162] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 165] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 167] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 170] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 172] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 175] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 177] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 180] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 182] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 185] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 187] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 190] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 192] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 195] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 197] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 200] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 202] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 205] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 207] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 210] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 212] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 215] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 217] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 220] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 222] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 225] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 227] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 230] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 232] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 235] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 237] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 240] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 242] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 245] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 247] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 250] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 252] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 255] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 257] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 260] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 262] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 265] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 267] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 270] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 272] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 275] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 277] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 280] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 282] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 285] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 287] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 290] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 292] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 295] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 297] State: P1:(0.0,100.0) P2:(200.0,200.0)
[Tick 300] State: P1:(0.0,100.0) P2:(200.0,200.0)

✅ Success Criteria

  • • Game loop runs at exactly 60 ticks per second
  • • Players move smoothly when input is applied
  • • Friction gradually slows players to a stop
  • • No tick exceeds the 16.67ms time budget
  • • State is printed every tick for debugging

Ready for Week 2?

Next week we'll dive into networking protocols, UDP vs TCP trade-offs, and build your first networked game server that can handle multiple clients.

Continue to Week 2: Network Protocols →