Table of Contents
- Introduction
- Prerequisites
- Setup
- Creating the Game Server
- Implementing the Game Logic
- Handling Client Connections
- 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:
-
Create a new directory for your project:
$ mkdir multiplayer-game-server $ cd multiplayer-game-server
-
Initialize a new Go module:
$ go mod init example.com/multiplayer-game-server
-
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:
-
Create a new file named
server.go
:$ touch server.go
-
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" )
-
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. -
Implement the
NewGameServer
function to initialize a newGameServer
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 theCheckOrigin
function of thewebsocket.Upgrader
to allow connections from any origin. -
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. -
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:
-
Open
server.go
again and add the following code to theGameServer
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 aGameState
struct to represent the current state of the game. We also add a methodhandlePlayerMovement
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. -
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:
-
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.
-
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!