-
-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a multiplayer interface and visual nodes for SceneTree replication #3459
Comments
I really appreciate the thought of building the higher level API abstraction on top of a Lower level replication API. This is a great design decision. I can imagine developers familiar with networked games wanting more flexibility at a lower level than a SceneTree implementation, so this is a nice touch. For clarification, we should not confuse server tick-rate with the Also appreciate thought going in here: |
I think large part of the proposal, and large part of the initial implementation are derived from this single assumption:
I think this is where the crux of our difference in perspective lies. In my view, spawn state is unnecessary and redundant, so I will try to make my point why: How do you instantiate a scene normally in a regular game? (non-multiplayer) var a = load("res://gamelevel.tscn")
add_child(a) How do we do this in a multiplayer game? You instantiate the same scene. Sure, enemies may move around later, but this happens later, the initial state is the same and needs no synchronization. Let's go for a bit more complex use case, like something that we will spawn multiple times, like a bullet in a single player game: var b = load("res://bullet.tscn")
b.linear_velocity = $player.facing_axis
b.position = $player.gun.position
add_child(a) What is the difference to the previous example? That spawning something multiple times requires code to set it up. How does this work with multiplayer? This proposal assumes that we will probably set linear_velocity and position as spawn, so when added to the scene it will be synchronized. But is it really needed or the best way to do this? Let's look at it from a different perspective. What we know for certain up to here is that code needs to run on initialization. In other words, the user will have to write this code, always. So, instead of thinking in terms of spawn state, why don't we think in terms of spawn code? We now have the NetworkSpawn node, this node could have a virtual function that the user can hook into: extends NetworkSpawn
class_name BulletSpawn
func _spawn():
var b = load("res://bullet.tscn")
b.linear_velocity = $player.facing_axis
b.position = $player.gun.position
return b Then, all the client needs to do is call: $bullet_spawner.spawn() This will call the spawn function, spawn the node, replicate and create this node over the network. Again, remember that we stated this code needs to exist anyway. All we did is put it in a place where it makes more sense. Of course, I can see that this does not exactly the same, extends NetworkSpawn
class_name BulletSpawn
func _spawn(position,velocity):
#validate what comes from the server
position = clamp(position,min_range,max_range)
velocity = clamp(position,min_range,max_range)
var b = load("res://bullet.tscn")
b.linear_velocity = velocity
b.position = position
return b then $bullet_spawner.spawn($player.facing_axis,$player.gun.position) For reconnection (in case a peer appears out of nowhere with the game already running) the spawn node can remember that this instance requires those parameters and send them again. So to sum up, my point on this is that:
This is why when I read that, when for spawning a spawn state is required, It feels odd. Maybe there are times where this is the case, but in the vast majority of them all that matters is that the same code is used to create the instance, not the same state. I think the proposal and idea of implementation was imagined based on the concept that scenes simply appear out of nowhere and in any state and was designed to solve that synchronization, but on further analysis it should be possible to prove that scenes will always come from the same state + an initializer code snippet. Only ensuring the initializer code snippet runs on both ends is enough to have proper state initialization. Spawn states impose a much higher toll of complexity into the system, when they are really not as useful. |
I would argue this is a worse place to put this logic. The example you started with is simple game logic: var b = load("res://bullet.tscn")
b.linear_velocity = $player.facing_axis
b.position = $player.gun.position
add_child(a) The above example creates a new entity, and sets up various state data that this entity is intialized with. It's part of the logical state of the game's simulation, and is probably what a lot of developers are already doing. You want this logic to be part of the game simulation code (wherever that may be), NOT the netcode. Putting this game logic into the proposed virtual function of a network node, is not good. You want isolation here: extends NetworkSpawn
class_name BulletSpawn
func _spawn(position,velocity):
#validate what comes from the server
position = clamp(position,min_range,max_range)
velocity = clamp(position,min_range,max_range)
var b = load("res://bullet.tscn")
b.linear_velocity = velocity
b.position = position
return b Likewise overriding |
But part of the idea is you don't want to have to be explicit in your game simulation logic about what should happen at the network sync level. This can be configured independently of your game logic. If you can mark certain scenes as replicated/synced, then you don't actually have to put this line: $bullet_spawner.spawn($player.facing_axis,$player.gun.position) anywhere into your game code, which is a good practice. You can operate as normal, as if you were building a single player game. And this is a good thing. Because if you want to end up adding advancements to this networking module in Godot (say client-side prediction, etc), it is going to be CRUCIAL to have the correct separations of concerns here. You do not want your netcode interleaved with your game logic code, because in advanced scenarios, (say client-side prediction, rollback and resimulation) these will run completely separate and independently. |
@jordo Again, as I described, marking scenes replicated and synced is a no go because it's a large security risk. You will need to go via a spawner node that controls the location, types and lifetime of instantiated scenes. I think that part is not on discussion. |
@Fales In any case, I think the proposal overall is really good, I am just nitpicking on details of spawning, because I would really like to have the ability to do as I described, even if alternatively, but for the most part I think it's great. If I understood the limitation well, I don't think you really need to know the scene beforehand to synchronize the parameters when spawning it. As I described, this can be a simple hash of scene + properties contained in node that can group them internally. |
@reduz That's a small part of the points I brought up, and you are misinterpretting... I was referring to this screenshot that @Faless posted, which are checkboxes (marking) on the spawner node. There is zero security issues with the points I brought up. This above is a perfect design (according to your concerns) regarding security. Scene's are checked (allowed) at the spawn node, and a target node path is set for spawn. Is there anything else you are concerned with? |
@jordo oh but it's the same, in @Faless proposal you do something like this instead: var b = load("res://bullet.tscn")
b.linear_velocity = velocity
b.position = position
$bullet_spawner.spawn( b ) It's not too different, but the main change is that you don't really have much control on the spawning logic. What I argue is that I prefer to write the spawn code once and running in both peers, rather than having two separate systems doing this. To me using spawn properties for this is a waste of time and making something more complex for no purpose. If you folks really want to use it and believe it's useful or better represents your way of thinking, I am not against having them. I would myself prefer any day to use spawn parameters having the choice, so I am advocating for this also being supported, even if you don't use them. |
I will advise, with 100% certainty, that you do not want the developer to have to write your proposed 4th line of code here: var b = load("res://bullet.tscn")
b.linear_velocity = velocity
b.position = position
$bullet_spawner.spawn( b ) Regardless of whether this fits into your design or @Faless design. Frankly, this is a must to avoid. |
This should be var b = load("res://bullet.tscn")
b.linear_velocity = velocity
b.position = position
$Players.add_child( b ) Which will work in both single player and multiplayer. And this is the pattern already used by developers using Godot right now |
@jordo As I mentioned, thanks for your advise, but this is done explicitly for security reasons. If you want to make a game where the server is 100% trusted, and you took measures again potential MITM, and your code does lots of checks to avoid problems with this, then that's up to you, but it won't be the case for all the other users. I prefer this is secure by default, even if it's a bit more hassle to have to force users to explicitly state what, where and when they will replicate scenes. Explicit spawning to me is best. Nothing also prevents the spawn node from working single player if networking is not used. |
Explicit spawning 'code' is going to end up sending the same data on the wire... Your argument for security falls flat, because you can MITM either way. Can you explain to me (from the wire's perspective), and a potential MITM injector, what the difference would be on the wire for code that explicitly spawns an entity |
If you are making a MITM security argument, please explain what would be different in the wire format in each scenario. Because I can't see how they are different. |
@jordo There is plenty of difference:
All these 3 are huge security improvements and the users dont have to worry about inadvertently making something they can mess up. |
For the spawner node scenario, there is implicit validation, for the other scenario there is not. |
All three of those security concerns are addressed in the screenshot @Faless posted, I will post again for your review: 1): Spawner node is the scene tree. (spawner node, left side) Check 2): Spawner node has a node path target. (spawn path, right side) Check 3): Spawner node has a limit of what scenes can be spawned. (replicated scenes, bottom) Check |
@jordo Oh ok, either I misunderstood you or you missed a vital piece of information in your reasoning here (or both) :) So having to guess, what you imply is that the spawner node is needed, but you still want it to magically detect when a node is added to the scene and synchronize it? If this is the case, I am also against this, I really prefer explicitness, I am not a fan of automagically doing things for users. User is not dumb. |
This was honestly a legitimate question. If you want to make that argument, let me know what the difference is on the wire. |
@jordo read the post above yours. |
If you read any of my comments, I never once said or suggested anything against the spawner node... So I'm not sure where you got that idea. I prefer the node design as I referenced @Faless proposal screenshot.
If this ends up being case, this will end up being an unfortunate limitation for Godot. The explicitness is defined within the parameters of the SpawnerNode, and there is no need to complicate the game logic. As I mentioned, you really want separation of concerns here. |
It does not complicate the game logic, It's the same amount lines of code and the meaning of the code is more explicit. This is always good to me. I am sorry you may not share some of my views or values in software design, but I strongly abide by them. |
To be clearer, anything that helps better understand the intention of the code when reading it is good for me. |
|
@jordo sorry, most likely cultural difference, did not meant to offend or anything of the sort. I just meant that in my experience, when given the choice, it is always better in the long run to make intention explicit and clear in APIs, programming languages, user interfaces, etc. It helps others (teammates or users) get familiar with what's going on faster. |
@Faless How about what I suggested as a |
OK, so as far as I can tell. @reduz, your first reason for changing the proposal was security related, but as discussed above, it seems your comments in your points (1,2, and 3) are resolved. So currently right now, as far as I can understand, your current issues are issues with:
and
In your first statement, you are just using the adjective 'magically' in a negative context to make your point. There's nothing wrong with the network sync system taking care of this for you, whether you want to call it 'magic' or not. It could be considered 'magic' when someone places a node in the scene tree and it appears on screen. Which is why you don't want to be explicit everywhere. This is why developers don't have to explicitly write say vulkan commands. This stuff is supposed to be 'magically' handled by the engine as it's just an abstraction. The second statement is basically an argument that your software design principles are better than others, and it bothers me, because it's actually opposite of good software engineering principles, and even generally accepted OO principles. What you want to strive to build are systems that work together with HIGH cohesion, and LOOSE coupling. By requiring developers to explicitly add in network code to their game code, example:
var b = load("res://bullet.tscn")
b.linear_velocity = velocity
b.position = position
$Players.add_child( b )
At any rate, take it for what it is. I like the current proposal, with the exception of I would like to see |
Sorry to butt in, @jordo, just wanted to say I think what @reduz meant re: the second point was not that your principles were inferior at all, but that there are legitimately good programmers that disagree with his principles on the point - i.e. there are a lot of people who would prefer the tersest but still sensible declaration of it (he's mentioned this before on Twitter, for context). Further to the second point, unfortunately, as usual, the OO blade cuts both ways: by adding an extra responsibility to add_child, you're breaking the Single Responsibility Principle for the class itself. In reference to the first point, I can really see what reduz meant: I would hate it if add_child did something like this without my explicit desire/declaration, personally. I shouldn't need to know all the side-effects of the underlying calls I'm making, but it feels like in this case I would absolutely have to, and it has led to a very leaky abstraction. That said, I do understand your point about putting extra work on the developers using the engine and that makes it a pain-point. I think having a pain-point that requires some refactoring and code updates is not the end of the world, but is sometimes the only thing that makes sense - you wouldn't want a single function having a bunch of extra code added into it for the sake of convenience (not that that's what you were suggesting - just an example). But I digress. If I had my druthers, it would simply be a node that, when the game is run, it would connect to a singleton signal that is raised whenever a child is added anywhere in the tree. It feels like something of a middle-ground, albeit not necessarily super-OO either - by adding a raise call in add_child, you wouldn't really be breaking the SRP because that signal is still defined as a thing for adding children, and you'd still have the better maintainability aspect that comes with your/Faless suggestion. (For the record, I have no idea if, in context, this suggestion makes any sense at all; just thinking out loud as I'm working on my own stuff.) |
I attempted a multiplayer game once, in another engine, and it was a smash bros clone (platform fighter with physics). One thing that was impossible to achieve (without rewriting the entire engine) was deterministic physics across machines, i.e. you track the input of all players and timestamp them, and by replaying those inputs you get exactly (frame-by-frame) the same result. It never worked out; physics interactions worked ever so slightly differently every time, causing a butterfly effect. I guess it's more of a physics topic than networking, but I wanted to post this here anyway because it would be a huge win for Godot if it could do this out of the box, as any physics-based multiplayer game suffers from this issue. |
This should be discussed in a separate proposal – see #2821. Note that while there are plans to improve physics determinism in future 4.x releases, it's unlikely that GodotPhysics will ever be fully deterministic. If fully deterministic physics are required, it's best to write your own physics implementation or use a third-party physics engine. Also, the use of floating-point coordinates should be avoided whenever possible to improve determinism across platforms and CPU architectures. |
Perfect determinism is impossible to achieve without sacrificing a lot of
performance, but in any case Godot physics / bullet physics are already
deterministic enough to create networked games.
In past months I been creating samples with networked balls, characters,
bullets: even with high latency the game was responding correctly.
Tbh, it’s not even needed perfect determinisms at the physics level,
because is expected to lose determinism by losing some sent packets on the
road. What make a lot of difference is the recovery mechanism, that should
be able to smoothly correct the player. If that algorithm works well, your
game will be perfectly playable even with poor connections. So it’s much
productive focus on the networking side, rather than the physics 😊🤘
|
Great discussion . What is the networking technology being used currently ? i have been using NATS for games and other real time systems. Nats has c++, rust. Golang, js clients 100% open single MacBook Air can do 8 million transactions a second . It’s highly performant. Clients can run on anything . Iot. Web, mobiles. It’s very lean and has zero allocation design . used by some big players . Can someone let me know the current situation with the real one networking stack so I can get involved please ?? |
@MartinHaeusler: Most network synchronization techniques don't require true determinism, and like everyone above says, you can make really great online multiplayer games without it! That said, it just so happens that I created a 2D deterministic physics engine for Godot, and published a tutorial about getting started with it today: I specifically created it for implementing rollback & prediction netcode where you are synchronizing only the inputs (as opposed to some of the state as well), which is what it sounds like you were attempting to implement in your Smash Bro's clone. |
Sorry I haven't looked into networking stuff for a while, but wanted to chime in on this proposed revision. I can't really speak at the moment to any particularly strong opinions on how the high level nodes should be structured, but they seem decent enough. My main concern with the first API implementation was actually at the low level, that by having sync operate at per-scene rather than per-object, it essentially blocks the ability to do any kind of dynamic interest management. This draft seems a lot more flexible in that regard. |
thats actually a really good point. I havent had much time myself to read the proposal in detail. I believe I read an older iteration of it but i dont recall. either way, being able to support dynamic interest management is certainly a critical requirement of whatever design we go with. Hopefully Ill have some time soon to read through the current proposal and formulate some new opinions. |
Okay. Ive read through the existing comment thread and not only does it seem that not much more discussion has happened since I last read through it (in fact, most of the new discussion has been off topic or tangential at best), but it seems that there still hasn’t been a decision made in regards to @reduz ’s concerns and @jordo ’s rebuttal. I believe that resolving this dispute should be of a high priority since so many other network feature proposals—both existing and future—depend on this one. @akien-mga what do we do about this? |
@jonbonazza if you are referring to the automatic vs explicit spawn debate: |
I'm just wondering if the replication sync will be able to be "disabled" by the client, because in my use case I have two situations where I would want to do this:
|
I believe this is known as interest management, which will be worked on in the future (but not in the initial implementation). |
While we're on that subject - are there standard techniques for that? It's not something I'm very familiar with.. For example, imagine you have things that are specific to your game or some context in your game. Like... there is a "cloaked player", and you want to make sure the network doesn't even tell other players where the cloaked player is (so the client side can't cheat and expose the person as if they were not cloaked). I would consider that a form of interest management, just not the typical ones listed above ("on different screens" etc). Without thinking very hard, it seems like the type of thing where masks/layers (ala rendering and collisions) could be applicable.. |
FYI, just a small update about where things are: currently there is more work in my progress at godotengine/godot#55950 We're currently waiting for reduz to review those additional changes before proceeding in any direction. This may take a bit still, as reduz is currently understandably busy due to holidays and various IRL stuff. :) We will update this issue once there is more information. Thanks to everyone providing feedback! |
@agnosthesia what you describe is indeed called “interest management” and while high level facilities for this won’t be included initially, it should certainly be doable if you wish to craft it yourself. There are a couple common ways to do this:
Both have their own pros and cons. there are others as well, but i believe these are the most common. all of that said, this conversation is beginning to fall out of the scope of this particular proposal. Feel free to join the discord and/or developer chat to discuss it further with other interested parties (heh). |
Well, it came up via @nonunknown 's post because there's an obvious question of how this scene replication concept relates to interest management (if at all). I don't see anything wrong with discussing that - the work being done by @Faless and others is great too, and of course people can still discuss that. After all, this is a public proposal board that asks for feedback from godot users about what's being discussed... I'm definitely on the discord, but it's pretty hard to get a discussion like this going there in my experience. |
Closing as this is now implemented via godotengine/godot#55950 . |
I hope thats helpfull for the further development. An real "authoritive server (fast-paced)" -> |
This is the final version? In latest alpha (and all other) this is very hard for debug. Errors like this without any details (source of code, or node id/name).
|
Error reporting can be improved, reporting source of code or node name is not always possible unless you send that information along the wire, which is not viable if we want it to work within a reasonable bandwidth amount. What's really strange is this error:
Not sure if this happens after a disconnect, but the fact that the channels allocated by ENet is 0 is likely a bug, so if you can reliably reproduce that it would be great if you could open an issue. I'll poke around the code in any case. |
"Unable to send packet on channel 2, max channels: 0" - this is randomly. I don't see dependent between events (yet). It also happens when player left the game. On player side error looks like this:
On the server side probably in the same time: When player left the game, I remove node (by queue_free). This node is synchronized by "multiplayer synchronizer" node, and spawned with controller "multiplayer spawner". In authority problem I understand the reason. Only source of problem - no. I don't know the reason of the other errors. Maybe they are consequence of "authority problem". I can send you my code (2 main scenes, max 10-15 nodes), but only on PM, because it's private. I don't except resolution/support. Just if you feel that it could help you with growing engine, I can send you. |
Can I use MultiplayerSpawner in Node spawned from other MultiplayerSpawner? |
Am I interested in having a room on the server? For example, the global world or the local world is temporarily generated by a scene that players can enter inside the scene. Tree structure example As a result of the rest of the settings, this is already replication synchronization from the object to the parent, which is looking for descendants, if there is a room, then they will replicate if it turns off. As you can see in the structure of the tree, player 1 and player 2 both see in the game but do not see other players and do not transmit information from players 3 and 4 since they are in another room and other players 2 and 3 also do not see other players, this will reduce huge packs when players accumulate in the main scene. You can also specify a replicator spawner not a list of objects. PS Sorry for the translation. I myself am Russian and I love to program but I can't write in English |
Describe the project you are working on
A multiplayer game with Godot.
Describe the problem or limitation you are having in your project
Game state replication over network is hard to achieve in Godot, even for simple games.
Making multiplayer games has historically been a complex task, requiring ad-hoc optimizations and game-specific solutions. Still, two main concepts are almost ubiquitous in multiplayer games, some form of messaging, and some form of state replication (synchronization and reconciliation).
While Godot does provide a system for messaging (i.e. RPC), it does not provide a common system for replication.
Describe the feature / enhancement and how it helps to overcome the problem or limitation
The goal of this document is to propose a replication API that can be used for the common cases, and is extensible via plugins, outlining what emerged during the
#networking
meetings while gathering and integrating feedback from a broader public.In this sense, an initial "node interface" is also proposed incorporating the suggestions in #3359, as well as changes to the lower level API to make room for the more Godot-y approach to the subject.
Design Goals
Whatever system gets implemented (even if limited for the scope of Godot 4.0) should aim to:
Glossary
Object
.Object
.Brief
The idea is to add a fully customizable object replication interface to the
MultiplayerAPI
to deal with replication at the lower level, along with a set of higher level nodes to replicate the state inSceneTree
as suggested in #3359.Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
Higher level replication API (default SceneTree implementation)
Two extra nodes and a new resource type will be added to deal with scene replication:
SceneReplicationConfig
: Will allow to define which properties of a given node (or subnode) should be synchronized automatically.Somehow similar to
Animation
, the initial options will be limited but might grow over time to allow more and more features and transparent optimizations engine-side.MultiplayerSpawner
: Will allow to spawn instances of predefined scenes viaMultiplayerSpawner.spawn(node)
according to their replication config.MultiplayerSynchronizer
: Will allow synchronizing the remote and local state.Nodes will behave according to the respective multiplayer authority (
Node.get_multiplayer_authority()
).Some implementation notes
MultiplayerSpawner
:MultiplayerSynchronizer
:sync_interval
.While we strive to support as many use cases as possible, the default behaviours might not fit all needs given the wide range of multiplayer games (from 3v3, to battle royale, to MMORPG), so while we should definitely provide more features out of the box (interpolation, interest management, etc) those are left out of this proposal, which will instead at least provide a way for game developers to deeply customize the spawn and sync process if needed.
Some optimization notes
Optimizations, and bandwidth optimizations in particular, are crucial to an effective networking.
When done properly, this is also going to be the most optimized state possible, that no tool can produce for you.
Lower level replication API
A lower level replication API, (part of
MultiplayerAPI
) will be used by the higher level nodes to implement those functionalities and an extensibleMultiplayerReplicatorInterface
will allow overriding the behaviour globally, or on a per-node basis.The lower level API will accept
Object
parameters (and not just nodes), but the default implementation will only work using the higher level nodes.The
MultiplayerReplicatorInterface
will receive theMultiplayerSpawner
orMultiplayerSynchronizer
as configuration for the nodes being spawned/synced.This changes the current API quite a bit, but should incorporate the same concepts discussed during meetings:
MultiplayerReplicatorInterface
, which will also have GDExtension support for performant send/receive (using pointers instead of PackedByteArray).sync_all
method has been removed. Each node is synced independently (although bulking happens) replication start/stop will be called on each object instead (and you can decide to batch them as you wish with the custom interface).track
/untrack
methods are also removed. Since spawn is now explicit, you will be able to track objects in the_on_spawn_send
/_on_spawn_receive
callback yourself.Further Work
The long discussions over this topic spawned (pun intended!) a lot of ideas for further improvements but this proposal is already dense enough to further expand on them:
If this enhancement will not be used often, can it be worked around with a few lines of script?
It's a highly requested core feature of the engine.
Is there a reason why this should be core and not an add-on in the asset library?
It needs to be integrated to the multiplayer API.
The text was updated successfully, but these errors were encountered: