Reparenting scenes under MultiplayerSpawners

Godot Version

4.7

Question

Hello,

I’m mainly looking for a sanity check regarding reparenting scenes under MultiplayerSpawners.

As an example project, imagine a multiplayer card game like dominion, hearthstone or mtg. Playing cards are moved between play areas such as a deck, a player’s hand, the battlefield, discard pile, etc. I thought it made sense to lay out my scene tree where I have a MultiplayerSpawner under each of these play areas. Each spawner had its own “Cards” spawn-path node to spawn the cards in that area. This is nice because each of these areas have their own position and I need to access them under a predictable node path. I figured I could reparent cards between these areas as the game progressed.

Attempting to reparent spawned nodes however causes serious errors immediately despite identical auto-spawn lists. A commonly suggested workaround is to duplicate the card, add it to the new play area, and delete the old card. After all, that is pretty much all that reparenting does anyhow. Though cumbersome, this worked well enough at first.

As my project grew however, using this pattern became uglier and more error-prone. Unwanted lifecycle signals and functions fired causing all sorts unintuitive bugs and required goofy workarounds. Race conditions between clients arose around when old cards are actually deleted and duplicates appeared. Add in issues like potential network lag, client disconnects/reconnects, undo-redos and this reparenting workaround becomes very problematic and very costly to refactor.

I think a better approach is to have a single spawner higher up the scene tree to handle all cards. Moving between play areas can be tracked by Groups or by custom card properties. Yes, positioning and scene tree navigation needs more manual handling, but it avoids far worse issues. I’m surprised this sort of issue doesn’t come up more often since I would think that moving spawned nodes around the scene tree would be very common. I think it would have helped if the docs or somebody had said something like the following:

It is generally advisable to use only one MultiplayerSpawner for each class of spawned scene if you intend on moving instances of those scenes around in the scene tree.

Am I missing something? Is there another more elegant solution? I hope this doesn’t come off as a complaint, I’ve been very happy with Godot so far.

Thanks!

There are long-standing issues about this – Issues · godotengine/godot · GitHub

Would @rpc calls to add and remove cards from piles be enough and just avoid the Spawner/Synchronizer approach?

Yeah, I’m definitely not alone running into this problem. Also, good point, rpcs would be another legit approach. It would still be a lot of legwork though and you’d still get unwanted lifecycle signals. I still lean towards one big spawner and maybe adding some kinda PlayZoneChanger card component to handle stuff like position and movement animations. I havent used Groups yet, but that sounds like a good way to get references of all the nodes in a particular play zone. Thanks.

The @rpcs can reparent without trouble.

The @rpcs can reparent without trouble.

I guess what I meant was that the cards would still have to be duplicated (thus causing init and ready to fire), queue_free’d, and added back to the tree (thus triggering stuff like child_entered). All of this is conceptually nonsensical from a high level perspective since I’m just moving a card around the board.

I feel like a lot of more typical non-card games avoid this because they’re using CharacterBody2D or something similar to move dynamic nodes around. They arent likely creating/destroying these nodes to get them into new play zones. This is another reason that pushes me towards some kind of solution that uses a component to handle movement.

Oh, sorry I misunderstood, yeah, I guess if I remove spawners and syncers entirely, then using rpcs to reparent would not cause init and ready to fire. It would mean manually creating and syncing the cards, which would be costly.