Jump to content
  • Advertisement
  • 05/10/13 07:16 PM
    Sign in to follow this  

    Multiplayer Pong with Go, WebSockets and WebGl

    General and Gameplay Programming

    This article will guide you through the implementation of a pong server in Go and a pong client in JavaScript using Three.js as the render engine. I am new to web development and implementing pong is my first project. So there are probably things that could be done better, especially on the client side, but I wanted to share this anyway. I assume that you are familiar with Go and the Go environment. If you are not, I recommend doing the Go tour on http://golang.org.

    Setting up the Webserver and the Client

    We first implement the basic webserver functionallity. For the pong server add a new directory to your go workspace, e.g., "$GOPATH/src/pong". Create a new .go file in this directory and add the following code: package main import ( "code.google.com/p/go.net/websocket" "log" "net/http" "time" ) func wsHandler(ws *websocket.Conn) { log.Println("incoming connection") //handle connection } func main() { http.Handle("/ws/", websocket.Handler(wsHandler)) http.Handle("/www/", http.StripPrefix("/www/", http.FileServer(http.Dir("./www")))) go func() { log.Fatal(http.ListenAndServe(":8080", nil)) }() //running at 30 FPS frameNS := time.Duration(int(1e9) / 30) clk := time.NewTicker(frameNS) //main loop for { select { case <-clk.C: //do stuff } } } We use the "net/http" package to serve static files that are in the "./www" directory relative to the binary. This is where we will later add the pong client .html file. We use the websocket package at "code.google.com/p/go.net/websocket" to handle incoming websocket connections. These behave very similar to standard TCP connections. To see the webserver in action we make a new directory in the pong directory with the name "www" and add a new file to the directory called "pong.html". We add the following code to this file: Pong This code simply opens a websocket connection to the server. The libraries which will be used later in this tutorial are already included. Namely, we will use jQuery for some helper functions, Three.js to render stuff and a small helper library named DataStream.js which helps parsing data received from the server. We could also download those .js files and put them into the "www" directory and serve them directly from our Go webserver. Now if we go back to the pong diretory and start the pong server (type "go run *.go" in the terminal) you should be able to connect to the webserver in your browser. Go to the url "http://localhost:8080/www/pong.html" and you should see a message in your terminal saying "incoming connection". If you want to include one or several websocket-based games in a larger website I recommend using nginx as a reverse proxy. In the newest version you can also forward websocket connections. In a unix-type operating system this feature can be used to forward the websocket connection to a unix domain socket on which the game server is listening. This allows you to plug in new games (or other applications) without reconfiguring or restarting the webserver.

    Handling Connections on the Server

    We add three new types to store information for each client connection: type PlayerId uint32 type UserCommand struct { Actions uint32 } type ClientConn struct { ws *websocket.Conn inBuf [1500]byte currentCmd UserCommand cmdBuf chan UserCommand } The type PlayerId is used for unique identifiers for the players. The struct UserCommand describes the information that is sent from the clients. For now it contains an integer that we use as a bitmask, which basically encodes the keyboard state of the client. We will see how to use that later on. Now we come to the actual ClientConn struct. Each client has a websocket connection which is used to receive and send data. The buffer is used to read data from the websocket connection. The currentCmd field contains the most recent received user command. The last field is a buffer for user commands. We need this buffer since we receive the user command packages from the client asynchronously. So the received commands are written into the buffer and at the beginning of each frame in the main loop we read all commands from each player and place the most recent one in the currentCmd field. This way the user command cannot suddenly change mid-frame because we received a new package from the client. So lets see how to implement the wsHandler function. We first need to add a new global variable var newConn = make(chan *ClientConn) that we need to handle incoming connections sychronously in the main loop. Next we have to import two additional packages, namely "bytes" and "encoding/binary". Now we are set up to handle incoming connections and read incoming packages: func wsHandler(ws *websocket.Conn) { cl := &ClientConn{} cl.ws = ws cl.cmdBuf = make(chan UserCommand, 5) cmd := UserCommand{} log.Println("incoming connection") newConn <- cl for { pkt := cl.inBuf[0:] n, err := ws.Read(pkt) pkt = pkt[0:n] if err != nil { log.Println(err) break } buf := bytes.NewBuffer(pkt) err = binary.Read(buf, binary.LittleEndian, &cmd) if err != nil { log.Println(err) break } cl.cmdBuf <- cmd } } The wsHandler function gets called by the http server for each websocket connection request. So everytime the function gets called we create a new client connection and set the websocket connection. Then we create the buffer used for receiving user commands and send the new connection over the newConn channel to notify the main loop of the new connection. Once this is done we start processing incoming messages. We read from the websocket connection into a slice of bytes which we then use to initialize a new byte buffer. Now we can use the Read function from "encoding/binary" to deserialize the buffer into a UserCommand struct. If no errors ocurred we put the received command into the command buffer of the client. Otherwise we break out of the loop and leave the wsHandler function which closes the connection. Now we need to read out incoming connections and user commands in the main loop. To this end, we add a global variable to store client information var clients = make(map[PlayerId]*ClientConn) We need a way to create the unique player ids. For now we keep it simple and use the following function: var maxId = PlayerId(0) func newId() PlayerId { maxId++ return maxId } Note that the lowest id that is used is 1. An Id of 0 could represent an unassigned Id or something similar. We add a new case to the select in the main loop to read the incoming client connections: ... select { case <-clk.C: //do stuff case cl := <-newConn: id := newId() clients[id] = cl //login(id) } ... It is important to add the clients to the container synchronously like we did here. If you add a client directly in the wsHandler function it could happen that you change the container while you are iterating over it in the main loop, e.g., to send updates. This can lead to undesired behavior. The login function handles the game related stuff of the login and will be implemented later. We also want to read from the input buffer synchronously at the beginning of each frame. We add a new function which does exactly this: func updateInputs() { for _, cl := range clients { for { select { case cmd := <-cl.cmdBuf: cl.currentCmd = cmd default: goto done } } done: } } and call it in the main loop: case <-clk.C: updateInputs() //do stuff For convenience later on we add another type type Action uint32 and a function to check user commands for active actions func active(id PlayerId, action Action) bool { if (clients[id].currentCmd.Actions & (1 << action)) > 0 { return true } return false } which checks if the bit corresponding to an action is set or not.

    Sending Updates

    We send updates of the current game state at the end of each frame. We also check for disconnects in the same function. var removeList = make([]PlayerId, 3) func sendUpdates() { buf := &bytes.Buffer{} //serialize(buf,false) removeList = removeList[0:0] for id, cl := range clients { err := websocket.Message.Send(cl.ws, buf.Bytes()) if err != nil { removeList = append(removeList, id) log.Println(err) } } for _, id := range removeList { //disconnect(id) delete(clients, id) } } We use the Message.Send function of the websocket package to send binary data over the websocket connection. There are two functions commented out right now which we will add later. One serializes the current game state into a buffer and the other handles the gameplay related stuff of a disconnect. As stated earlier we call sendUpdates at the end of each frame: ... case <-clk.C: updateInputs() //do stuff sendUpdates() ...

    Basic Gameplay Structures

    Now that we have the basic server structure in place we can work on the actual gameplay. First we make a new file vec.go in which we will add the definition of a 3-dimensional vector type with some functionality: package main type Vec [3]float64 func (res *Vec) Add(a, b *Vec) *Vec { (*res)[0] = (*a)[0] + (*b)[0] (*res)[1] = (*a)[1] + (*b)[1] (*res)[2] = (*a)[2] + (*b)[2] return res } func (res *Vec) Sub(a, b *Vec) *Vec { (*res)[0] = (*a)[0] - (*b)[0] (*res)[1] = (*a)[1] - (*b)[1] (*res)[2] = (*a)[2] - (*b)[2] return res } func (a *Vec) Equals(b *Vec) bool { for i := range *a { if (*a) != (*b) { return false } } return true } We use three dimensions, since we will render the entities in 3D and for future extendability. For the movement and collision detection we will only use the first two dimensions. The following gameplay related code could be put into a new .go file. In pong we have three game objects or entities. The ball and two paddles. Let us define data types to store relevant information for those entities: type Model uint32 const ( Paddle Model = 1 Ball Model = 2 ) type Entity struct { pos, vel, size Vec model Model } var ents = make([]Entity, 3) The Model type is an id which represents a model. In our case that would be the model for the paddle and for the ball. The Entity struct containst the basic information for an entity. We have vectors for the position, the velocity and the size. The size field represents the size of the bounding box. That is, for the ball each entry should be twice the radius. Now we initialize the entities. The first two are the two paddles and the third is the ball. func init() { ents[0].model = Paddle ents[0].pos = Vec{-75, 0, 0} ents[0].size = Vec{5, 20, 10} ents[1].model = Paddle ents[1].pos = Vec{75, 0, 0} ents[1].size = Vec{5, 20, 10} ents[2].model = Ball ents[2].size = Vec{20, 20, 20} } Note that the init function will be called automatically once the server starts. The way we will set up our camera on the client, the first coordinate of a vector will point to the right of the screen, the second one will point up and the third will be directed out of the screen. We also add the two actions we need for pong, i.e., Up and Down: const ( Up Action = 0 Down Action = 1 ) and an empty update function func updateSimulation() { } which we call in the main loop ... case <-clk.C: updateInputs() updateSimulation() sendUpdates() ...

    Serialization and Client functionality

    We add the serialization and the rendering on the client now because it is nice to see stuff even when the entities are not moving yet.


    When serializing game state my approach is to serialize one type of information after the other. That is, we first serialize all positions, then the velocities, etc. In a more complex game with many entities I would first send the amount of entities which are serialized and then a list of the corresponding entity ids. The serialization would then also be dependent on the player id, since we might want to send different information to different players. Here we know that there are only three entities and we always send the full game state to each client. For the serialization we need the "io" and the "encoding/binary" packages. The actual code is quite simple func serialize(buf io.Writer) { for _, ent := range ents { binary.Write(buf, binary.LittleEndian, ent.model) } for _, ent := range ents { binary.Write(buf, binary.LittleEndian, ent.pos) } for _, ent := range ents { binary.Write(buf, binary.LittleEndian, ent.vel) } for _, ent := range ents { binary.Write(buf, binary.LittleEndian, ent.size) } } Note that it actually does not make sense to send the size and the model more than once, since the websocket connection is reliable and the values do not change. In general it would be better to only send data that changed in the current frame. To this end we keep a copy of the last game state and only send fields for which differences are detected. Of course we have to tell the client which data is actually sent. This can be done by including a single byte for each data type which acts as a bitmask. We add the new variable for the old game state: var entsOld = make([]Entity, 3) which is updated with func copyState() { for i, ent := range ents { entsOld = ent } } in the sendUpdates() function directly after we sent the updates func sendUpdates() { buf := &bytes.Buffer{} //serialize(buf, false) removeList = removeList[0:0] for id, cl := range clients { err := websocket.Message.Send(cl.ws, buf.Bytes()) if err != nil { removeList = append(removeList, id) log.Println(err) } } copyState() for _, id := range removeList { delete(clients, id) //disconnect(id) } } We copy the state here, because the difference is needed for the serialization, and the disconnect() function can already alter the game state. Then we update the serialization function (We also need to import the package "bytes") func serialize(buf io.Writer, serAll bool) { bitMask := make([]byte, 1) bufTemp := &bytes.Buffer{} for i, ent := range ents { if serAll || ent.model != entsOld.model { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, ent.model) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) bitMask[0] = 0 bufTemp.Reset() for i, ent := range ents { if serAll || !ent.pos.Equals(&entsOld.pos) { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, ent.pos) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) bitMask[0] = 0 bufTemp.Reset() for i, ent := range ents { if serAll || !ent.vel.Equals(&entsOld.vel) { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, ent.vel) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) bitMask[0] = 0 bufTemp.Reset() for i, ent := range ents { if serAll || !ent.size.Equals(&entsOld.size) { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, ent.size) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) } We have to write the data into a temporary buffer since we have to write the bitmask before the actual data and we only know the bitmask once we've iterated over all entities. The serialization could probably be implemented more efficiently in terms of memory allocation, but I leave that as an exercise to the reader. Note that we added an additional input argument serAll. If serAll is set to true we serialize the complete gamestate. This flag is used to send the whole game state once to each newly connected player. Thus we have to add to the main loop on the server ... case cl := <-newConn: id := newId() clients[id] = cl buf := &bytes.Buffer{} serialize(buf, true) websocket.Message.Send(cl.ws, buf.Bytes()) ... and uncomment the call in sendUpdates() func sendUpdates() { buf := &bytes.Buffer{} serialize(buf, false) ... }

    Pong Client

    First we add the input-related functionality to the client. At the beginning of our script in pong.html add a variable for the client actions and for the frame duration: ...

    Copyright (c) 1999-2018 GameDev.net, LLC Powered by Invision Community


    Important Information

    By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

    GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

    Sign me up!