Real-Time Multiplayer Games over UDP

Update

You can now use client/server connections over UDP (SCTP) in your HTML5 Multiplayer Games using geckos.io.


Real-Time Multiplayer Games over UDP

Although this topic is not only related to Phaser 3, I decided to put it here, since I use it and like it.

I do not know if you have seen the phaser-3-real-time-multiplayer-game-with-physics example I built a while ago. There, I use socket-io to communicate between the client and the server.

This was the first Real-Time Mulitplayer game I built. At that time, I did not know potential issues related to HTML5 Real-Time games, until @pyxld_kris asked “Have you done anything special to account for tcp?”.

Today I know what he meant. TCP is just not meant to be used for Real-Time gaming because of how it works. You should use UDP instaed! But does this work in the browser? Yes and no. You could use something like https://peerjs.com/ which uses WebRTC to communicate from client to client over WebRTC’s DataChannel. But what if you want to use a authoritative server with RTCDataChannel communication? There is no way to do this. At least as far as I know based on what I read online.

So why am I telling you all of this? Because I made it WORK! At the moment I’m programming a nice library to use it with any type of HTML5 game.

Please note that I am very new to this stuff. If anyone knows already a easy way to do it, please share. This will safe me a lot of work building a complex library. Also please share your thoughts on potential issues that might occur.

I have set up a chat app on http://18.184.1.123:8080/. Of cource UDP is not good for chat apps, but it shows that it works. If you are using chrome you can check the connection on chrome://webrtc-internals/

The goal is to build an simple API like socket-io has. This is how the source of the chat app looks right now. It’s very clean and easy to use.

// client.ts
import LibNameClient from '../lib/client'

let url = location.origin
let libName = new LibNameClient(url)

libName.connectSync(() => {
  console.log('connected')

  let button = document.getElementById('button')
  let text = document.getElementById('text') as HTMLInputElement
  let list = document.getElementById('list')

  if (button)
    button.addEventListener('click', e => {
      if (text) {
        let content = text.value
        if (content && content.trim().length > 0) {
          // send a new message to the server
          libName.emit('sendMessage', content.trim())
          text.value = ''
        }
      }
    })

  // receives messages from other clients through the server
  // and adds the message to the list
  libName.on('sendMessage', data => {
    if (list && typeof data === 'string') {
      let li = document.createElement('li')
      li.innerHTML = data
      list.appendChild(li)
    }
  })
})
// server.ts
import LibNameServer from '../lib/server'

// starts the webrtc signaling server on port 8080
let libName = new LibNameServer(8080)

// receives a message from one client
libName.on('sendMessage', data => {
  // send that message to all clients
  libName.emit('sendMessage', data)
})

This project is in very early stage. But I just want to know your thoughts about it. I will of course publish the source code once I have implemented everything I want.

Also, this project does not have a name yet. If you could think of a suitable name, please share it.

5 Likes

Hey Yannick,

Glad my question gave you a nudge towards this cool project. If you can get this to work, I’m sure it would be used by a large number of developers! I’ve looked pretty extensively and haven’t found a good solution for what you’re building here.

Could you explain what’s going on behind the scenes in the code you attached? Which libraries/APIs are you using? What have you tried that’s worked, and what have you tried that hasn’t? I think with a few more details people may be able to point out potential problem areas.

Looking good so far!

I does already work! Have you tried the chat app I mentioned in my first post? It works over UDP from client to server and then from the server to all connected clients.

As always, I tried 1000 things that did not work, until I made it work. It uses the native WebRTC API on the client and a Node-WebRTC implementation on the server. I do the peer signaling over http via an express server.

It does not use many dependencies. I try to do it on my own to keep it clean and have control overt it.

For now the library structure looks like this

├── src
│   ├── core     # The core, which handles the WebRTC connections
│   ├── lib      # The lib, which will expose a nice looking api to the programmer
│   ├── bridge   # The bridge, which is a helper to communicate from lib to core

The API should look similar to the one socket-io uses. I use their cheatsheet as a model.

Very exciting stuff! I had been tempted to try to mess with WebRTC, but had read over and over that it was “infeasible for general-purpose UDP”.

If you can get a library up and running for it, this could have widespread interest!

The WebRTC’s DataChannel actually uses SCTP on top of UDP.
This protocol has many options. For example it can be set to unreliable and unordered, just like UDP.

I have also read a lot about this. But I thought, if it can be used to stream a video, it surely can stream simple data. I hope I’m right. I guess we will see as soon as I have build a game on top of it and we can compare its performance to the other multiplayer game I have made using socket-io.

