How to Make Instanced Dungeons in Multiplayer?

Godot Version

4.6

Question

Hello, I currently have a very basic 3D mutliplayer game: You can move around world, take a portal to move to the dungeon and there’s also a portal in dungeon you can take to move back to world.

Here’s the current setup from the view of the host (the server/host is always player 1):

At the start, someone hosts a game and we add World and Dungeon nodes via add_child() to the WorldSpawner which is a MultiplayerSpawner node. If other players now join this game, they will automatically receive these nodes. Note that each, World and Dungeon have their own MultiplayerSynchronizer nodes, where the public_visbility of World is turned on but for Dungeon its turned off. This will tell the WorldSpawner to only spawn World initially. So a player connects, he gets World but not Dungeon.

Now if a non-host player takes the portal, we simply toggle the visibility of World and Dungeon via their MultiplayerSynchronizer nodes. Note that this isn’t the same as toggling .visible property of a node but it tells the spawner which nodes to spawn or despawn for the non-host players. We can do this, because the players only need to care about the nodes in their world they currently are in. E.g. if they are in World, their client doesn’t have to run any simulation logic for e.g. the Dungeon. So this all works fine.

For the host, we have the problem, that we can’t just despawn or spawn the nodes we need. We always need to basically run everything all the time because the host is the main authority running everthign for everyone basically. So e.g. if the host player is in World, we still need to run the logic for the AI for the mobs in Dungeon. That’s why you can see both nodes, World and Dungeon in the screenshot above (it’s from the pov of the host).

Now this comes with a problem: Both nodes, World and Dungeon exist in the same World3D given by our root viewport root. Furthermore, these two scenes World and Dungeon overlap in the global coordinate system of the roots World3D.

Since we can’tdespawn or spawn nodes for the host but need all of them all the time we solve it by toggling the .vibisle property of the nodes. So if the host is in World, we hide Dungeon and vice versa. The problem with this is that since we are in the same World3D, we share the same physics space, so we get issues when it comes to collision etc.

And this is the main problem: We need to decouple the physics space between World and Dungeon. This isn’t a problem for the non-host players simply because we only run one of them at a time but for the host we run both simultaneously.

The question is, how do I solve this? I can think of three “solutions”.

  1. I simply add an offset of e.g. y=10000 (random value) to each node, so they are physically separated. The still share the same physics space but in theory, we shouldn’t run into collision issues. Unless we might have projectiles or some other funny things but you can see, this probably isn’t a nice solution. Depends a bit on the game.
  2. We set collision masks/layer for each World and Dungeon differently and just toggle it on the player, when we take the portal. I can imagine though, that this will get complicated but that’s a bit of a guess since I’m still a noob in Godot.
  3. We might be able to use different World3D instances for each node. From the docs we know that “A resource that holds all components of a 3D world, such as a visual scenario and a physics space.” but I’m not sure if I can actually use it like that. I guess this would involve using SubViewports but again, I’m very unsure about this and the implications. I’m very interested in this solution though. Now assume I can actually use that, what would be the best way to do that? E.g. can I just run both worlds and only “attach” one to the viewport of the root or do I use subviewports? do I use 1 or 2? etc. Furthermore I’m not sure how I would structure this because as far as I know, I can only have one SpawnPath for my PlayerSpawner but if we move the World and Dungeon into their respective own World3D’s, then I think the Player also needs to be a child of that World3D, no?

I hope this all makes sense. Curious on whats the best approach on solving that. :slight_smile:

Thanks in advance.

Edit: I thought about it and maybe I could do something like this:

root/
├── WorldSpawner (MultiplayerSpawner: spawn_path = ^"WorldContainer")
├── DungeonSpawner (MultiplayerSpawner: spawn_path = ^"DungeonContainer")
├── WorldContainer (Node3D)
│   └── WorldViewport (SubViewport: own_world_3d = true)
│       ├── WorldSynchronizer (MultiplayerSynchronizer)
│       ├── WorldScene (StaticBody3D, Environment, etc.)
│       └── [Players currently in World]
├── DungeonContainer (Node3D)
│   └── DungeonViewport (SubViewport: own_world_3d = true)
│       ├── DungeonSynchronizer (MultiplayerSynchronizer)
│       ├── DungeonScene (StaticBody3D, Environment, etc.)
│       └── [Players currently in Dungeon]
└── UI (CanvasLayer)
    ├── WorldDisplay (SubViewportContainer: visible = true/false)
    │   └── [Remote Path to WorldViewport]
    └── DungeonDisplay (SubViewportContainer: visible = true/false)
        └── [Remote Path to DungeonViewport]

