Creating online games using Nakama and Defold

We posted an article on online multiplayer game development using Nakama and Defold. Check it out and let us know what you think:


Been excited to see this. I’ve seen a lot of posts asking for help with multiplayer (and some with Nakama in particular.) Really glad to see some material targeting this. Thanks for writing!


I’ve awaited this post eagerly! Looks great!


Issue: played the demo game on two browser tabs. The “You won!” message is shown to both the winner and the loser after the game completes.

1 Like

Huh, yeah, that is not right. And now when I tested I see “Your turn” in both tabs… hmm. I’ll check this as soon as I can.

1 Like

This is awesome. I have been wondering though (for a while now)… why does the documentation for Nakama not feature much on Defold / Lua . Is it something that is still being addressed on their side?

I’ve started hooking up my project. Locally (from the Defold IDE) it works great. When bundled as HTML5 something goes wrong.

Update: Enabling websocket debugging reveals a generic error:

dmloader.js:511 INFO:ENGINE: Defold Engine 1.2.179 (5209b50)
dmloader.js:511 INFO:WEBSOCKET: dmWebSocket::g_DebugWebSocket == 2
dmloader.js:511 INFO:ENGINE: Loading data from: dmanif:game.dmanifest
dmloader.js:511 INFO:WEBSOCKET: Registered websocket extension
dmloader.js:511 Registered rnd Extension
dmloader.js:511 INFO:ENGINE: Initialised sound device 'default'
dmloader.js:511 DEBUG:SCRIPT: login()	function: 0x18d1f18
dmloader.js:511 DEBUG:SCRIPT: init()
dmloader.js:511 DEBUG:SCRIPT: device_login()	table: 0x18d47a0
dmloader.js:511 DEBUG:SCRIPT: Unable to get hardware mac address for UUID
dmloader.js:511 DEBUG:SCRIPT: 	defold.uuid(),
dmloader.js:511 53760a21-8c36-47ef-c791-4e16dc651aeb
dmloader.js:511 DEBUG:SCRIPT: Unable to get hardware mac address for UUID
dmloader.js:511 DEBUG:SCRIPT: 	body,
dmloader.js:511 { --[[0x18d58e0]]
dmloader.js:511   id = "885faa5b-1666-4ff7-cf6a-376c6cb8e7bf"
dmloader.js:511 }
dmloader.js:511 DEBUG:SCRIPT: authenticate_device() with coroutine
dmloader.js:511 DEBUG:SCRIPT: HTTP	POST
dmloader.js:511 DEBUG:SCRIPT: DATA	{"id":"885faa5b-1666-4ff7-cf6a-376c6cb8e7bf"}
dmloader.js:511 DEBUG:SCRIPT: {"created":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2MGJiOTk1Mi1kMWJkLTQ1NjItODUxZS02NDc5NTFiZTBkMWIiLCJ1c24iOiJiaVJkWndSdlFoIiwiZXhwIjoxNjE0NzY2NzAyfQ.aHYo8HOK07EPUYNdBF7iULLoTvM0FR57V244zrrs1UY"}
dmloader.js:511 DEBUG:SCRIPT: eyJ1aWQiOiI2MGJiOTk1Mi1kMWJkLTQ1NjItODUxZS02NDc5NTFiZTBkMWIiLCJ1c24iOiJiaVJkWndSdlFoIiwiZXhwIjoxNjE0NzY2NzAyfQ
dmloader.js:511 DEBUG:SCRIPT: result,
dmloader.js:511 { --[[0x191c060]]
dmloader.js:511   created = 1614766642,
dmloader.js:511   token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2MGJiOTk1Mi1kMWJkLTQ1NjItODUxZS02NDc5NTFiZTBkMWIiLCJ1c24iOiJiaVJkWndSdlFoIiwiZXhwIjoxNjE0NzY2NzAyfQ.aHYo8HOK07EPUYNdBF7iULLoTvM0FR57V244zrrs1UY",
dmloader.js:511   expires = 1614766702,
dmloader.js:511   username = "biRdZwRvQh",
dmloader.js:511   user_id = "60bb9952-d1bd-4562-851e-647951be0d1b"
dmloader.js:511 }
dmloader.js:511 DEBUG:SCRIPT: get_account() with coroutine
dmloader.js:511 DEBUG:SCRIPT: HTTP	GET
dmloader.js:511 DEBUG:SCRIPT: DATA	nil
dmloader.js:511 DEBUG:SCRIPT: {"user":{"id":"60bb9952-d1bd-4562-851e-647951be0d1b","username":"biRdZwRvQh","lang_tag":"en","metadata":"{}","create_time":"2021-03-03T10:17:22Z","update_time":"2021-03-03T10:17:22Z"},"wallet":"{}","devices":[{"id":"885faa5b-1666-4ff7-cf6a-376c6cb8e7bf"}]}
dmloader.js:511 DEBUG:SCRIPT: account,
dmloader.js:511 { --[[0x1936240]]
dmloader.js:511   devices = { --[[0x19362f8]]
dmloader.js:511     1 = { --[[0x19364c0]]
dmloader.js:511       id = "885faa5b-1666-4ff7-cf6a-376c6cb8e7bf"
dmloader.js:511     }
dmloader.js:511   },
dmloader.js:511   wallet = "{}",
dmloader.js:511   user = { --[[0x1936268]]
dmloader.js:511     lang_tag = "en",
dmloader.js:511     create_time = "2021-03-03T10:17:22Z",
dmloader.js:511     update_time = "2021-03-03T10:17:22Z",
dmloader.js:511     id = "60bb9952-d1bd-4562-851e-647951be0d1b",
dmloader.js:511     username = "biRdZwRvQh",
dmloader.js:511     metadata = "{}"
dmloader.js:511   }
dmloader.js:511 }
dmloader.js:511 DEBUG:SCRIPT: ws://
dmloader.js:511 WARNING:WEBSOCKET: WebSocket OnError
dmloader.js:511 WARNING:WEBSOCKET: WebSocket OnClose
dmloader.js:511 WARNING:WEBSOCKET: PushMessage '' 0 bytes
dmloader.js:511 DEBUG:SCRIPT: EVENT_ERROR: 	
dmloader.js:511 DEBUG:SCRIPT: Unable to connect: 	

Update 2: The specific websocket error:

WebSocket connection to 'ws://' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED

I suspect this is because of the server setup, since it works in the IDE. It’s using Digital Ocean, which might require enabling a websocket connection through a browser/IP specifically.

Update 3: It seems indeed that the issue is the server. When (cheekily) using the xoxo server credentials it works both on desktop and as exported to HTML5. The main difference between the xoxo server and mine is that xoxo uses ssl. Is ssl required for Nakama to run on HTML5?

1 Like

This is my next task to add!


I’m not sure why it works from a desktop build but not from the browser. Perhaps a request header sent by the browser that is somehow interfering.

1 Like

I’ve asked the same question on the Nakama forums. Will report back as and when I get it working. (I also updated the post above).


Nope, I don’t think so. The WebSocket will use wss:// if the port is 443. But you must also check how they page is served. You can’t serve your page from http and access a server at https/wss and vice versa.

Cheers @britzl. All that makes sense, and I’ve made sure:

  • The port is 7350 and config.use_ssl is false
  • The web page is accessed via http and not https
  • The logs show that ws is used and not wss

Stuck at the first hurdle! I’ve been rooting around in server log files to see if I can find a more specific reason why the connection was refused, but so far no luck. Nakama itself doesn’t log anything at all when trying to connect with a browser, leading me to believe the issue might lie with the server itself, rather than with Nakama.

Does the browser developer console give any clues? Your should be able to see the connection attempt and WebSocket upgrade request.

All the output from the browser console is listed in my previous post. It seems the connection is immediately refused in a browser, and magically works when run from Defold with the same code.

I’m more interested in the output from the Network tab of the developer tools in Chrome (or similar for Firefox or Safari). Like this:

1 Like

Oh, that’s really handy! And there is definitely something going on:

The above results are with the same code, the only difference being that it’s connecting to different servers.

Update: Using this technique to get the network log from Chrome gives these events:

Start Time: 2021-03-04 09:25:38.857

t=79026 [st=0] +HTTP_STREAM_JOB  [dt=0]
                --> expect_spdy = false
                --> original_url = ""
                --> priority = "MEDIUM"
                --> source_dependency = 19159 (HTTP_STREAM_JOB_CONTROLLER)
                --> url = ""
                --> using_quic = false
t=79026 [st=0]    HTTP_STREAM_JOB_WAITING  [dt=0]
                  --> should_wait = false
t=79026 [st=0]   +HTTP_STREAM_JOB_INIT_CONNECTION  [dt=0]
                    --> group_id = "pm/"
t=79026 [st=0]     +SOCKET_POOL  [dt=0]
                      --> idle_ms = 0
t=79026 [st=0]        SOCKET_POOL_BOUND_TO_SOCKET
                      --> source_dependency = 19149 (SOCKET)
t=79026 [st=0]     -SOCKET_POOL
                  --> source_dependency = 19157 (URL_REQUEST)
t=79026 [st=0] -HTTP_STREAM_JOB 
Start Time: 2021-03-04 09:25:38.928

t=79097 [st= 0] +REQUEST_ALIVE  [dt=24]
                 --> priority = "LOWEST"
                 --> traffic_annotation = 17188928
                 --> url = "ws://"
t=79097 [st= 0]    NETWORK_DELEGATE_BEFORE_URL_REQUEST  [dt=0]
t=79097 [st= 0]   +URL_REQUEST_START_JOB  [dt=24]
                   --> initiator = ""
                   --> load_flags = 18 (BYPASS_CACHE | DISABLE_CACHE)
                   --> method = "GET"
                   --> network_isolation_key = ""
                   --> privacy_mode = "disabled"
                   --> site_for_cookies = "SiteForCookies: {scheme=http;; schemefully_same=true}"
                   --> url = "ws://"
t=79097 [st= 0]      HTTP_CACHE_GET_BACKEND  [dt=0]
t=79097 [st= 0]     +HTTP_STREAM_REQUEST  [dt=24]
                       --> source_dependency = 19162 (HTTP_STREAM_JOB_CONTROLLER)
t=79121 [st=24]        HTTP_STREAM_REQUEST_BOUND_TO_JOB
                       --> source_dependency = 19163 (HTTP_STREAM_JOB)
t=79121 [st=24]     -HTTP_STREAM_REQUEST
t=79121 [st=24]   -URL_REQUEST_START_JOB
                   --> net_error = -102 (ERR_CONNECTION_REFUSED)
t=79121 [st=24] -REQUEST_ALIVE
                 --> net_error = -102 (ERR_CONNECTION_REFUSED)

#### ws://

Start Time: 2021-03-04 09:25:38.928

t=79097 [st= 0] +HTTP_STREAM_JOB_CONTROLLER [dt=24] --> is_preconnect = false --> url = "ws://" t=79097 [st= 0] HTTP_STREAM_JOB_CONTROLLER_BOUND [ --> source_dependency = 19161 (URL_REQUEST)]( t=79097 [st= 0] +PROXY_RESOLUTION_SERVICE [dt=0] t=79097 [st= 0] PROXY_RESOLUTION_SERVICE_RESOLVED_PROXY_LIST --> pac_string = "DIRECT" t=79097 [st= 0] -PROXY_RESOLUTION_SERVICE t=79097 [st= 0] HTTP_STREAM_JOB_CONTROLLER_PROXY_SERVER_RESOLVED --> proxy_server = "DIRECT" t=79097 [st= 0] HTTP_STREAM_REQUEST_STARTED_JOB [ --> source_dependency = 19163 (HTTP_STREAM_JOB)]( t=79121 [st=24] -HTTP_STREAM_JOB_CONTROLLER
Start Time: 2021-03-04 09:25:38.928

t=79097 [st= 0] +HTTP_STREAM_JOB  [dt=24]
                 --> expect_spdy = false
                 --> original_url = "ws://"
                 --> priority = "LOWEST"
                 --> source_dependency = 19162 (HTTP_STREAM_JOB_CONTROLLER)
                 --> url = "ws://"
                 --> using_quic = false
t=79097 [st= 0]    HTTP_STREAM_JOB_WAITING  [dt=0]
                   --> should_wait = false
t=79097 [st= 0]   +HTTP_STREAM_JOB_INIT_CONNECTION  [dt=24]
                     --> group_id = ""
t=79097 [st= 0]     +SOCKET_POOL  [dt=24]
t=79098 [st= 1]        SOCKET_POOL_BOUND_TO_CONNECT_JOB
                       --> source_dependency = 19164 (WEB_SOCKET_TRANSPORT_CONNECT_JOB)
t=79121 [st=24]     -SOCKET_POOL
                     --> net_error = -102 (ERR_CONNECTION_REFUSED)
                   --> source_dependency = 19161 (URL_REQUEST)
t=79121 [st=24] -HTTP_STREAM_JOB

The last one has a “group id” of Maybe it’s something to do with it trying to connect to port 80 (as per this thread)? Over an ssl connection, the port would default to 443, which may explain why it works over ssl?

Yeah, there might be something going on with port 80 vs 7350.

1 Like

Confirmed! Changing the Nakama server and client to run on port 80 fixes the issue:

This will now allow me to continue developing this test game! :partying_face: Still, let me know if I can help figure out why the default Nakama port doesn’t work.


Happy to hear you managed to solve it by changing port. Perhaps @novabyte knows why port 7350 doesn’t work in an HTML5 build?


@britzl I have a simple question.

I would like to implement online multiplayer mode for my game but ONLY for testing purpose of the LOCAL multiplayer mode. The game is an arcade / action. I am planning to exchange only input and no game state (again, just for testing).

Is this example using Nakama suitable for my needs?

Thank you so much!