Introduction
One day during lock down while on a call with some friends, we wanted to play an online drawing game but the website we were planning on using was broken just when we wanted to use it.
I realised that the mechanics probably weren't too complicated and I could probably make my own version. I'd also been wanting to work on a project with a friend called Louis Tarvin.
I had a general idea how this was going to be achieved: a canvas element to draw on and a websocket server to shuttle messages between clients. I hadn't had much experience with using an HTML canvas before but I guessed it would be simple enough. I did have a bit of experience with writing a websocket server in Rust which ended up meaning this was a project I could use as a teaching project through pair programming.
The end result
The source code is available at https://github.com/Louis-Tarvin/draw_game.
If you're not interested in the technical details you can find the deployment at https://draw.dewardt.uk.
The technical details
Research and resources
I always find that the best way to learn programming is through examples. For the websocket service I wanted to use actix-web since I knew how to integrate Websockets into it (and even if I didn't they have great documentation and examples) and the actor model fits very nicely with concurrent programming.
Fortunately I stumbled upon an example in MDN (Mozilla Developer Network) for drawing onto a canvas with the mouse which was exactly what we needed to do.
Server
So we set about implementing the Websocket server first.
We decided to codify the messages that our app sends into an enum
.
Rust has a really nice feature of enum
s where variants can have associated fields which
is essentially the equivalent of union
and enum
in C.
This allowed us to write the logic that handled the draw game separately to the the logic that serialises them allowing for a nice separation of concerns.
Here's a snippet of some of the events we used:
enum Event {
/// Chat message containing username followed by content
Message(usize, String),
/// Draw event containing: (x1, y1, x2, y2, penSize)
Draw(u32, u32, u32, u32, u32),
/// Clears the canvas
ClearCanvas,
/// Start of a new round
NewRound(usize, Option<u128>),
/// Assign the session a word to draw
NewLeader(bool, String, Option<u128>),
/* ... more variants ... */
}
Then when we sent the Event
to our Websocket handler it would be able to pattern match and decode it:
let message = match event {
Event::Message(username, msg) => format!("m{},{}", username, msg),
Event::Draw(x1, y1, x2, y2, pen_size) => {
format!("d{},{},{},{},{}", x1, y1, x2, y2, pen_size)
}
Event::ClearCanvas => "b".to_string(),
Event::NewRound(username, timeout) => format!("r{},{}", username, timeout.unwrap_or(0)),
Event::NewLeader(canvas_clearing, word, timeout) => format!(
"l{}{},{}",
if canvas_clearing { 'T' } else { 'F' },
word,
timeout.unwrap_or(0)
),
/* ... more variants ... */
}
We came up with a relatively simple string encoding using a single (usually) character to denote the type of message and then some sequence of values unique to the type delineated by commas.
The code that handles the logic within a game is self contained within the Room
struct which provides a series of public methods for operating on the room such as join
or handle_guess
which meant that the Websocket side just needed to parse the message and call the right method.
We ended up writing a lot of validation code which really helped when developing the front-end as we could see in the logs when the server side code had detected and issue in the messages we were sending. It also meant that it was very difficult to send malicious messages.
We adopted the policy that any message from the client was untrustworthy and we would never forward messages from one client to another without it being valid. This reduces the risk of someone finding a vulnerability that they could use to cheat or trigger bugs that break the client. For example if someone tries to send a clear command when it's not enabled we won't send that information along to the other clients.
Front end
Our initial prototype was in pure HTML, CSS (for positioning but no real styling) and JS with no external libraries. We manged to test a version that had rooms, chat and the drawing onto a canvas. At this point we decided to take some time to do an actual design.
To do this we used figma which is a great online collaborative design tool. We came up with a few designs of varying quality.
We could never come up with a clean looking way of showing the score at all times and it may be something we revisit. Clearly the purple designs took and got polished up the become the finished designs.
The redesign was a complete re-write of the front end including switch to using the React framework.
We used redux
for managing the global state of our application. Redux is great for ensuring a consistent state with transitions from one to another. Our socket manager would listen for incoming events and then tell redux about them. This would change the state which would trigger an automatic re-render of the various React components that detected changes.