A function acts differently when I call it via RPC than when I call locally

Godot Version

Godot 4.2


Hey, all!
I have a problem that I hope somebody can help me with. I have tested absolutely everything I can think of, but I simply cannot find out what I’m doing wrong!

TLDR: When I call a function locally, everything works fine. If I call it through an RPC, it doesn’t work right at all.

I have a character that moves around the map and can kill other characters (duplicates of itself)
To start with, I have a boolean variable (is_vampire), that I create and assign to ‘true’ at the top of the script.
Second, I have a blank variable (animated_sprite) that I create at the top of the script.

In the ready function, I have an ‘if’ statement checking to see if the ‘is_vampire’ boolean is true. If this is the case, I set the empty ‘animated_sprite’ variable to be the child node Vampire_Sprite.

In another function (kill), I have use RPC_local to call the ‘die’ function on another character.

In this ‘die’ function, I first use an ‘if’ statement to see if the ‘is_vampire’ is true. If so, I tell the Vampire_Sprite node (through the previously declared and assigned animated_sprite variable) to play a dying animation and print a message to the console.
When I call this node through RPC, it claims that the animated_sprite variable is completely empty.
When I call the ‘die’ function locally (through a button press), everything works as expected.

I tried adding in a line of code into the ‘die’ function - ‘animated_sprite = Vampire_Sprite’. This removed the error message, but the animation still does not play.

I tried to tell the Vampire_Sprite to play the animation directly - $Vampire_Sprite.play(“die”).

I tried calling a different animation - no luck.

Here’s the essentials of the code - please let me know if anybody needs to see more of it.

@export var is_vampire = true
var animated_sprite

func _ready():
    if $MultiplayerSynchronizer.get_multiplayer_authority() ==      multiplayer.get_unique_id():
    if is_vampire:
        animated_sprite = $Vampire_Sprite

func kill():
    if cooldown_active == false and killable_player != "":
        rpc_id(int(killable_player), "die")

@rpc("any_peer", "reliable")
func die():
    if is_vampire:

        # subtract a life
        lives = lives - 1

    # if vampire is totally dead
        if lives <= 0: 
        	action_in_progress = true
	        #animated_sprite = $Vampire_Sprite
	        print("Vampire's been killed!")

    # if vampire can respawn
        elif lives > 0:
	        action_in_progress = true
	        #animated_sprite = $Vampire_Sprite
	        print("Vampire's lost a life: now has " + str(lives))

Hi @whitecolumnsdev,

This does not seem trivial even at a third look :smiley: Have you tried to debug the process step by step to see what happens?

Also, are you testing it with a single peer? Or is this an issue of different things happening on different peers? If the latter, some data may be slightly different on some peers, which often means that not the same logic has run on every peer.

If you’re testing with a single peer ( e.g. running only one game instance ), then I’d debug _ready, kill and die step by step, to see if/where the animated_sprite variable loses its value. Maybe we can figure out what’s the issue from there.

Also, not necessarily related, but this line is a bit confusing to me:


Are you spawning the player with the peer ID as name?

Also for checks like this:

if $MultiplayerSynchronizer.get_multiplayer_authority() ==      multiplayer.get_unique_id()

You can use is_multiplayer_authority(), it’s a bit shorter and does the same thing :slight_smile:

Hey, @elementbound!
Thanks so much for answering!
I am using 2 separate game instances with a copy of the identical node on each instance. I’ve tested every single line of code I can think of, but I can’t find the problem. I even tried using the rpc to call a separate function, which then called the original one: still didn’t work.

I tried tying the ‘die’ function in to a keyboard button, and everything worked perfectly.
When the function is called remotely, everything else works as planned except the animation. I get a game-stopping warning saying that the animated_sprite variable is empty. I tried adding a line of code that manually sets the variable to the node path. This removed the warning, but the animation still did not play.

