Check out our latest project ✨ OpenChapter.io: free ebooks the way its meant to be 📖

Scene Safe Multiplayer

An asset by TestSubject06
The page banner background of a mountain and forest
Scene Safe Multiplayer hero image

Quick Information

0 ratings
Scene Safe Multiplayer icon image
TestSubject06
Scene Safe Multiplayer

A collection of high level multiplayer nodes that handshake between the authority and remote peers to prevent sending data to peers that are not yet ready to receive data.

Supported Engine Version
4.1
Version String
0.2.0
License Version
MIT
Support Level
community
Modified Date
1 year ago
Git URL
Issue URL

Godot Scene Safe Multiplayer

A collection of high level multiplayer nodes that handshake between the authority and remote peers to prevent sending data to peers that are not yet ready to receive data.

Purpose

To improve the reliability and eventual consistency of scene transitions when using the high-level multiplayer nodes MultiplayerSpawner and MultiplayerSynchronizer.

When you call get_tree().change_scene_to_packed(...) there is no guarantee what order your peers will arrive on the new scene. There's no way to know which ones are running on faster or slower machines, and may take more or less time to load into the scene. There exists a possibility that a spawner or synchronizer will attempt to send a spawn event or a synchronizer will attempt to begin replication when a remote peer isn't ready to receive the data. This results in the dreaded Node not found errors, and any clients who missed the events will never get them again, unless you write handshaking code. Or you can lean on these nodes which handle that handshaking for you.

This plugin cannot completely prevent these Node not found errors, as there will always be a possibility that a packet is already in flight destined for a peer that has moved to a new scene, that's just the nature of networks. However, the handshaking process used here does guarantee that if and when the peer returns to the scene, it will receive spawn and synchronizer updates again.

Installation

The installation is as easy as it gets - until I can get this added to the Godot Asset Library. Then it will be even easier.

Just put the addons/scene_safe_multiplayer folder into your Godot project, and enable the plugin in your project settings menu:

README image

Usage Recipes

I want to spawn player controlled entities

You can refer to the examples in this repository as you go along.

First, create a SceneSafeMpSpawner, just add a new node and search for it - it should show up in the list:

README image

Our test scene is very small and simple - it's just a spawner, a plank to stand on, and a bucket to place our players into:

README image

Our spawner won't do much on its own, so you'll need a scene with at least one SceneSafeMpSynchronizer in it for the spawner to actually spawn. Creating one is the same as creating the spawner. We need at least one because we have to have at least one synchronizer on a spawned scene that has the is_spawner_visibility_controller property set to true, and has the same authority as the spawner. In our example, we create two - an almost empty one to serve as the visibility controller for the spawner, and one that our players own to synchronize their position and rotation.

README image

The authority owned synchronizer must synchronize something - anything, even if the spawn and sync checkboxes are both disabled. It just has to have something in the replication panel or Godot won't process it, and the plugin won't work.

Now that we have a scene with a spawner and a spawnable scene with a synchronizer set up we need to attach to the SceneSafeMpSpawner's signals to let us know when peers are ready to receive spawns:

extends Node3D

@onready var spawner: SceneSafeMpSpawner = $SceneSafeMpSpawner as SceneSafeMpSpawner;

func _ready():
    spawner.spawn_function = player_spawner;
    
    if is_multiplayer_authority():
        spawner.peer_ready.connect(spawn_player);
        spawner.peer_removed.connect(remove_player);
        spawner.flush_missed_signals();
        multiplayer.peer_disconnected.connect(remove_player);


func spawn_player(peer_id: int):
    var spawn_data = {"id": peer_id};
    $SceneSafeMpSpawner.spawn(spawn_data);
    
    
func remove_player(peer_id: int):
    if $Multiplayer.has_node(str(peer_id)):
        $Multiplayer.get_node(str(peer_id)).queue_free();

We call spawner.flush_missed_signals() because the scene's _ready() function is executed after the SceneSafeMpSpawner's _ready() function. This means that if the multiplayer authority is itself a player in the game, the peer_ready signal would have been emitted before the signal was connected in the scene, so the spawner keeps track of signals that would have been emitted when there were no listeners and saves them for later. The SceneSafeMultiplayer autoload handles the rest for us:

  1. The multiplayer peer confirms the existence of the spawner.
  2. The authority spawns the player's scene locally, including the two synchronizers.
  3. The spawned scene's synchronizer with is_spawner_visibility_controller set to true links with the parent spawner.
  4. At the same time, the synchronizer that the peer owns is registered and a notification from the authority is sent to the peer for later.
  5. When the synchronizer links with the spawner, it sees that there is a confirmed peer on the other end for the authority owned spawner.
  6. The visibility controller synchronizer enables visibility for the confirmed peer, which allows the underlying MultiplayerSpawner to replicate the instance to the peer.
  7. The peer receives their spawned scene and registers the two synchronizers.
  8. The synchronizer owned by the peer picks up on the waiting notification from the spawner authority that the synchronizer on the other end is ready and waiting.
  9. The visibility is enabled from the peer to the multiplayer authority.
  10. All other ready peers simultaneously receive a copy of the spawn and similarly notify the synchronizer's owner that they're ready to receive data.

Note however that there are two signals attached to the remove_player function - multiplayer.peer_disconnected and spawner.peer_removed. The spawner's peer_removed signal does not account for the peer exiting the game abruptly, it only accounts for the spawner calling the _exit_tree() lifecycle method. You still need to handle peer_disconnected to remove the player in the event that the player suddenly disconnects. And in doing so, you must also take care to not to try and delete entities that don't exist - as peer_disconnected doesn't care where the peer was when it disconnected. It may not have been ready in the first place.

