Building a WebSocket-Based Multiplayer Game Server in Go

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setup
  4. Creating the Game Server
  5. Implementing the Game Logic
  6. Handling Client Connections
  7. Conclusion

Introduction

In this tutorial, we will build a WebSocket-based multiplayer game server using the Go programming language. By the end of this tutorial, you will have a basic understanding of how to create a server, handle incoming client connections, and implement game logic in Go.

Prerequisites

Before starting this tutorial, make sure you have the following prerequisites:

  • Basic knowledge of the Go programming language.
  • Go installed on your system.
  • A text editor or integrated development environment (IDE) for writing Go code.
  • Basic understanding of WebSocket communication.

Setup

To begin, let’s set up our project structure and dependencies. Follow the steps below:

  1. Create a new directory for your project:

     $ mkdir multiplayer-game-server
     $ cd multiplayer-game-server
    
  2. Initialize a new Go module:

     $ go mod init example.com/multiplayer-game-server
    
  3. Install the Gorilla WebSocket package, which provides a WebSocket implementation for Go:

     $ go get github.com/gorilla/websocket
    

Creating the Game Server

Now, let’s create the basic structure for our game server. To do this, follow these steps:

  1. Create a new file named server.go:

     $ touch server.go
    
  2. Open server.go in your favorite text editor or IDE and import the necessary packages:

     package main
        
     import (
         "log"
         "net/http"
        
         "github.com/gorilla/websocket"
     )
    
  3. Define the GameServer struct:

     type GameServer struct {
         clients   map[*websocket.Conn]bool
         broadcast chan []byte
         upgrader  websocket.Upgrader
     }
    

    Here, we define a struct that will hold the client connections, a channel for broadcasting messages to all clients, and a websocket.Upgrader to upgrade HTTP connections to WebSocket connections.

  4. Implement the NewGameServer function to initialize a new GameServer instance:

     func NewGameServer() *GameServer {
         return &GameServer{
             clients:   make(map[*websocket.Conn]bool),
             broadcast: make(chan []byte),
             upgrader: websocket.Upgrader{
                 CheckOrigin: func(r *http.Request) bool { return true },
             },
         }
     }
    

    This function creates a new instance of the GameServer struct, initializes the clients map and the broadcast channel, and sets the CheckOrigin function of the websocket.Upgrader to allow connections from any origin.

  5. Implement the Run method to run the game server:

     func (s *GameServer) Run() {
         http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
             // Upgrade the connection to a WebSocket connection
             conn, err := s.upgrader.Upgrade(w, r, nil)
             if err != nil {
                 log.Println("Failed to upgrade connection:", err)
                 return
             }
            
             // Add the client connection to the clients map
             s.clients[conn] = true
            
             // Close the connection and remove it from the clients map
             defer func() {
                 conn.Close()
                 delete(s.clients, conn)
             }()
            
             // Handle incoming messages
             for {
                 messageType, message, err := conn.ReadMessage()
                 if err != nil {
                     log.Println("Failed to read message:", err)
                     break
                 }
            
                 // Broadcast the received message to all clients
                 s.broadcast <- message
             }
         })
            
         go s.handleBroadcasts()
            
         // Start the HTTP server
         log.Println("Starting game server...")
         err := http.ListenAndServe(":8080", nil)
         if err != nil {
             log.Fatal("Failed to start server:", err)
         }
     }
    

    Here, we define a handler function for the /ws endpoint, which will be responsible for upgrading the HTTP connection to a WebSocket connection, handling incoming messages, and broadcasting messages to all clients. We also start a goroutine to handle the broadcasting of messages.

  6. Implement the handleBroadcasts method to broadcast messages to connected clients:

     func (s *GameServer) handleBroadcasts() {
         for {
             // Read the next message from the broadcast channel
             message := <-s.broadcast
            
             // Broadcast the message to all clients
             for client := range s.clients {
                 err := client.WriteMessage(websocket.TextMessage, message)
                 if err != nil {
                     log.Println("Failed to write message:", err)
                 }
             }
         }
     }
    

    This method reads messages from the broadcast channel and sends them to all connected clients. If an error occurs while sending a message to a client, the error is logged.

