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.
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.
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:
Running...
dmloader.js:514
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 http://142.93.41.41:7350/v2/account/authenticate/device?create=true
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 http://142.93.41.41:7350/v2/account
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://142.93.41.41:7350/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2MGJiOTk1Mi1kMWJkLTQ1NjItODUxZS02NDc5NTFiZTBkMWIiLCJ1c24iOiJiaVJkWndSdlFoIiwiZXhwIjoxNjE0NzY2NzAyfQ.aHYo8HOK07EPUYNdBF7iULLoTvM0FR57V244zrrs1UY
dmloader.js:511 WARNING:WEBSOCKET: STATE_CREATE -> STATE_CONNECTING
dmloader.js:511 WARNING:WEBSOCKET: WebSocket OnError
dmloader.js:511 WARNING:WEBSOCKET: STATE_CONNECTING -> STATE_DISCONNECTED
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:
dmloader.js:511 DEBUG:SCRIPT: EVENT_DISCONNECTED:
Update 2: The specific websocket error:
WebSocket connection to 'ws://142.93.41.41/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI2OWI2ZGMxMC0zODE3LTQ3ZTItOWFlMS1kYTkzODJiZjY2NjQiLCJ1c24iOiJudkdCVHVKdWpsIiwiZXhwIjoxNjE0NzY4MzIzfQ.tSGf0HKb59dMgYjIHF7GnVPrjmB3vadXN3SaDIEbles' 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?
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.
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:
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:
19160: HTTP_STREAM_JOB
http://142.93.41.41:7350/
Start Time: 2021-03-04 09:25:38.857
t=79026 [st=0] +HTTP_STREAM_JOB [dt=0]
--> expect_spdy = false
--> original_url = "http://142.93.41.41:7350/"
--> priority = "MEDIUM"
--> source_dependency = 19159 (HTTP_STREAM_JOB_CONTROLLER)
--> url = "http://142.93.41.41:7350/"
--> 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]
t=79026 [st=0] TCP_CLIENT_SOCKET_POOL_REQUESTED_SOCKET
--> group_id = "pm/142.93.41.41:7350"
t=79026 [st=0] +SOCKET_POOL [dt=0]
t=79026 [st=0] SOCKET_POOL_REUSED_AN_EXISTING_SOCKET
--> idle_ms = 0
t=79026 [st=0] SOCKET_POOL_BOUND_TO_SOCKET
--> source_dependency = 19149 (SOCKET)
t=79026 [st=0] -SOCKET_POOL
t=79026 [st=0] -HTTP_STREAM_JOB_INIT_CONNECTION
t=79026 [st=0] HTTP_STREAM_JOB_BOUND_TO_REQUEST
--> source_dependency = 19157 (URL_REQUEST)
t=79026 [st=0] -HTTP_STREAM_JOB
19161: URL_REQUEST
ws://142.93.41.41/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIzMTZlOGNmYS0xOWUwLTRiYzYtYTBiOC05NmE4ZTEzMzE0NjEiLCJ1c24iOiJRUUpwVkVUSFJiIiwiZXhwIjoxNjE0ODQ5OTk5fQ.9SiTdL6fFjrsO2dovSTDSFOqkws6CFxWkDpw__cQiKY
Start Time: 2021-03-04 09:25:38.928
t=79097 [st= 0] +REQUEST_ALIVE [dt=24]
--> priority = "LOWEST"
--> traffic_annotation = 17188928
--> url = "ws://142.93.41.41/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIzMTZlOGNmYS0xOWUwLTRiYzYtYTBiOC05NmE4ZTEzMzE0NjEiLCJ1c24iOiJRUUpwVkVUSFJiIiwiZXhwIjoxNjE0ODQ5OTk5fQ.9SiTdL6fFjrsO2dovSTDSFOqkws6CFxWkDpw__cQiKY"
t=79097 [st= 0] NETWORK_DELEGATE_BEFORE_URL_REQUEST [dt=0]
t=79097 [st= 0] +URL_REQUEST_START_JOB [dt=24]
--> initiator = "http://bullet_rain.totebo.com"
--> load_flags = 18 (BYPASS_CACHE | DISABLE_CACHE)
--> method = "GET"
--> network_isolation_key = "http://totebo.com http://totebo.com"
--> privacy_mode = "disabled"
--> site_for_cookies = "SiteForCookies: {scheme=http; registrable_domain=totebo.com; schemefully_same=true}"
--> url = "ws://142.93.41.41/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIzMTZlOGNmYS0xOWUwLTRiYzYtYTBiOC05NmE4ZTEzMzE0NjEiLCJ1c24iOiJRUUpwVkVUSFJiIiwiZXhwIjoxNjE0ODQ5OTk5fQ.9SiTdL6fFjrsO2dovSTDSFOqkws6CFxWkDpw__cQiKY"
t=79097 [st= 0] NETWORK_DELEGATE_BEFORE_START_TRANSACTION [dt=0]
t=79097 [st= 0] HTTP_CACHE_GET_BACKEND [dt=0]
t=79097 [st= 0] +HTTP_STREAM_REQUEST [dt=24]
t=79097 [st= 0] HTTP_STREAM_JOB_CONTROLLER_BOUND
--> 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] URL_REQUEST_DELEGATE_RESPONSE_STARTED [dt=0]
t=79121 [st=24] -REQUEST_ALIVE
--> net_error = -102 (ERR_CONNECTION_REFUSED)
#### 19162: HTTP_STREAM_JOB_CONTROLLER
#### ws://142.93.41.41/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIzMTZlOGNmYS0xOWUwLTRiYzYtYTBiOC05NmE4ZTEzMzE0NjEiLCJ1c24iOiJRUUpwVkVUSFJiIiwiZXhwIjoxNjE0ODQ5OTk5fQ.9SiTdL6fFjrsO2dovSTDSFOqkws6CFxWkDpw__cQiKY
Start Time: 2021-03-04 09:25:38.928
t=79097 [st= 0] +HTTP_STREAM_JOB_CONTROLLER [dt=24] --> is_preconnect = false --> url = "ws://142.93.41.41/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIzMTZlOGNmYS0xOWUwLTRiYzYtYTBiOC05NmE4ZTEzMzE0NjEiLCJ1c24iOiJRUUpwVkVUSFJiIiwiZXhwIjoxNjE0ODQ5OTk5fQ.9SiTdL6fFjrsO2dovSTDSFOqkws6CFxWkDpw__cQiKY" t=79097 [st= 0] HTTP_STREAM_JOB_CONTROLLER_BOUND [ --> source_dependency = 19161 (URL_REQUEST)](https://netlog-viewer.appspot.com/#events&s=19161) 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)](https://netlog-viewer.appspot.com/#events&s=19163) t=79121 [st=24] -HTTP_STREAM_JOB_CONTROLLER
19163: HTTP_STREAM_JOB
ws://142.93.41.41/
Start Time: 2021-03-04 09:25:38.928
t=79097 [st= 0] +HTTP_STREAM_JOB [dt=24]
--> expect_spdy = false
--> original_url = "ws://142.93.41.41/"
--> priority = "LOWEST"
--> source_dependency = 19162 (HTTP_STREAM_JOB_CONTROLLER)
--> url = "ws://142.93.41.41/"
--> 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]
t=79097 [st= 0] TCP_CLIENT_SOCKET_POOL_REQUESTED_SOCKET
--> group_id = "142.93.41.41:80"
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)
t=79121 [st=24] -HTTP_STREAM_JOB_INIT_CONNECTION
t=79121 [st=24] HTTP_STREAM_JOB_BOUND_TO_REQUEST
--> source_dependency = 19161 (URL_REQUEST)
t=79121 [st=24] -HTTP_STREAM_JOB
The last one has a “group id” of 142.93.41.41:80. 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.
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! 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!