Compatible function/rpc calls for an online and offline game?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By nicooo

Hi everyone,

I’ve just started to code an online multiplayer version for my game.
My pretty common concern is that the game should be playable both offline - it already is - and online, using Godot’s High level multiplayer mechanisms.

I’ll take the example of the server spawning a ghost at a random position (it’s a pacman-like game), and transmitting it to 2 players across the network. Playing offline, the game should simply spawn the ghost.

After reading the doc and a bit of testing, I came up with three ways to go:

rpc only

rpc("spawn_ghost", random_position)

where the spawn_ghost method is defined as remotesync (thus called on both the server and the two clients).
This first method is perfectly fine when playing online, but won’t work when playing locally (function is not called).

rpc + local call

rpc("spawn_ghost", random_position)
spawn_ghost(random_position)

where the spawn_ghost method is defined as remote or puppet (thus rpc-called on the two clients only, by the server).
This works and synchronises the three parties, but it raises debugger warnings - hundreds of them - about performing rpc() calls while not being online (seems legit).

rpc + local call, under conditions

if online:
  rpc("spawn_ghost", random_position)
else:
  spawn_ghost(random_position)

(where spawn_ghost is defined as remotesync)
This will work and not raise any debugger warning.

Solution?

This last solution is pretty cumbersome, because it has to be these same 4 lines of code in many many places in the code (for every synchronised action, that is, most of them).

Did I miss a simple way of calling remote/remotesync/puppet functions that works both online and offline, and is along the lines of how things should be done in Godot? (and, say, a maximum of two lines for each call)

Looking forward to your input, many thanks ind advance!

in the Bomber demo that Callnou pointed out, it uses the rpc+local call as in your second example.
However the 2 functions (rpc and local) are defined as master and puppet, havent tried if this is what prevent the debugger to rais warning

puppet func stun():
	stunned = true

master func exploded(_by_who):
	if stunned:
		return
	rpc("stun") # Stun puppets
	stun() # Stun master - could use sync to do both at once

Andrea | 2020-05-11 11:03

I believe master and puppet are only helpers to prevent one player from calling a function it shouldn’t. They also clarify your code as to what methods should be rpc-called by whom.
But they don’t prevent the debugger from raising warnings. :wink:

nicooo | 2020-05-12 18:41

:bust_in_silhouette: Reply From: Calinou

You can still start a high-level multiplayer server even if there’s only one player. This is what the Multiplayer Bomber demo does.

Hi Calinou,
Thank you very much for your answer!

I think I’ll go that way, as it allows for the one-liner rpc("spawn_ghost") if method is set to remotesync.

nicooo | 2020-05-12 18:38

:bust_in_silhouette: Reply From: tavurth

I have found a workaround for this:

func is_online():
    return get_tree().get_network_peer().get_connection_status() == NetworkedMultiplayerPeer.CONNECTION_CONNECTED

func nrpc(node: Node, name: String, args = []):
    """
    Call the `rpc` function on a node if the network is currently connected
    If the network is not connected we'll call the function name anyway
    """
    # Ensure array to simplify logic
    if typeof(args) != TYPE_ARRAY:
	    args = [args]

    if not is_online():
	    return funcref(node, name).call_funcv(args)
    else:
	    return funcref(node, "rpc").call_funcv([name] + args)

Then you can use the following in your nodes, where Network is loaded as a Singleton.

remotesync func say(message: String):
    $VoiceLabel.text = message

remotesync func some_multiarg(message: String, name: String):
    $VoiceLabel.text = message + " " + name

func _input(event: InputEvent):
    if event.is_action_pressed("say_hello"):
        Network.nrpc(self, "say", "hello")

    if event.is_action_pressed("say_hello_to"):
        Network.nrpc(self, "say", ["hello", "some_user"])

See this issue for more information