You presented a problem with your architecture, and then suggested a number of workarounds to the flaw in your architecture. I recommend you fix your architecture. Here are a couple of possible solutions.

  1. Create the server as a headless version of the game. Have the host start that up and then their own client. This will force you to make architectural decisions for the server that do not rely on what’s on screen to work because there is no screen.
  2. Follow the instincts in your first solution, and figure out how to spawn the dungeons in different physical spaces, but not randomly so that you know where each one is and can support them separately.

Physics Layers

Setting different physics layers is another solution, but it is still possible to get bugs, and they will be esoteric and really hard to track down. For example player B swings and hits an enemy in player C’s dungeon, and so player C reports that an enemy just dies in front of them for no reason. There are only 32 physics layers, so assuming you need one for environment, one for players, one for enemies, and one for pickups, you are limited to 7 dungeons.

SubViewport

That’s not how SubViewports work. That whole idea is just going down bad road.

1 Like

Headless Server

Sure but I don’t know how to properly do that. In particular, do I design it in a way that the user who hosts the server runs two processes: the server process and the game client itself? I still only have on Godot project. Or is the proper solution to just instantiate two MultiplayerPeer and have them act on the same Scene Tree i.e. manage multiplayer.multiplayer_peer accordingly (which sounds dreadful, if even possible?)

If the answer is to have two processes, then what’s the correct way to spawn the second from the first?

Furthermore, assuming the two processes approach is correct: In that case, I still would need to have the nodes physically separate sine they still all run in the same physics space on the server.

I hope it makes sense.

Note: I do not plan to support dedicated servers but have players host their own servers and just use Steam Network as a relay service (or plain old LAN)

SubViewport

Do you mind explaining this a bit more? I try to get a better feeling for SubViewports. Figured since it’s used for split screen, it should also be fine for such a usecase BUT it did feel a bit like an abuse since most examples talk about Minimaps etc. But why? I lack the knowledge to argue for or against it.

Yes. You have one project, but two (or more) exports. One for the client and one for the server. You don’t spawn the second process, you just run two programs.

Yup. Again, it’s a way to help you encapsulate your design to make it easier to design solutions.

If you want the server and to be embedded in the game, then you’re back to making sure dungeons don’t overlap.

Sure. A SubViewport is just used to show something from the view of an additional camera. It’s like having multiple cameras on a movie or TV set. It’s all still in the same place. In your minimap example it’s often used by placing an overhead Camera3D to show the game from above, and using visual layers to make certain things not appear to make it simplified and look like it’s not a 3D rendering.

1 Like

Ah, exports. Something I never looked into it yet hence it wasn’t on my radar at all. Gonna have to read a bit about it to see how that helps me but I get it, I basically have two executables. If a player creates a lobby, I just run the server executable basically.

Regarding the SubViewport. “A SubViewport is just used to show something from the view of an additional camera.” That’s a good way to put it but is there a more involved, technical reason as to why it would be a bad solution to what I proposed i.e. basically use it as a “window into a new World3D”? I will go the route of a headless server, I’m just trying to get a better understanding of this.

Because you would still have to move the two worlds in space to be in different places. And once you’ve done that, you don’t need the SubViewport.

Could I just use two Viewports and choose which to render and each has own_world_3d set to true? Again just asking for better understanding Godot

No, because the visual layer is different from the physics layer. Regardless of what the Camera2D/3D inside the SubViewport sees, the physics still happen.

Yeah ofc but each would have its own World3D which does decouple everything I thought.

No. Each has its own copy of the same level. That’s how the physics work. If they are different copies completely, there would be no physics interaction - but also there would be no need for a separate viewport.

So why do the docs talk about the World’s Physics Space when it is a “global” one? Also if there are copies, how is the state synced?

Also just wanna leave this here for future reference: Exporting for dedicated servers — Godot Engine (stable) documentation in English

1 Like

I’d need a link to this in the docs to tell you.

Through RPCs, or special networking nodes.