I am spawning the player with the multiplayer number as the name.
Thanks for the tip on the shorter version of the authority check - I got this one from a tutorial (and it did seem a but over-complicated).

Any suggestions would be VERY much appreciated - I have been working on the problem for days, with no result. Thank you very much! :slight_smile:

Would you be ok with submitting a minimal project that can reproduce this issue? Maybe I or someone can figure it out from that.

Your problem is here in the ready function:
if $MultiplayerSynchronizer.get_multiplayer_authority() == multiplayer.get_unique_id():
if is_vampire:
animated_sprite = $Vampire_Sprite

The animated sprite is only assigned if the machine is the authority, the puppets will not get this assignment.

Hey, @pennyloafers!
Thanks a lot for responding! I really appreciate it. :grinning:
This sounds promising, but I don’t think it’s the solution.
I did a quick test where I set the variable without the preceding if statement, but the animation still did not play. I did not get the ‘empty variable’ warning though, but I didn’t get it when I added the animated_sprite setter in the rpc function either.
I am also getting the exact error when I use a local variable set in the ‘die’ function.

When I call the ‘die’ function locally, everything works exactly as expected - the animation plays on both instances, the life counter changes, etc.
When I call it through rpc, the animation does not play, but everything else works.

Thanks a lot for your help - I’ll definitely keep your tip in mind in the future!

I’ll see if I can figure out how to upload my project - this is my first time using the forum, so it might take me a bit. Thanks!

1 Like

No problem!

If you are using the multiplayer spawner, it will not sync the state of your scene. This gets a little tricky to talk about.

I assume you are not doing a central server only authority and making a peer to peer network.

If the animation player is static to the player scene, it wouldn’t make sense that it didn’t play or load. I don’t see an issue with your code other then the initial vampire animation not being added to puppet/peer instances of the scene.

If the animation player is dynamically added to the player. That is another problem. Since it will not by sent over to the peers by the root spawner. You will need a child spawner that spawns the animation object and signals that notify the player object that a animation scene was added.

I think there is much context missing from the example given. Like killable_player, only that players machine is told to die and no one else’s. Maybe that is the issue? You would want to broadcast that to all peers.

1 Like

Hey, @pennyloafers!
I am making a peer-to-peer network, but I am not currently using a multiplayer spawner.

The AnimatedSprite2D is inherent to the player scene (added in the editor, not during gameplay).

As for killable_player, that is the multiplayer unique id of the player being killed, which I am using in an rpc_id to call the die function on the targeted player.

The AnimatedSprite2D has its animation and current frame synced across all peers through a MultiplayerSynchronizer node.

Would you like me to post the entire script?

@whitecolumnsdev, I could take a look, I’m working on a central auth server myself and I think have gotten over the mind hurdle of juggling the network state and some of Godot’s quirks. Although I’m working on my first game, which is in 3d, and have barely touched the animation stuff let alone most of the 2d nodes.

All right - here’s the full script for the project:

This script is used as a global autoload to keep track of player data.

extends Node

var Players = {}

Here’s the script for the network start. The scene has 3 buttons: Host, Join, and Start.

extends Control

@export var network = ""
@export var PORT = 9999
var max_players = 10
var peer

# Called when the node enters the scene tree for the first time.
func _ready():

func server_disconnected():
	print("Server Disconnected")

func peer_connected(id):
	print("Player Connected " + str(id))

func peer_disconnected(id):
	print("Player Disconnected " + str(id))
	var players = get_tree().get_nodes_in_group("Player")
	for i in players:
		if i.name == str(id):

func connected_to_server(): 
	print("Connected to server!")
	# rpc the host with the name and unique id of the player who joined
	Send_Player_Information.rpc_id(1,"", multiplayer.get_unique_id())

func connection_failed(): print("Couldn't connect!")

