Question about web based multiplayer

Godot Version

4.3

Question

Hello Everyone, hope you are fine!

So for a long time, I have been working on an io based game, for crazygames. I done the main mechanics, and tried to add a multiplayer system. It will be like slither io or snake io you may know, which support seamless servers.

I have a basic idea on multiplayer, I tried to add it in my game by different ways. But it seems websocket is not working. If I use ENet for windows, it works but not with websocket (in web). I really have no idea what was going on, when I pressed host, the player spawns with the name of 1 and join triggers for a player named 0 in the web test.

Also, I did not know if WebSocket is ok for this type of online io game. I would like to know more about it from experienced developers on multiplayer. Honestly, I never worked on advanced multiplayer. Do I need to use any third party module?

Thanks in advance!

Hello,
I don’t have much experience with online multiplayer, as the only multiplayer game I’ve made uses FireBase Realtime data base to manage data.
This approach works on all platforms capable of sending HTTPS requests.
You can follow this tutorial to use an approach similar to mine.

I hope this helps.

1 Like

Thanks you so much! It helps a lot. :smile:

I literally confused with godot multiplayer system and spent few days on it, but you solved my problem with just a link xD

I already done the first step, it seems working. I will mark it as solution if everything works perfectly.

1 Like

Its good but there were few problems:

  1. How can I interact with other players (eg hit/hurt)
  2. How can I make server system, like 1st contain 100, 2nd 200…
  3. It will really cost too much performance to read every servers and many players around 50… if I manage to do the above works.
  4. Also its lagging for the movement.
  5. How can I handle player disconnection if he not exit the game properly.
  6. How do I update rotation smoothly in every frames, it gives the data with a delay in firebase

Just an information, its 3d. But not a problem.

Have you fully followed the tutorial?

Here are some methods (maybe not the best ones)

I store all the back-end logic in an Autoload node called DatabaseLinker. This node handles everything related to the server, such as disconnection, connection, and reconnection. It also triggers any changes made in the database through Server-Sent Event.

To solve your issues, you can try this:

  1. You can send player data to Firebase. For each retrieved entry, you can create a new player in your scene. When this instance gets hurt, DatabaseLinker notifies the server.

Your data on the server may look like this:

	"hosts": {
		# The id returned by Firebase when a player create a lobby
		# is used as ID server/lobby id
		"-YRoZcAzsROP8fDJCaZz": {
			"timestamp": 4684646544363157,
			"max_players": 200,
			# The id of the player who first registered
			"host": "-OEnNcAzLRWJ5fDJCaZz",
			"notification": "",
			"expulsed_player": "",
			"players": {
				# The id returned when a player register its data
				# in the data base is used as ID in the game
				"-OEnNcAzLRWJ5fDJCaZz": {
					"name": "player1",
					"id": "-OEnNcAzLRWJ5fDJCaZz",
					"ready": true,
					"requested": {
						"method": "_on_damaged",
						"arguments": {
							"amount": 200,
							"critique": 12,
							"from": "-OEnNcAzLWJ5fJsdCaZz",
						}
					},
					"in_results": false,
					"state": {
						"position": "Vector3(45,86,98)",
						"rotation": "Vector3(0,0,0)",
					}
				}
			}
		}
	}

In the DatabaseLinker script, you can detect when a change is made to requested and then parse the information. In this example, the _on_damaged method will be called with the specified arguments."

  1. You can simply use the ID returned by Firebase (e.g., -YRoZcAzsROP8fDJCaZz) as a host/server ID. Your DatabaseLinker script should retrieve this ID and then send all requests to Firebase within this data ID.

  2. With Server-Sent Events, you don’t need to read every server or the entire server data, only the specific properties you want to trigger. You can listen to data, as described in part 2 of the tutorial. Scroll down until you see ‘Server-Sent Events’."

  3. It might be because you’re fetching the server data every frame.You can implement a function that only triggers necessary changes and updates your scene accordingly.

Take a look at what I use:

var _players_url: String = "%s/hosts/%s/players.json" % [DATA_BASE_URL, "%s"]:
	set(value):
		_players_url = value
		_remote_data_to_listen["players"]["url"] = _players_url

var _expulsed_player_url: String = "%s/hosts/%s/expulsed_player.json" % [DATA_BASE_URL, "%s"]:
	set(value):
		_expulsed_player_url = value
		_remote_data_to_listen["expulsed_player"]["url"] = _expulsed_player_url

var _vars_depending_on_host_id := PackedStringArray([
	"_players_url", "_host_name_url", "_expulsed_player_url",
	"_notification_url", "_game_data_url", "_log_url", "_host_open_prop_url",
])

## Syntax: {"url": (...), "func": (...)}
## func -> function to call if changes are made in data base
var _remote_data_to_listen := {
	"players": {
		"func": "_on_players_changed",
	},
	"host_name": {
		"func": "_on_host_name_changed",
	},
	"expulsed_player": {
		"func": "_on_player_expulsed",
	},
	"notification": {
		"func": "_on_database_notified",
	},
	"log": {
		"func": "_on_in_game_log_updated",
	},
}