What this plugin can't do

These nodes won't allow you to have sets of players freely moving around and existing in different scenes. While these handshakes could allow that, there's a huge amount of work that would have to be done on top of these nodes to handle re-assigning authority of a scene if the current authority leaves, and cleanup if everyone leaves. The example project here allows each peer (including the host/authority) to freely move between scenes - but peers without an authority present are essentially suspended in a void until the authority arrives to provide them with their player spawns. The name of the game here is eventual consistency.

FAQ

Why am I getting errors printed to the console when peers switch scenes?

When a peer changes scenes, like in the example project in this repository, that peer will receive falling-edge errors: on_despawn_receive ... and Node not found root/.../SceneSafeMpSynchronizer. This is normal and expected behavior. Essentialy what happens is the peer removes the associated synchronizer(s), spawner(s), and spawned scenes - while at the same time emitting an RPC that it has done so. However, there will still be in-flight packets destined for those removed synchronizers resulting in the Node not found errors.

Additionally, when the authority of a spawner receives the notification that a peer has broken the handshake the authority will remove the peer from the handshake-based visibility list, which will try to trigger a despawn on the remote peer who just broke the handshake. This is normal behavior from the underlying MultiplayerSpawner instance, but the peer on the other end doesn't have the node anymore. This results in the on_despawn_receive error.

Neither of these errors are harmful to the running of the game, they're no-ops and just there to inform you that something unusual happened - from the perspective of the underlying spawner and synchronizer nodes. If the peer returns to the original scene and handshakes that it's ready - the visibility will be restored and the peer will receive both the spawned scene and any new synchronizer events.

Without the handshaking in this plugin these failures would be leading edge errors that would prevent future packets from flowing if the visibility wasn't managed otherwise.

API Documentation

SceneSafeMpSpawner

Signals

peer_ready ( peer_id: int )

This signal is only emitted on the authority when a peer has confirmed this spawner has been added to the scene. This signal is emitted with one piece of data: an int representing the id of the peer that has confirmed the handhake of the associated spawner. This signal does emit for the authority itself, and does so immediately.

It is possible to receive a spawn signal for a spawner that the authority no longer has, for example if the remote peers are split between two scenes, and a new peer joins a scene that the authority is no longer present in. A bit contrived, and definitely not generally supported, but possible.

peer_removed ( peer_id: int )

This signal is only emitted on the authority when a peer has removed this spawner from their node tree. For example, by transitioning scenes. This is not emitted when a peer is disconnected, only when the handhake for the associated spawner is intentionally broken by the peer. This signal is emitted with one piece of data: an int representing the id of the peer that has confirmed the removal of the associated spawner. This signal does emit for the authority itself, but it's uninteresting if the reason it's being emitted is a scene transition - rather than a spawner cleanup.

Like the peer_ready signal, it is possible to receive an emission for a spawner that is no longer present on the authority.

Method Descriptions

void flush_missed_signals( )

When a SceneSafeMpSpawner is registered in an authority's scene tree, the SceneSafeMultiplayer singleton instructs it to emit ready signals for any peers that are already confirmed. This is sometimes done before the scene itself has become _ready, in which case the signals are not completely missed - but are instead stored until the signals are connected later. Once you've connected the signals, call this function to flush out any signals that may have been missed by the authority while building out the scene tree.

SceneSafeMpSynchronizer

Property Descriptions

bool is_spawner_visibility_controller = false

Controls whether this synchronizer is used to share authority with, and control spawns from a SceneSafeMpSpawner node. If true, then this synchronizer's visibility will be controlled by the authority to manage node spawns between connected peers. One, and only one, synchronizer MUST be marked as a spawner visibility controller in each spawnable scene, or the authority will not be able to send spawned entities to remote peers.

bool public_visibility = false

You MUST NOT use the public_visibility property directly. This will break the handshaking process and undermine all benefits gained from this plugin. Use the set_public_visibility method instead.

Method Descriptions

void set_public_visibility ( visible: bool )

We CANNOT use the public_visibility property directly, because the SceneSafeMpSynchronizer handles two separate streams of visibility: handshake-based visibility and intentional visibility. These two visibility streams are automatically managed and combined into one and sent to the underlying MultiplayerSynchronizer instance. The handshake-based visibility is managed by the SceneSafeMultiplayer singleton.

void set_visibility_for ( peer_id: int, visible: bool )

This is an overridden native method to set the intentional visibility for a specific peer. This is changed to ensure it works correctly with the handshake-based visibility filtering as well. You MUST call this when referencing a cast SceneSafeMpSynchronizer to ensure the correct version is called: ($SceneSafeMpSynchronizer as SceneSafeMpSynchronizer).set_visibility_for( ... ).

SceneSafeMultiplayerManager

This autoload exists as a shared data storage and stable RPC recipient for the two nodes. It should not be directly interacted with, unless you're modifying it for your own purposes.

A collection of high level multiplayer nodes that handshake between the authority and remote peers to prevent sending data to peers that are not yet ready to receive data.

Reviews

0 ratings

Your Rating

Headline must be at least 3 characters but not more than 50
Review must be at least 5 characters but not more than 500
Please sign in to add a review

Quick Information

0 ratings
Scene Safe Multiplayer icon image
TestSubject06
Scene Safe Multiplayer

A collection of high level multiplayer nodes that handshake between the authority and remote peers to prevent sending data to peers that are not yet ready to receive data.

Supported Engine Version
4.1
Version String
0.2.0
License Version
MIT
Support Level
community
Modified Date
1 year ago
Git URL
Issue URL

Open Source

Released under the AGPLv3 license

Plug and Play

Browse assets directly from Godot

Community Driven

Created by developers for developers