# update the player information (called from rpc when a new player joins the server)
func Send_Player_Information(name, id): 
	if not GameManager.Players.has(id):
			"name" : name,
			"id" : id
	if multiplayer.is_server():
		for i in GameManager.Players:
			Send_Player_Information.rpc(GameManager.Players[i].name, i)

func host_game():
	peer = ENetMultiplayerPeer.new()
	# port to listen on, max number of players
	var error = peer.create_server(PORT, max_players)
	if error != OK:
		print("cannot host " + error)
	# send host name and id
	Send_Player_Information("", multiplayer.get_unique_id())
	print("waiting for players")

func join_game():
	peer = ENetMultiplayerPeer.new()
	# first the IP address, then the port to listen on (1--9999)
	peer.create_client(network, PORT)

# everybody gets this rpc, including the player who calls it
@rpc("any_peer", "call_local")
func start_game(): 
	var scene = load("res://level.tscn").instantiate()

func _on_start_button_pressed():

Here’s the script for the game level:

extends Node2D

@export var PlayerScene : PackedScene

func _ready():
	var index = 0
	for i in GameManager.Players:
		var CurrentPlayer = PlayerScene.instantiate()
		CurrentPlayer.name = str(GameManager.Players[i].id)
		for spawn in get_tree().get_nodes_in_group("SpawnLocations"):
			if spawn.name == str(index):
				CurrentPlayer.global_position = spawn.global_position
		index += 1

And finally, here’s the full player script. Please note that many functions are commented out, as they are still in progress:

extends CharacterBody2D


@onready var kill_cooldown = $Kill_Cooldown
@onready var kill_button = $UI_Vampire/VBoxContainer/Kill
@onready var pivot = $pivot

### SIGNALS ###
# used to tell if an object in the kill or vision zone is a player or not
signal player

@export var is_vampire = true

@export var is_human = false

@export var is_ghost = false

# if the vampire can kill or not
var cooldown_active = false

# number of lives the player has
var lives = 3

# blood points that can be spent on buffs TODO
var blood_points = 0

# whether the player is hiding or not TODO
var is_hiding = false

var animated_sprite

var killable_player = ""

# character speed
var speed = 100

var walking = false
var action_in_progress = false

# Called when the node enters the scene tree for the first time.
func _ready():
	if is_vampire:
		animated_sprite = $Vampire_Sprite
	if $MultiplayerSynchronizer.get_multiplayer_authority() == multiplayer.get_unique_id():
# shows the vampire UI, connects the kill button, connects the kill cooldown timer, sets number of lives		
		if is_vampire:
			lives = 3
			animated_sprite = $Vampire_Sprite

# shows the human UI
		if is_human:
			lives = 1
			animated_sprite = $Human_Sprite

		$Camera2D.enabled = true

func get_input():
	if $MultiplayerSynchronizer.get_multiplayer_authority() == multiplayer.get_unique_id():
		var input_dir = Input.get_vector("left", "right", "up", "down")

		if Input.is_action_pressed("left"):
			animated_sprite.flip_h = true
			pivot.rotation_degrees = 0
			walking = true
		elif Input.is_action_pressed("right"):
			animated_sprite.flip_h = false
			pivot.rotation_degrees = 180
			walking = true
		elif Input.is_action_pressed("down"):
			pivot.rotation_degrees = 270
			walking = true
		elif Input.is_action_pressed("up"):
			pivot.rotation_degrees = 90
			walking = true
		else: walking = false
#			animated_sprite.play("idle")
		if Input.is_action_pressed("down") and Input.is_action_pressed("left"):
			pivot.rotation_degrees = 315
		elif Input.is_action_pressed("down") and Input.is_action_pressed("right"):
			pivot.rotation_degrees = 225
		elif Input.is_action_pressed("up") and Input.is_action_pressed("left"):
			pivot.rotation_degrees = 45
		elif Input.is_action_pressed("up") and Input.is_action_pressed("right"):
			pivot.rotation_degrees = 135
		#if walking == false and action_in_progress == false:
		velocity = input_dir * speed

