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.
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:
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:
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.
We add three new types to store information for each client connection:
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
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:
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
We need a way to create the unique player ids. For now we keep it simple and use the following function:
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:
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:
and call it in the main loop:
For convenience later on we add another type
and a function to check user commands for active actions
which checks if the bit corresponding to an action is set or not.
We send updates of the current game state at the end of each frame. We also check for disconnects in the same function.
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:
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:
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:
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.
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:
and an empty update function
which we call in the main loop
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
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:
which is updated with
in the sendUpdates() function directly after we sent the updates
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")
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
and uncomment the call in sendUpdates()
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:
Inside the anonymous function passed to $(document).ready() we add handler for key events:
The key codes 83 and 87 correspond to the 'w' and 's' key on the keyboard. If the 'w' key is pressed we set the first bit in the actions bit mask to 1, i.e., the button 'w' corresponds to the Up action. If the key is released we set the corresponding bit to 0. Of course you could use other keys. You can check the key codes here: http://unixpapa.com/js/testkey.html
Now we know the current state of the actions, but we still have to send them to the server. To this end we have to implement a main loop in the client. As I said, I am new to JavaScript, but I read that the recommended way to do this is the following (add this function to the end of the script):
The function requestAnimationFrame gives some control of the update interval to the browser. That is, the number of frames per second is reduced if the browser tab is not open etc. We encapsulate this function into setTimeout(..., interval) to set a maximum number of frames per second. For simplicity we run the client with the same frames per second as the server. This could be done differently, e.g., we could run the client faster than the server and interpolate between the received game states. There is a lot of other stuff which can be done on the client-side to improve the player experience which we do not cover in this tutorial (google for client-side prediction/interpolation, lag compensation etc.).
We still have to implement the sendCmd() function. We use the DataStream class to serialize the actions. This works similar to the "encoding/binary" package. The write functions of DataStream use LittleEndian by default.
If we now call clientFrame() once inside the ws.onopen callback
the client will start sending user commands to the server.
What is still missing is the logic for receiving the game states and the rendering of the game entities. The render engine is initialized as follows
We create a new camera and a new scene, add some lights and define two geometries. One is a sphere which we will use for the ball and the other is a cube which will be used for the paddles. We also define two materials with different colors which we will use for the ball and the paddles respectively. The radius of the sphere is set to 0.5, which results in a unit bounding box.
We also add a function to our script which we use to add new objects to the scene:
where we know that a model id of 1 is a paddel and a model id of 2 is the ball.
We still have to add a call to the init function
For the deserialization we need a new variable which stores the entity data:
The deserialization is done in the onmessage callback of the websocket.
No magic here. If the bit for a entity is set we read the corresponding data. If we get an update for the model, which should happen only once for each entity, we add a new mesh to the scene.
For now the only thing left to do on the client is to render the scene. To this end we update the clientFrame() function
We set the position of the mesh to the current entity position and then render the scene. If you run the server (go run *.go) you should be able to connect to http://localhost:8080/www/pong.html and see the entities rendered in 3D.
It is time to bring some movement into the game. Before we can do that we have to deal with logins and disconnects. We add a new global variable to the gameplay file which stores the player ids of the active players, i.e., the players controlling the paddles,
Now we can add the login function
As soon as two players logged in we start the game by setting the velocity of the ball to a non-zero vector
We also have to call the login function in the main loop
We also have to handle disconnects
where stopGame() resets the entity positions and velocities
The disconnect function is called in sendUpdates
Now we finally add movement for the entities
which has to be called in the update function
If we connect to the server from two tabs in our browser the ball starts moving to the right. Once we close one of the tabs the ball returns to the starting position.
Next we process player input.
If two players are connected, we check for the Up and Down actions and change the vertical velocity accordingly. This has to be called in the update function before we move the entities
We can now move the paddles if we connect to the server http://localhost:8080/www/pong.html from two tabs.
We are still missing collision detection and response. First we introduce an upper and lower border
The field is centered at the origin and we check if the bounding box of each entity is leaving the field. If we detect a collision, we reset the position and flip the vertical velocity. This might actually not be the optimal way to handle the collision for the paddles since we do not really want them to bounce from the borders, but we will keep it for now.
We call the collision check after processing the inputs and before moving the entities
If we change the starting velocity of the ball, e.g.
we can see it bouncing off the borders. The same effect can be seen when moving the paddles.
If we now add a collision response for the entities we are almost done. In our case the collision detection is rather simple. We have to detect the collision between a sphere and an axis aligned bounding box. This and more advanced problems are explained int this article: http://www.wildbunny.co.uk/blog/2011/04/20/collision-detection-for-dummies/.
Before implementing the collision response we have to add some functionality to our vec.go file:
The Clamp function clamps a vector such that it lies within a box of size s centered at the origin. The other operators should be self explanatory.
With the new functionality we can implement the collision response by adding the following to the checkCollision function (we also need to import the "math" package)
I added some comments to hopefully make things clear. The first part is finding the closest distance from the paddle to the sphere which is done as described in the linked article. For the collision response we have to change the velocity of the ball. We use the vector connecting the closest points as contact normal and reflect the ball at the corresponding contact plane.
A last check we need to add is if the ball got past the paddle on the left or right side. The check itself is straightforward, but we first need to add a field to our entity struct containing the score
This is probably not the cleanest way to keep track of the scores, because not every entity needs to have a score field, but it keeps things simple. I will share some more thoughts on the storage of the game state later.
We reset the scores in the startGame function
Now we need to add the following to the collision check:
We also have to send the score to the client, so it can be displayed in the browser. To this end we add the following to the serialization function
and the following to the deserialization on the client
To display the scores on the client we simply add two div elemnts to the html body
and change the deserialization of the scores to update the html elements
We completed the first prototype of the multiplayer pong project (Here is the link again: http://localhost:8080/www/pong.html )
The server-side serialization is a bit awkward, with lots of code duplication. We could use a different structure for storing the game state which is more compatible with the serialization. Note that this is only a suggestion and all ways of storing the game state have pros and cons.
To this end we remove the following global variables and structures related to the game state
and replace them with
We can see that we switched from a slice of structures to a structure of slices. We also included the list of active players in the game state since it could also be useful to send it to the client.
We have to modify all references to the old global variables. This is a bit tedious, but most of it should be fairly straightforward. Keep in mind that the slices are reference types, so we have to perform a deep copy when copying the states
We replace all other references to the ents variable except in the serialization function, which we will rewrite now. We add a helper function to serialize a slice of vectors
and update the serialization function
We also have to make a small change to the client since we are sending only two score variables now. We change the deserialization of the scores to
If we also send each client its own player id and deserialize the list of active players, the client would actually know which paddle it controls. This could for example be used to write a client which plays on its own. This and making the client more pretty is left as an exercise for the reader.
The files for this tutorial can also be found on https://github.com/dane-unltd/pongtut. The files with the changes from the last section can be found on https://github.com/dane-unltd/pongtutimp
You can use the Go command, e.g., "go get github.com/dane-unltd/pongtut" to install the files in your Go workspace. Then go to the pongtut directory in the terminal and execute "go run *.go". You can now play pong in your browser under following link http://localhost:8080/www/pong.html.
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:
<html> <head> <title>Pong</title> <style> body { width: 640px; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script> <script src="https://raw.github.com/kig/DataStream.js/master/DataStream.js"></script> <script src="http://threejs.org/build/three.min.js"></script> <script type="text/javascript"> var ws $(document).ready(function() { if ("WebSocket" in window) { // Let us open a web socket ws = new WebSocket("ws://"+document.location.host+"/ws/pong"); ws.binaryType = "arraybuffer"; ws.onopen = function() { console.log("connection open") } ws.onmessage = function(evt) { } ws.onclose = function() { console.log("Connection is closed..."); }; }else{ alert("no websockets on your browser") } }) </script> </head> <body> </body> </html>
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)[i] != (*b)[i] { 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.
Serialization
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[i] = 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[i].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[i].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[i].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[i].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:
... <script type="text/javascript"> var ws var actions = 0 var interval = 1000/30 $(document).ready(function() { ...
Inside the anonymous function passed to $(document).ready() we add handler for key events:
... $(document).ready(function() { if ("WebSocket" in window) { ... }else{ alert("no websockets on your browser") } document.onkeydown = function(event) { var key_press = String.fromCharCode(event.keyCode); var key_code = event.keyCode; if (key_code == 87) { actions |= 1<<0 } if (key_code == 83) { actions |= 1<<1 } } document.onkeyup = function(event){ var key_press = String.fromCharCode(event.keyCode); var key_code = event.keyCode; if (key_code == 87) { actions &= ~(1<<0) } if (key_code == 83) { actions &= ~(1<<1) } } }) ...
The key codes 83 and 87 correspond to the 'w' and 's' key on the keyboard. If the 'w' key is pressed we set the first bit in the actions bit mask to 1, i.e., the button 'w' corresponds to the Up action. If the key is released we set the corresponding bit to 0. Of course you could use other keys. You can check the key codes here: http://unixpapa.com/js/testkey.html
Now we know the current state of the actions, but we still have to send them to the server. To this end we have to implement a main loop in the client. As I said, I am new to JavaScript, but I read that the recommended way to do this is the following (add this function to the end of the script):
function clientFrame() { setTimeout(function() { window.requestAnimationFrame(clientFrame); sendCmd(); }, interval); }
The function requestAnimationFrame gives some control of the update interval to the browser. That is, the number of frames per second is reduced if the browser tab is not open etc. We encapsulate this function into setTimeout(..., interval) to set a maximum number of frames per second. For simplicity we run the client with the same frames per second as the server. This could be done differently, e.g., we could run the client faster than the server and interpolate between the received game states. There is a lot of other stuff which can be done on the client-side to improve the player experience which we do not cover in this tutorial (google for client-side prediction/interpolation, lag compensation etc.).
We still have to implement the sendCmd() function. We use the DataStream class to serialize the actions. This works similar to the "encoding/binary" package. The write functions of DataStream use LittleEndian by default.
function sendCmd() { var cmd = new DataStream() cmd.writeUint32(actions) ws.send(cmd.buffer); }
If we now call clientFrame() once inside the ws.onopen callback
... ws.onopen = function() { console.log("connection open") clientFrame() } ...
the client will start sending user commands to the server.
What is still missing is the logic for receiving the game states and the rendering of the game entities. The render engine is initialized as follows
var camera, scene, renderer; var mat1,mat2 var cube,sphere function init3d() { camera = new THREE.PerspectiveCamera( 45, 400/300, 1, 10000 ); camera.position.z = 200; scene = new THREE.Scene(); var ambientLight = new THREE.AmbientLight(0x252525); scene.add(ambientLight); var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.9 ); directionalLight.position.set( 150, 50, 200 ); scene.add( directionalLight ); cube = new THREE.CubeGeometry(1,1,1) sphere = new THREE.SphereGeometry(0.5,32,16) mat1 = new THREE.MeshLambertMaterial( { color: 0xff0000, shading: THREE.SmoothShading } ); mat2 = new THREE.MeshLambertMaterial( { color: 0x00ff00, shading: THREE.SmoothShading } ); renderer = new THREE.WebGLRenderer(); renderer.setSize( 640, 480) document.body.appendChild( renderer.domElement); }
We create a new camera and a new scene, add some lights and define two geometries. One is a sphere which we will use for the ball and the other is a cube which will be used for the paddles. We also define two materials with different colors which we will use for the ball and the paddles respectively. The radius of the sphere is set to 0.5, which results in a unit bounding box.
We also add a function to our script which we use to add new objects to the scene:
function newMesh(model) { var mesh if (model==2){ mesh = new THREE.Mesh(sphere, mat2) }else if (model==1){ mesh = new THREE.Mesh(cube, mat1) } scene.add(mesh) return mesh }
where we know that a model id of 1 is a paddel and a model id of 2 is the ball.
We still have to add a call to the init function
$(document).ready(function() { if ("WebSocket" in window) { init3d() ... } }
For the deserialization we need a new variable which stores the entity data:
var ents = [new Object(),new Object(),new Object()]
The deserialization is done in the onmessage callback of the websocket.
... ws.onmessage = function(evt) { var buf = new DataStream(evt.data) var nEnts = 3 var bitMask = buf.readUint8() for (var i = 0; i<nEnts; i++) { if ((bitMask & (1<<i))>0) { var model = buf.readUint32() ents[i] = newMesh(model) } } var bitMask = buf.readUint8() for (var i = 0; i<nEnts; i++) { if ((bitMask & (1<<i))>0) { var pos = buf.readFloat64Array(3) ents[i].position.x = pos[0] ents[i].position.y = pos[1] ents[i].position.z = pos[2] } } var bitMask = buf.readUint8() for (var i = 0; i<nEnts; i++) { if ((bitMask & (1<<i))>0) { var vel = buf.readFloat64Array(3) //On the client, we do not actually do //anything with the velocity for now ... } } var bitMask = buf.readUint8() for (var i = 0; i<nEnts; i++) { if ((bitMask & (1<<i))>0) { var size = buf.readFloat64Array(3) ents[i].scale.x = size[0] ents[i].scale.y = size[1] ents[i].scale.z = size[2] } } } ...
No magic here. If the bit for a entity is set we read the corresponding data. If we get an update for the model, which should happen only once for each entity, we add a new mesh to the scene.
For now the only thing left to do on the client is to render the scene. To this end we update the clientFrame() function
function clientFrame() { setTimeout(function() { window.requestAnimationFrame(clientFrame); renderer.render(scene, camera); sendCmd(); }, interval); }
We set the position of the mesh to the current entity position and then render the scene. If you run the server (go run *.go) you should be able to connect to http://localhost:8080/www/pong.html and see the entities rendered in 3D.
Movement
It is time to bring some movement into the game. Before we can do that we have to deal with logins and disconnects. We add a new global variable to the gameplay file which stores the player ids of the active players, i.e., the players controlling the paddles,
var players = make([]PlayerId, 2)
Now we can add the login function
func login(id PlayerId) { if players[0] == 0 { players[0] = id if players[1] != 0 { startGame() } return } if players[1] == 0 { players[1] = id startGame() } }
As soon as two players logged in we start the game by setting the velocity of the ball to a non-zero vector
func startGame() { ents[2].vel = Vec{5, 0, 0} }
We also have to call the login function in the main loop
... case cl := <-newConn: id := newId() clients[id] = cl login(id) buf := &bytes.Buffer{} ...
We also have to handle disconnects
func disconnect(id PlayerId) { if players[0] == id { players[0] = 0 stopGame() } else if players[1] == id { players[1] = 0 stopGame() } }
where stopGame() resets the entity positions and velocities
func stopGame() { ents[0].pos = Vec{-75, 0, 0} ents[1].pos = Vec{75, 0, 0} ents[2].pos = Vec{0, 0, 0} ents[2].vel = Vec{0, 0, 0} }
The disconnect function is called in sendUpdates
... for _, id := range removeList { disconnect(id) delete(clients, id) } ...
Now we finally add movement for the entities
func move() { for i := range ents { ents[i].pos.Add(&ents[i].pos, &ents[i].vel) } }
which has to be called in the update function
func updateSimulation() { move() }
If we connect to the server from two tabs in our browser the ball starts moving to the right. Once we close one of the tabs the ball returns to the starting position.
Next we process player input.
func processInput() { if players[0] == 0 || players[1] == 0 { return } newVel := 0.0 if active(players[0], Up) { newVel += 5 } if active(players[0], Down) { newVel -= 5 } ents[0].vel[1] = newVel newVel = 0.0 if active(players[1], Up) { newVel += 5 } if active(players[1], Down) { newVel -= 5 } ents[1].vel[1] = newVel }
If two players are connected, we check for the Up and Down actions and change the vertical velocity accordingly. This has to be called in the update function before we move the entities
func updateSimulation() { processInput() move() }
We can now move the paddles if we connect to the server http://localhost:8080/www/pong.html from two tabs.
We are still missing collision detection and response. First we introduce an upper and lower border
const FieldHeight = 120 func collisionCheck() { for i := range ents { if ents[i].pos[1] > FieldHeight/2-ents[i].size[1]/2 { ents[i].pos[1] = FieldHeight/2 - ents[i].size[1]/2 if ents[i].vel[1] > 0 { ents[i].vel[1] = -ents[i].vel[1] } } if ents[i].pos[1] < -FieldHeight/2+ents[i].size[1]/2 { ents[i].pos[1] = -FieldHeight/2 + ents[i].size[1]/2 if ents[i].vel[1] < 0 { ents[i].vel[1] = -ents[i].vel[1] } } } }
The field is centered at the origin and we check if the bounding box of each entity is leaving the field. If we detect a collision, we reset the position and flip the vertical velocity. This might actually not be the optimal way to handle the collision for the paddles since we do not really want them to bounce from the borders, but we will keep it for now.
We call the collision check after processing the inputs and before moving the entities
func updateSimulation() { processInput() collisionCheck() move() }
If we change the starting velocity of the ball, e.g.
func startGame() { ents[2].vel = Vec{2, 3, 0} }
we can see it bouncing off the borders. The same effect can be seen when moving the paddles.
If we now add a collision response for the entities we are almost done. In our case the collision detection is rather simple. We have to detect the collision between a sphere and an axis aligned bounding box. This and more advanced problems are explained int this article: http://www.wildbunny.co.uk/blog/2011/04/20/collision-detection-for-dummies/.
Before implementing the collision response we have to add some functionality to our vec.go file:
func (res *Vec) Clamp(s *Vec) { for i := range *res { if (*res)[i] > (*s)[i]/2 { (*res)[i] = (*s)[i] / 2 } if (*res)[i] < -(*s)[i]/2 { (*res)[i] = -(*s)[i] / 2 } } } func Dot(a, b *Vec) float64 { return (*a)[0]*(*b)[0] + (*a)[1]*(*b)[1] + (*a)[2]*(*b)[2] } func (v *Vec) Nrm2Sq() float64 { return Dot(v, v) } func (res *Vec) Scale(alpha float64, v *Vec) *Vec { (*res)[0] = alpha * (*v)[0] (*res)[1] = alpha * (*v)[1] (*res)[2] = alpha * (*v)[2] return res }
The Clamp function clamps a vector such that it lies within a box of size s centered at the origin. The other operators should be self explanatory.
With the new functionality we can implement the collision response by adding the following to the checkCollision function (we also need to import the "math" package)
func collisionCheck() { ... rSq := ents[2].size[0] / 2 rSq *= rSq for i := 0; i < 2; i++ { //v points from the center of the paddel to the point on the //border of the paddel which is closest to the sphere v := Vec{} v.Sub(&ents[2].pos, &ents[i].pos) v.Clamp(&ents[i].size) //d is the vector from the point on the paddle closest to //the ball to the center of the ball d := Vec{} d.Sub(&ents[2].pos, &ents[i].pos) d.Sub(&d, &v) distSq := d.Nrm2Sq() if distSq < rSq { //move the sphere in direction of d to remove the //penetration dPos := Vec{} dPos.Scale(math.Sqrt(rSq/distSq)-1, &d) ents[2].pos.Add(&ents[2].pos, &dPos) //reflect the velocity along d when necessary dotPr := Dot(&ents[2].vel, &d) if dotPr < 0 { d.Scale(-2*dotPr/distSq, &d) ents[2].vel.Add(&ents[2].vel, &d) } } } }
I added some comments to hopefully make things clear. The first part is finding the closest distance from the paddle to the sphere which is done as described in the linked article. For the collision response we have to change the velocity of the ball. We use the vector connecting the closest points as contact normal and reflect the ball at the corresponding contact plane.
A last check we need to add is if the ball got past the paddle on the left or right side. The check itself is straightforward, but we first need to add a field to our entity struct containing the score
type Entity struct { pos, vel, size Vec model Model score uint32 }
This is probably not the cleanest way to keep track of the scores, because not every entity needs to have a score field, but it keeps things simple. I will share some more thoughts on the storage of the game state later.
We reset the scores in the startGame function
func startGame() { ents[0].score = 0 ents[1].score = 0 ents[2].vel = Vec{2, 3, 0} }
Now we need to add the following to the collision check:
func collisionCheck() { ... if ents[2].pos[0] < -100 { ents[2].pos = Vec{0, 0, 0} ents[2].vel = Vec{2, 3, 0} ents[1].score++ } else if ents[2].pos[0] > 100 { ents[2].pos = Vec{0, 0, 0} ents[2].vel = Vec{-2, 3, 0} ents[0].score++ } }
We also have to send the score to the client, so it can be displayed in the browser. To this end we add the following to the serialization function
func serialize(buf io.Writer, serAll bool) { ... bitMask[0] = 0 bufTemp.Reset() for i, ent := range ents { if serAll || ent.score != entsOld[i].score { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, ent.score) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) }
and the following to the deserialization on the client
... ws.onmessage = function(evt) { ... var bitMask = buf.readUint8() for (var i = 0; i<nEnts; i++) { if ((bitMask & (1<<i))>0) { ents[i].score = buf.readUint32() } } } ...
To display the scores on the client we simply add two div elemnts to the html body
<body> <div id = "p1score" style="float:left">score1</div> <div id = "p2score" style="float:right;margin-right:10px">score2</div> </body>
and change the deserialization of the scores to update the html elements
... var bitMask = buf.readUint8() for (var i = 0; i<nEnts; i++) { if ((bitMask & (1<<i))>0) { ents[i].score = buf.readUint32() console.log(i,ents[i].score) if (i==0) { $("#p1score").html(ents[i].score) } else if (i==1) { $("#p2score").html(ents[i].score) } } } ...
We completed the first prototype of the multiplayer pong project (Here is the link again: http://localhost:8080/www/pong.html )
Further Considerations
The server-side serialization is a bit awkward, with lots of code duplication. We could use a different structure for storing the game state which is more compatible with the serialization. Note that this is only a suggestion and all ways of storing the game state have pros and cons.
To this end we remove the following global variables and structures related to the game state
type Entity struct { pos, vel, size Vec model Model score uint32 } var ents = make([]Entity, 3) var entsOld = make([]Entity, 3) var players = make([]PlayerId, 2)
and replace them with
type GameState struct { pos, vel, size []Vec model []Model score []uint32 players []PlayerId } func NewGameState() *GameState { st := &GameState{} st.pos = make([]Vec, 3) st.vel = make([]Vec, 3) st.size = make([]Vec, 3) st.model = make([]Model, 3) st.score = make([]uint32, 2) st.players = make([]PlayerId, 2) return st } var state = NewGameState() var stateOld = NewGameState()
We can see that we switched from a slice of structures to a structure of slices. We also included the list of active players in the game state since it could also be useful to send it to the client.
We have to modify all references to the old global variables. This is a bit tedious, but most of it should be fairly straightforward. Keep in mind that the slices are reference types, so we have to perform a deep copy when copying the states
func copyState() { copy(stateOld.pos, state.pos) copy(stateOld.vel, state.vel) copy(stateOld.size, state.size) copy(stateOld.model, state.model) copy(stateOld.score, state.score) copy(stateOld.players, state.players) }
We replace all other references to the ents variable except in the serialization function, which we will rewrite now. We add a helper function to serialize a slice of vectors
func serializeVecSlice(buf io.Writer, serAll bool, vs, vsOld []Vec) { bitMask := make([]byte, 1) bufTemp := &bytes.Buffer{} for i := range vs { if serAll || !vs[i].Equals(&vsOld[i]) { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, &vs[i]) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) }
and update the serialization function
func serialize(buf io.Writer, serAll bool) { bitMask := make([]byte, 1) bufTemp := &bytes.Buffer{} for i := range state.model { if serAll || state.model[i] != stateOld.model[i] { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, state.model[i]) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) serializeVecSlice(buf, serAll, state.pos, stateOld.pos) serializeVecSlice(buf, serAll, state.vel, stateOld.vel) serializeVecSlice(buf, serAll, state.size, stateOld.size) bitMask[0] = 0 bufTemp.Reset() for i := range state.score { if serAll || state.score[i] != stateOld.score[i] { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, state.score[i]) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) bitMask[0] = 0 bufTemp.Reset() for i := range state.players { if serAll || state.players[i] != stateOld.players[i] { bitMask[0] |= 1 << uint(i) binary.Write(bufTemp, binary.LittleEndian, state.players[i]) } } buf.Write(bitMask) buf.Write(bufTemp.Bytes()) }
We also have to make a small change to the client since we are sending only two score variables now. We change the deserialization of the scores to
... var nPlayers = 2 var bitMask = buf.readUint8() for (var i = 0; i<nPlayers; i++) { if ((bitMask & (1<<i))>0) { var score = buf.readUint32() if (i==0) { $("#p1score").html(score) } else if (i==1) { $("#p2score").html(score) } } } ...
If we also send each client its own player id and deserialize the list of active players, the client would actually know which paddle it controls. This could for example be used to write a client which plays on its own. This and making the client more pretty is left as an exercise for the reader.
Shortcut
The files for this tutorial can also be found on https://github.com/dane-unltd/pongtut. The files with the changes from the last section can be found on https://github.com/dane-unltd/pongtutimp
You can use the Go command, e.g., "go get github.com/dane-unltd/pongtut" to install the files in your Go workspace. Then go to the pongtut directory in the terminal and execute "go run *.go". You can now play pong in your browser under following link http://localhost:8080/www/pong.html.