func _setup_tcp_stream() -> StreamPeerTCP:
	var tcp: StreamPeerTCP = StreamPeerTCP.new()
	var error = tcp.connect_to_host(DATA_BASE, HTTPS_PORT)
	assert(error == OK)
	tcp.poll()
	var status = tcp.get_status()
	
	while status != StreamPeerTCP.STATUS_CONNECTED:
		await get_tree().process_frame
		tcp.poll()
		status = tcp.get_status()
	
	return tcp


func _setup_tls_stream(tcp: StreamPeerTCP) -> StreamPeerTLS:
	var stream: StreamPeerTLS = StreamPeerTLS.new()
	var error = stream.connect_to_stream(tcp, DATA_BASE)
	assert(error == OK)
	stream.poll()
	var status = stream.get_status()
	
	while status != StreamPeerTLS.STATUS_CONNECTED:
		await get_tree().process_frame
		stream.poll()
		status = stream.get_status()
	
	return stream


func _start_sse_stream(stream: StreamPeer, url: String = HOSTS_URL) -> void:
	var request_line: String = "GET %s HTTP/1.1" % url
	var headers: Array = [
		"Host: %s" % DATA_BASE,
		"Accept: text/event-stream",
	]
	var _request: String = ""
	_request += request_line + "\n" # request line
	_request += "\n".join(headers) + "\n" # headers
	_request += "\n" # empty line
	stream.put_data(_request.to_ascii_buffer())


func _read_stream_response(stream: StreamPeer) -> String:
	stream.poll()
	var available_bytes: int = stream.get_available_bytes()
	
	while available_bytes == 0:
		await get_tree().process_frame
		stream.poll()
		available_bytes = stream.get_available_bytes()
	
	return stream.get_string(available_bytes)


class EventData:
	var type: String
	var data: Dictionary


func _parse_event_data(str_event: String) -> EventData:
	var event_lines: Array = str_event.split("\n")
	
	if event_lines.size() != 2: return null
	
	var event_type_line = event_lines[0]
	
	if !event_type_line.begins_with(EVENT_TYPE_PREFIX):
		return null
	
	var event_data_line = event_lines[1]
	
	if !event_data_line.begins_with(EVENT_DATA_PREFIX):
		return null
	
	var event_type_str = event_type_line.substr(EVENT_TYPE_PREFIX.length())
	var event_data_str = event_data_line.substr(EVENT_DATA_PREFIX.length())
	
	var event_data_json = Game.get_parsed_json_data(event_data_str)#JSON.parse_string(event_data_str)
	
	if event_data_json == null:
		event_data_json = {}
	
	var event: EventData = EventData.new()
	event.type = event_type_str
	event.data = event_data_json
	
	return event


func _parse_response_event_data(response: String) -> Array[Dictionary]:
	var response_parts: PackedStringArray = response.replace("\r", "").split("\n\n")
	var event_data: Array[Dictionary] = []
	
	for response_part: String in response_parts:
		var event = _parse_event_data(response_part)
		
		if event:
			print(event.data)
		
		if event == null or event.type != "put": continue
		
		event_data.append(event.data)
	
	return event_data


func _start_listenning_cur_host() -> void:
	stop_listenning_cur_host = false
	
	for _data: String in _remote_data_to_listen:
		# Doesn't wait func to fisnish in order to allow others "_data" to be processed
		_listen_remote_data(_data)


func _listen_remote_data(_data: String) -> void:
	var tcp: StreamPeerTCP = await _setup_tcp_stream()
	var stream: StreamPeerTLS = await _setup_tls_stream(tcp)
	_start_sse_stream(stream, _remote_data_to_listen[_data]["url"])
	#await _read_stream_response(stream)
	
	while not stop_listenning_cur_host:
		var response = await _read_stream_response(stream)
		var events: Array[Dictionary] = _parse_response_event_data(response)
		
		if is_hoster:
			print("events: ", events)
		
		if (
			events.is_empty()
			or events[0].is_empty()
			or events[0].get("data") == null
			or (typeof(events[0]["data"]) == TYPE_STRING)# and events[0]["data"] == "")
			or (typeof(events[0]["data"]) == TYPE_DICTIONARY and events[0]["data"].is_empty())
		):
			continue
		
		for event: Dictionary in events:
			call(_remote_data_to_listen[_data]["func"], event["data"])

  1. You can trigger a game exit using the DatabaseLinker script and send the information to other players. In your player script, you can also ping the server every 10 seconds to check if the player is still connected. If no data is received after a certain delay, the player is considered disconnected.

  2. Same as response 3.

Additionally, you can check out this demo project. It’s not mine, and I use a very different method for handling data with Firebase.

1 Like

I know it can be a bit tricky and requires a lot of code :sweat_smile:. I don’t see any advantage apart from having full control over your data and the ability to use Firebase Security Rules to secure it.

You should use existing services like Nakama or Play room.

Thank you for this explanation and sharing your method! I will check it soon.
Playroom kits seems more easy, I will also check this.

Play room kit is not for my game. Otherwise in case of using firebase real time database, it seems like it lagging, not running smoothly. Maybe because the files are first downloaded, and then read from it with a delay, not at 60 fps (thats actually not possible btw).

So I am still looking for a better way to make the multiplayer io game.

1 Like