Implementing the Game Logic

Now that we have our game server set up, we can implement the game logic. Let’s create a simple game where clients can move a player around a 2D grid. Follow these steps:

  1. Open server.go again and add the following code to the GameServer struct:

     type Player struct {
         X int `json:"x"`
         Y int `json:"y"`
     }
        
     type GameState struct {
         Players []Player `json:"players"`
     }
        
     func (s *GameServer) handlePlayerMovement(conn *websocket.Conn) {
         var player Player
         player.X = 0
         player.Y = 0
        
         for {
             // Read the next message from the client
             messageType, message, err := conn.ReadMessage()
             if err != nil {
                 log.Println("Failed to read message:", err)
                 break
             }
        
             // Update the player's position based on the received message
             switch string(message) {
             case "up":
                 player.Y--
             case "down":
                 player.Y++
             case "left":
                 player.X--
             case "right":
                 player.X++
             }
        
             // Create a JSON representation of the game state
             gameState := GameState{
                 Players: []Player{player},
             }
             gameStateData, err := json.Marshal(gameState)
             if err != nil {
                 log.Println("Failed to marshal game state:", err)
                 break
             }
        
             // Broadcast the game state to all clients
             s.broadcast <- gameStateData
         }
     }
    

    Here, we define a Player struct to represent a player’s position on the grid, and a GameState struct to represent the current state of the game. We also add a method handlePlayerMovement that reads messages from a client and updates the player’s position accordingly. After updating the position, we broadcast the updated game state to all clients.

  2. Update the Run method to handle player movements:

     func (s *GameServer) Run() {
         // ...
        
         http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
             conn, err := s.upgrader.Upgrade(w, r, nil)
             if err != nil {
                 log.Println("Failed to upgrade connection:", err)
                 return
             }
        
             s.clients[conn] = true
        
             go s.handlePlayerMovement(conn)
        
             // ...
         })
        
         // ...
     }
    

    Here, we spawn a goroutine for each client connection to handle player movements.

Handling Client Connections

Lastly, let’s implement a way to handle when clients connect or disconnect from the game server. Follow these steps:

  1. Update the Run method to log when clients connect and disconnect:

     func (s *GameServer) Run() {
         // ...
        
         http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
             conn, err := s.upgrader.Upgrade(w, r, nil)
             if err != nil {
                 log.Println("Failed to upgrade connection:", err)
                 return
             }
        
             s.clients[conn] = true
        
             // Log when a client connects
             log.Println("Client connected:", conn.RemoteAddr())
        
             go s.handlePlayerMovement(conn)
        
             defer func() {
                 // Log when a client disconnects
                 log.Println("Client disconnected:", conn.RemoteAddr())
                    
                 conn.Close()
                 delete(s.clients, conn)
             }()
        
             // ...
         })
        
         // ...
     }
    

    Here, we log when a client connects and disconnects by printing the remote address of the client connection.

  2. Save and close server.go. Now, we can run the game server:

     $ go run server.go
    

    Congratulations! You have successfully created a WebSocket-based multiplayer game server in Go. Clients can connect to the server using a WebSocket connection, send messages to update their player’s position, and receive updates of other players’ positions.

Conclusion

In this tutorial, we learned how to build a WebSocket-based multiplayer game server using Go. We covered setting up the server, handling client connections, implementing game logic, and broadcasting updates to connected clients.

You can extend this game server by adding features like game rooms, player authentication, and game-specific logic. Feel free to experiment and enhance the server based on your own requirements.

Remember to refer to the official Go documentation and package documentation for more information on the Go language and the Gorilla WebSocket package. Happy coding!