A real-time game from scratch - WebSockets [2]
October 28, 2020
Previous Entries
Introduction
Cabin Fever is one of those side-projects which occupies a disproportionate amount of mind share compared to Tempus or C19. Whilst I am making an effort to work on side-projects which provide more end-to-end value over a deep technical dive form of value, the challenge of tackling a realtime entertainment requirement is far too enticing to not devote thinking time to.
Passing data back and forth
We need to be able to send instructions from the browser to the server and data back about the game state. In the first iteration I used the typical HTTP request-response so as to ensure that the setup was working.
This method however is far too inefficient. There is an alternative called WebSockets.
WebSockets
Instead of setting up a connection, sending a request, receing a response, then closing the connection, we can setup the connection, send and receive as much data as we want, then close the connection once we’re done.
Server-side
On the server-side, I considered using the built in support for websockets, however I got started much faster with github.com/gorilla/websocket
. Creating the initial websocket handler took only around 20 lines of code that hooked into the same http server I used for serving HTTP requests. My only issue was that I needed to provide a checkOrigin
function which is basically the websocket equivalent of CORS.
Client-side
On the client-side, I did not need any custom libraries so far. I simply started using the built-in WebSockets implementation to connect to the new endpoint (in this case, /ws
, since /
was taken by a standard HTTP handler).
Since we explicitly need to wait for the connection before starting, our code begins to look event-driven. To setup, I hook into onopen
on the WebSocket object to ensure that the connection has been setup before we begin to send messages back to the server and the onmessage
event to react to messages received from the server.
Towards a game
So far it has been sending datetimes back and forth, which does not amount to much of a game.
The endgoal of this project is to create a realtime browser game, so the next step is to begin introducing features one would find in a game. An easy place to start is capturing keyboard events and sending them to the server, having it update some state on the sever which can be sent back to the client.
This process has an important aim - by performing calculations on the server rather than the client we are aiming for a server-authorative model, which can make cheating more difficult.
Keyboard events
Capturing keyboard input was not difficult at all - it simply involved attaching a listener to the onkeydown
event on the <body>
tag of the HTML document. This gave me access to e.key
which value is a string referring to the key pressed. In my case, I stuck to the classic WASD
combination (which is a left hand equivalent of the arrow keys) which are used most of the time in games to control lateral movement.
I only begin listening to keyboard inputs after onopen
on the websocket and depending on whether the key was w
, a
, s
or d
I translate it to an enum with the values UP
, DOWN
, LEFT
or RIGHT
. Notably I do not update the internal state of the client side at this point in time. I do happen to know that some games do both at the same time and then update the state when it receives a response from the backend with the aim of making the game feel smoother and snappier. I plan on applying this optimisation later on.
Game State
On the server-side I initialise a struct called GameState
which for now simply tracks the X and Y position of the one player. On receiving of a message, I update the X and Y value according to whether the received string is UP
, DOWN
, LEFT
or RIGHT
.
Two points of note here:
- The co-ordinate system in the browser is partially inverted from what we learn at school in Maths
- The game state has no wrap around validation and I ended up causing an integer underflow
Browser Co-ordinate System vs School Maths Co-ordinates
In school, I learnt that a cartesian co-ordinate system works like this:
When it comes to the browser (or graphics in general), the Y Axis is actually inverted. Increasing the Y axis value means moving downwards rather than upwards.
Put simply:
- Moving up:
y--
- Moving down:
y++
- Moving left:
x--
- Moving right:
x++
Wrap Around and Underflows
In a game like pong, there are boundaries to the game. The ball will never exit the boundaries (hopefully.) This means that the x and y positions should never go lower than zero, if the origin point is the top left corner.
In my case, whether the final game will have wrap around or not is a design decision for a later stage. An interesting occurrence is that, out of habit, I used the uint
type to represent the X
and Y
values in the game state. uint
stands for unsigned integer
, meaning a zero or positive whole number, the value cannot be negative.
As I tested the client (which was writing the retrieved game state to the DOM), the Y value suddenly jumped from 0
to 18446744073709552000
. In this case, since the value is unable to be negative, it wrapped all the way around the largest possible value that it can store. This is the same reason that Gandhi in the original Civilization game would become aggressive, birthing the meme of Gandhi using Nuclear Weapons: the low aggression number would be pushed into a negative value, however since it was unsigned it would wrap around to the largest possible value.
The solution to this is to provide validation of the values - if it reaches below zero, then switch it back to zero.
Conclusion
So far I have not felt particularly challenged as I have not written any code which I was not already at least familar with. Here are the following avenues I look forward to exploring:
- A proper frontend for rendering graphics, using a framework such as PhaserJS or PixiJS
- Multiple Clients - how to handle receiving of data from multiple sources, updating a single game state and propogating those changes to all clients
- Physics - having the game state update on a core loop and push out updates to all clients regardless of whether they provide input or not
You can find the code for this article at the article-002 tag.
Next Entry