Introducing Matchbox
I'm very happy to announce the Matchbox project. A solution for painless peer-to-peer networking in rust web assembly.
Matchbox was born because I was making a multiplayer web game in rust, and I had the following problem:
How can I connect N web browsers with unreliable, unordered peer-to-peer connections?
Motivation
Now, why did I want this?
- I wanted unreliable and unordered connections because that's the ideal connection type for real-time multiplayer - Who has time to wait around for re-sending lost packets sent so long ago they are no longer relevant to the current game state?
- I wanted direct peer connections, i.e. not involving a server in the middle, because this allows even lower latency. If the peers are close together, for instance on the same network, they don't have to wait for traffic to be sent to some possibly far-away server and back. Also, it's cheaper because it means I don't have to run a server. And being p2p means it's compatible with GGPO-like rollback, like the amazing GGRS.
- Finally, I really enjoy making game jam games. And though it probably sounds crazy, I want to have a go at making a networked multiplayer game, and I believe that in order to get people to try it, it really has to be a web game.
Existing projects
Currently (and excluding browser plugins), the only way to connect web browsers with unreliable, unordered connections, is through WebRTC, which is notoriously difficult to set up.
There are a couple of projects that aim to make it easier.
For instance, there is naia-socket, which is an amazing project that hides away almost all of the complexity of connecting clients with WebRTC to a native server. The only problem is that it's very closely tied to a client-server architecture, and offers no way to establish a connection directly between two browsers.
I stumbled upon a project by Ernest Wong, dango-tribute, which has patches that modify naia-socket so the WebRTC server is hosted in its own iframe in one of the browsers. This was almost what I wanted, but it's still a client-server connection. I wanted connections between every player, which is why I ended up picking it apart and putting it back together as Matchbox.
Matchbox server
So the word "server" above might make it sound like this is not really peer-to-peer. That's not the case! The server is only needed to get peer connections up and running.
Though WebRTC allows peer-to-peer connections, establishing those connections is a bit tricky. The browsers need to know about each other, what their public IP addresses are, and how to traverse NATs and other pesky things in the way. Exchanging this kind of information is called signalling, and though the WebRTC spec describes what kind of information needs to be exchanged, it has been intentionally left out of the spec exactly how it is to be exchanged.
In any case, this meant I needed a signalling server, so I made matchbox_server
.
Matchbox server is relatively simple rust web server application. Whenever someone wants to join a p2p network, they connect to a Matchbox server over websockets and provide a room id. Everyone in the same room is then notified about the new peer, and the server will relay whatever signalling messages those peers need to establish direct WebRTC connections.
There is also a special type of dynamic room, next_n
rooms, where n is the maximum number of players in a room, which creates new rooms for the next n players to connect to the signalling server. i.e. it's a very crude form of matchmaking.
matchbox_socket
matchbox_socket
is a rust crate that hides away the intricacies of how to connect to a matchbox signalling server and establish unreliable, unordered WebRTC data connections to all peers in the room.
All you need to do, is provide a matchbox server address, and .await
or poll a message processing loop, and you will be notified whenever new peer connections have been established, and you'll be able to send and receive messages to those peers using non-blocking regular function calls.
matchbox_socket
also has a ggrs
integration feature, which adds an implementation of ggrs
's NonBlockingSocket
trait which means it can be directly used with ggrs
.
For instance, here's how to start a matchbox socket and handle messages in Bevy systems.
use matchbox_socket::WebRtcSocket;
fn start_matchbox_socket(mut commands: Commands, task_pool: Res<IoTaskPool>) {
let url = "wss://matchbox.example.com/next_2";
let (socket, message_loop) = WebRtcSocket::new(url);
// The message loop needs to be awaited, or nothing will happen.
// We do this here using bevy's task system.
task_pool.spawn(message_loop).detach();
// Put the socket interface somewhere other systems can get to it
commands.insert_resource(socket);
}
fn handle_new_peers(mut socket: ResMut<WebRtcSocket>) {
// Check if there are new connections since last time
// returns a vector of peer ids.
let new_peers = socket.accept_new_connections();
}
fn send_stuff(mut socket: ResMut<WebRtcSocket>) {
let payload: Box<[u8]> = // Whatever you want to send
socket.send(payload, "peer-id");
}
fn receive_stuff(mut socket: ResMut<WebRtcSocket>) {
for (peer_id, payload) in socket.receive() {
// Handle payload from peer
}
}
Demo
The easiest way to get a feel for how all this works in practice, is to take a look at the code of the matchbox_demo
example.
It shows how to make a working browser multiplayer "game" (if moving cubes around a plane can be called a game). It uses matchbox_socket
with the Bevy game engine and GGRS for rollback.
There is a live version here (move the cube with WASD):
- 2-Player: https://helsing.studio/box_game/
- 3-Player: https://helsing.studio/box_game/?players=3
- N-player: Edit the link above.
NOTE: Make sure you keep each window visible while running the demo. Bevy currently stops running if the tab is minimized or hidden.
You can also start the example locally by cloning the Matchbox repo: https://github.com/johanhelsing/matchbox.
From inside the project folder build and start the matchbox server:
cargo run --bin matchbox_server
Then go into the matchbox_demo
folder and build, run and host the example:
cargo make serve -p release
If you don't have cargo-make
you can install it with cargo install cargo-make
.
You can now access localhost:4000 in two browsers and you should be able to move two cubes around with WASD.
What's next?
It's still quite early days for the project, and there are lots of things I want to fix. Improving code quality, adding proper error handling and disconnects are at the very top.
I've only yet tested this with a few people and a couple of browsers/devices, so there are probably loads of broken things I have yet to discover.
I'm going make a release on crates.io, eventually. The bevy_ggrs integration however, depends on patches not yet released, so I'm probably going to wait for bevy 0.6 before doing that. matchbox_socket
and matchbox_server
have now been released to creates.io :)
That's all for now. Make sure to check out GitHub repository if you're interested.
I've also written a tutorial about how to make a complete game with Bevy, GGRS and matchbox.