Single Byte Transmissions

A cool feature that I will implement is the possibility to send Single Bytes.

In a multiplayer game I normally upload the users input in a single radix(36) converted number. The code would look like this

let total = 0
if (input.left) total += 1
if (input.right) total += 2
if (input.up) total += 4
if (input.action) total += 8
let str36 = total.toString(36)
this.socket.emit('U' /* short for updatePlayer */, str36)

If the user is pressing left, up and action at the same time, str36 would be ‘d’. When we send this over socket-io we send a string of 14 bytes 42/G,["U","d"]. (I actually do not know what the 45/G is, but it is sending it)

I plan to reduce this to 1 byte, by allowing to send a default event like so

this.io.emit(str36)

This will only send a single byte (d) and reduce the payload 14 times!


For all other events than ‘default’, we add at least 2 bytes. One for the eventName and a colon to separate the eventName and the data.
We would send it like so this.io.emit('U' /* short for updatePlayer */, str36) and data sent would look like so U:d. Still only 3 bytes instead of 14.


I’m very excited about this :star_struck:

You can send bytes with WebSocket. Just stop using socket.io as it doesn’t really add anything substantial and use raw WebSocket with typed arrays/buffers.

EDIT:
Also, when calculating reduction in payload, it’s worth to include the packet overhead.

1 Like

True

True. I guess I can only truly compare it once it’s finished.

Based on this, the packet overhead is much higher on WebRTC than it is with WebSocket.

I run a test by sending only a single byte. Chrome’s webrtc monitoring tool shows 40’000 bitsSentPerSecond with a fps of 60. This is about 83 bytes on each frame.

So it is true, it sends a lot more than WebSocket.

I have now built a game on top of it. You can’t do much but jump around. The browser only sends the inputs left, right and up to the server. The arcade physics is calculated on the server which sends the updated position of each dude back to each browser.

The initialState is sent via http. All other updates are sent via the RTCDataChannel.

You can test it here (18.184.1.123:1444)

I think it runs pretty nice. It has some mini lags but I have not yet seen huge ping spikes, which was the reason for me searching a WebSocket alternative.

I have not said it sends more than WebSocket, just that you need to include overhead in calculations. Spending too much work to lower your message from 10 to 5 bytes won’t be 50% improvement.

I’m surprised by how big overhead SCTP has. UDP itself is much smaller than TCP (of course, if you need to implement TCP features using UDP, you would mostly add that overhead back in). In either case, the main benefit is eliminating head of line blocking. Although if the overhead is really 60-120 bytes, that is really damn big.

No, I read this online.
It’s a misunderstanding. You where absolutely right.

Hey @yannick, Developer here if you would like help with this library and to go over details I’m willing to help build it with you. (As I need this for some projects I am working on).

Can you get in touch with me if you are interested. Thanks!

Github username is the same as this username I will receive alerts from there.

Hi @WhiteRaBot17,

Thanks for your offer.

At the moment the code is not well structured at all which will make a collaborative work hard.
But I plan to finish it (implementing all my thougts) and publish the code next weekend (28.04) on github.

I will also publish the chat app example and the multiplayer game example.

After that, contribution is welcome :blush:

You can now test the Multiplayer Demo I built.
Each connected user can control one dude and add multiple dummy dudes.

It’s running on EC2 (t3.nano) on http://35.157.53.149:1444/

1 Like

Nice work! For some types of heavily twitch-based games, some extra overhead with the protocol WebRTC is using may be worth it to prevent the blocking Antriel mentioned.

And I know this may be much further down the line, but those settings you mention in SCTP could be interesting. Since a lot of times we have to essentially rebuild TCP reliability on top of UDP to prevent UDP+TCP issues, if those are settings inside SCTP that could make a nice toggle layer to switch back and forth between reliable and unreliable as required by the game.

I will definitely be interested to check out the Github when it goes up.

Hmm…I get an error when trying to check out the demo on Firefox on Mac OS. I get:

“Network error trying to fetch resource: connectionManager.ts 55:6”
and
“Unhandled promise rejection Type error: ‘a is undefined’: es6.promise.js110 > connectionManager.ts 58”

Perhaps the server is not up anymore?

You’re right. I have the same issue on Firefox in Ubuntu. It works with chrome.

I will try to fix it.

It looks like a CORS issue. I have recently switched from express to a pure node http server.

Update: I have fixed it. I was only a CORS issue.

You can’t switch between reliable and unreliable once a channel has been created. But you can open multiple channels.

In the example game, I fetch the state via http and all games updates via a unreliable and unordered SCTP connection.