func _physics_process(delta):
	if $MultiplayerSynchronizer.get_multiplayer_authority() == multiplayer.get_unique_id():

func _input(event):
	#if $MultiplayerSynchronizer.get_multiplayer_authority() == multiplayer.get_unique_id():
	if Input.is_action_pressed("test"): 

func kill():
	if cooldown_active == false and killable_player != "":
		action_in_progress = true
		cooldown_active = true
		kill_button.disabled = true
		rpc_id(int(killable_player), "die")

# reset kill cooldown when you feed on a body
func reset_cooldown():
	cooldown_active = false
	kill_button.disabled = false

func feed():
	kill_cooldown.time_left = 0
	#action_in_progress = true TODO

# character death
@rpc("any_peer", "reliable")
func die():
	if is_vampire: 
		# subtract a life
		lives = lives - 1
		# if vampire is totally dead
		if lives <= 0: 
			action_in_progress = true
			#animated_sprite = $Vampire_Sprite
			print("Vampire's been killed!")
			# insert victory here! TODO

		# if vampire can respawn
		elif lives > 0:
			action_in_progress = true
			#animated_sprite = $Vampire_Sprite
			print("Vampire's lost a life: now has " + str(lives))
			# respawn at the sarcophagus! TODO

	if is_human: 
		lives -= 1
		if lives >= 0: print("human's dead!")
		action_in_progress = true
		# turn into ghost and leave corpse behind here! TODO

func hide_self(): 
	is_hiding = true
	# insert hiding code here TODO !

# if another player enters the kill zone
func kill_zone_entered(character): 
	if character.has_signal("player") and character != self:
		print("this player entered the kill zone" + str(character.name))
		killable_player = str(character.name)

# if a player exits the kill zone
func kill_zone_exited(character): 
	if character.has_signal("player") and character != self:
		print("this player exited the kill zone" + str(character.name))
		killable_player = ""

#func vision_zone_entered(character): 
	#if character.has_signal("player") and character != self:
		#print("this player entered the vision zone" + str(character.name))

#func vision_zone_exited(character): 
	#if character.has_signal("player") and character != self:
		#print("this player exited the vision zone" + str(character.name))

# emitted when any animated_sprite ends an animation
func animated_sprite_animation_timeout():
	action_in_progress = false

Here’s the node layout for the player. Only the Vampire UIs and AnimatedSprite2Ds are used.


I’ll add a picture of what properties are synced in the MultiplayerSynchronizer in another post - the forum won’t let me post more than one in a reply yet.

I hope this is enough - please let me know if I need to add anything.
Thanks so much, @pennyloafers!

I think the main issue is your kill function rpc call to die.

I think your intention is to kill the opponent if they are in the kill zone. But the code calls the die function of the player trying to kill the opponent on the remote computer with that multiplayer_id killable_player.

1: you should save the reference of the killable_player when they enter the killzone and not just the name.

2: when the kill button is pressed, you should kill the opponent by calling: killable_player.die.rpc(killable_player.name.to_int()) (there may be another syntax for this but I think this should work)

3: optional but probably necessary depending on your syncing strategy: we can add a “call_local” sync mode to the rpc attribute of die (). This will call die on the local machine for the killable player as well as sending to that killable player machine. ( you may not need this since you sync a lot of properties anyway from the authority of the node)

The key thing is that the rpc will only target the function that is relative to the node path of the object it was called on. In your case you are killing the player clicking the kill button only on the remote machine. And since you are syncing the animation from the master it probably gets turned off immediately because your local player didn’t die.

1 Like

To put simply, you are currently calling

self.die.rpc(killable_player_id) # sends die to self on remote player machine.

When you should be calling

Killable_player_node.die.rpc(killable_player_id) # sends die to killable player node on remote player machine.


@pennyloafers This works perfectly! Thank you so much - my project now works perfectly! I owe you so much! :grinning:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.