-
-
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 interest management support to the high-level multiplayer API #3904
Comments
Hmm after more deliberation, ive determined this isn’t quite going to work from a performance standpoint, particularly in especially dense areas of a map. Im considering alternative approaches now and will update this proposal once i think of something better. |
Okay, I've updated the proposal with something a bit more complex, but should scale much better. |
Interest Management is also essential for any competitive RTS genre games given the fact that Hacker can change Client's FOW (Fog Of War) code. |
Howdy! Thanks for looking into this. I've been thinking about this topic for a while now, and I'd like to propose something a bit different, but that should also allow implementing the nodes you mention, along with more game-specific custom nodes when needed by the game developers. This idea stems from the consideration that there a multiple reasons for doing interest management, which does not always overlap, e.g.:
Given these premises, the idea is to add a "tag" system to both objects and peers, requiring peers to have a matching tag to be able to "see" the given object. Currently, tag composition is assumed using OR (see below for examples), but it might be interesting to also add AND composition (multiple tag layers?) which could make more complex use cases easier at the cost of perfromance. So, here is a brief of the proposed changes. MultiplayerAPIclass MultiplayerAPI {
Error tag_object(Object *p_obj, const StringName &p_tag);
Error untag_object(Object *p_obj, const StringName &p_tag);
Error tag_peer(Object *p_obj, const StringName &p_tag);
Error untag_peer(Object *p_obj, const StringName &p_tag);
} Replicationclass MultiplayerReplicationInterface {
Error on_object_tag_change(Object *p_obj, const StringName &p_tag, bool p_add);
Error on_peer_tag_change(Object *p_obj, const StringName &p_tag, bool p_add);
} Note, in this context, spawn/despawn also means disabling sync for path-only nodes (which cannot be despawned). When an object is tagged:
When an object is untagged:
When a peer is tagged:
When a peer is untagged:
RPCThis needs further exploration, but I think we could use the tag system for RPCs too. class MultiplayerRPCInterface {
Error on_peer_tag_change(Object *p_obj, const StringName &p_tag, bool p_add);
} We could force that for objects with at least one tag, broadcast rpcs will only be sent to peers with at least a matching tag. ExampleMultiplayerVisibility (2D/3D)A pontential implementation for higher level nodes that can be included with Godot (properly expanded if necessary): An extension of Area2D (or Area3D) that automatically tag/untag overlapping objects. # Uses an area to detect visibility automatically tagging/untagging other MultiplayerVisibility
MultiplayerVisibility2D : public Area2D {
var tag_name : StringName
var root_path : NodePath
} Given Where MV1 represents P1, MV2 represents P2, and MV3 represents an object not associtated to a player. When MV1 overlaps MV2:
Player 1 and 2 will see each other. When MV2 and MV1 no longer overlaps:
Player 1 and 2 will no longer see each other. When MV1 overlaps MV3:
Player 1 will see root of MV3. When MV3 leaves MV1:
Player 1 will no longer see root of MV3. |
Interesting idee to implement this with tags. I suppose you could imitate a distance based system by having a grid of tag-areas... Not sure how I am feeling about using Strings under the hood. Enums or just numbers would scale significantly better. Edit: Wait, nevermind, forgot about StringNames. |
Note that it doesn't use EDIT: didn't get the edit in time :) |
What hooks are you referring to here? We can probably expose a bit more if it's not enough. |
In some of the earlier interest management discussion (on rocketchat I think) we discussed the possibilities of exposing the underlying code so that more complicated projects could write their own interest management system as a gdextension. |
@Ansraer oh yes, I see, we can also add a virtual method to synchronizer |
Uh, been a while. I think the general idea was that whenever this method returned true the object would be automatically spawned on the client if it didn't already exist there. Not sure how despawning was supposed to work. Perhaps an enum with four values could be returned instead of a boolean (spawn&sync, sync if already exists, don't sync, don't sync & despawn)? Spent some more time thinking about your proposed tag system btw, and I would really like the ability to provide a conjunctive normal form of tags. |
That is indeed the problem, it would mean that we need to call that function for every peer, for every MultiplayerSynchronizer, every frame, no matter which peer knows about it or not. Which I am not sure is going to work performance-wise unless the function you call is very simple or have a very low number of replicated objects implementing that function. So I'm not sure this can actually work in anything bigger than a small-sized game due to the high number of calls it requires during each frame.
I agree, this is a quite rigid, and forces to use combined tags (i.e. doing the class MultiplayerAPI {
Error tag_object(Object *p_obj, const StringName &p_tag, int p_layer=0);
Error untag_object(Object *p_obj, const StringName &p_tag, int p_layer=0);
} Where different layers are composed using AND, and a peer needs to have at least one matching tag with every layer in the object. But I think it can still perform well if used appropriately. |
I probably misunderstand, but how is there suppose to be an overlap check between two moving players, to check if they should send data to each other, when they are not already sending data to each other about their position? |
You could implement a grid with a tag based system: |
@TackerTacker The host knows about both players positions at all time, the host is the one doing the tagging/untagging in the I don't know any way to do position-based interest management in a true p2p scenario. |
I wanted to suggest some things to this proposal. I want to focus mostly on the usability side as always (what is exposed to the user) How it is implemented underneath we can see later. As such, I am more concerned about the MultiplayerSynchronizer node, which is what I think should allow controlling the interest management (Imo as what we synchronize is scenes, we should not do this at Object level). API can be something like: synchronizer.use_peer_filter= true/false This turns on the interest management for this node. If false (default), this always does synchronization and on every frame with all other peers. When on, certain rules need to be supplied for synchronization to happen: synchronizer.set_peer_allowed(MultiplayerSynchronizer* peer,true/false) And thats it. Additionally, I would still like to add the ability to customize, per frame, whether synchronization happens. This being independent of the peer filter (if peer filter is enabled it will only call this after the filter passes): synchronizer.add_custom_filter(Callable filter)
synchronizer.remove_custom_filter(Callable filter) Callables are efficient, so the custom function can be implemented as: func _validate_peer(peer : MultiplayerSynchronizer):
return (run_some_logic_on_whether_it_should_sync(peer)) So IMO, you can still efficiently implement a tag system or anything you want over this (or even control update frequency by returning true or false intermitently), but it gives you far more control and flexibility on implementing this. A Tag system can be an extra node (that inherits synchronizer or that exists outside) that users can download from the user library as an add-on and implement this. It can be perfectly implemented outside the multiplayer classes, which allows keeping the MP API simple. |
I've added a tentative implementation in godotengine/godot#62961 , see there for details, but here's the gist: Sets visibility for synchronized objects: or extends MultiplayerSynchronizer
func _enter_tree():
add_visibility_filter(self._is_visible_to)
func _is_visible_to(peer) -> bool:
return false # Hidden The visibility is updated on every frame (default), physics frame, or manually. This also allows players to join mid-game (with some quirks). |
I'm reopening this to keep track of the fact that we still need interest management in RPC (i.e. It should be implemented soon, but we are planning to do some core changes first godotengine/godot#63049 |
Closing since RPC visibility is now also implemented via godotengine/godot#68678 . |
Describe the project you are working on
And client-server multiplayer game with an authoritative server and large worlds, whether 2D or 3D.
Describe the problem or limitation you are having in your project
Problem Statement
When working with large worlds in client-server online multiplayer games with an authoritative server, the game world will have a lot going on all the time. Whether it's other players running around and doing various actions, NPC/Enemy AI, World Events, etc... it would be unreasonable to expect a player's computer to be able to handle processing and rendering everything in the game world at once.
Even if you break your world up into various instanced "zones," like many games choose to do, depending on the particular zone, it's still likely going to be too much data to process. Level streaming helps out on the rendering side of things, but it does little to assist in the processing department. Additionally, most of the stuff going on in the world or instanced zone isn't even going to be of interest (heh) to you since it's so far away that you likely can't even see it even if you wanted to.
The solution to this problem that is pretty much ubiquitous in large online worlds these days is Interest Management.
Describe the feature / enhancement and how it helps to overcome the problem or limitation
Interest Management
Interest Management can take a couple different forms, but ultimately, it's just reducing the number of things you send netowrk updates for to each player. A particular player will only recieve updates from game actors that are in their "area of interest" (a.k.a. "zone of interest", often abbreviated "AoI" or "ZoI"). In fact, game actors won't even be spawned into the player's client's game world until they are within their AoI and will be promply despawned from the player's local game world once they leave the AoI again.
Types of Interest Management
There are actually several different implementation approaches out there for interest management, however by and far the two most common are distance-based, and region-based. I'll be focusing on these two for the rest of this proposal. If you're interested in reading more about all of the various implementations out there for interest management, this paper does a great job of explaining them and each of their pros and cons.
Euclidean Distance
This is a popular one because we.... it's dead simple. A player will subscribe to server updates from any actor that falls within a set radius around them. In this implementation, this radius is often referred to as the player's "aura." While this is dead simple to implement, and computing the distance between two points in space is extremely fast, it has the major problem that it doesn't scale well at all. The more players that are in the game world or instance zone, the more comparisons need to be made every frame. It's for this reason, that the next approach is by and far the most common.
Grid-based
In this implementation, the world--or zone--is divided into a 2D grid of regions. As actors move about the world, some component (we'll call it the InterestManager here for lack of a better term) is responsible for keeping track of of what actors are in what regions. Any time the server sends an update by an actor, this InterestManager component is consulted to determine which specific peers should receive the update, and it only sends the update to those peers. Generally, this will not only include the other actors in the same region as that actor, but also those in the immediately adjacent regions as well (or sometimes a higher adjacency is used. The specific region size and adjacency are parameters that are unique to each game and require careful tuning to get right). Likewise, when the InterestManager notices that an actor changes regions, it will recalculate its interested parties, and spawn the actor in the game clients of newly interested players, and despawn it from those who are no longer interested.
Unlike the distance approach above, this one scales incredibly well, but it is much more complicated to implement. The biggest downside for this approach however, is that a simple 2D grid is a rather poor approximation of interest, so for games that require higher degrees of precision, other approaches might be considered instead.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
Proposed Implementation
UPDATE: @Faless created a much more elegantly generic API that better fits core here. His proposed implementation supersedes the one documented here, but I left this one for context.
Original Implementation
First, a common interface will be created that all interest management implementations can inherit from.
With this simple interface, we can implement different types of interest management and users can either put them directly in their scene or even add one as a Singleton. Additionally, multiple implementations could theoretically be used together as well. Whetehr or not that's actually useful in practice however, I'm not so sure.
Now let's take a looks at some pseudocode for a grid-based implementation.
This handles keeping the region list up to date with who's in what region, but now we need to handle those changes somehow. Doing this all in one big for loop obviously isn't great as it will end up bogging the framerate down at runtime. Instead, we will flip the problem on its head and use a second new Node type.
There should exist one InterestArea node for each actor that exists in the world, but there should only be one GridInterestManager for an entire "zone." (Though there doesn't strictly need to be, as mentioned above. Maybe users will get creative on uses here.)
Doing the spawning/despawning in this way should be pretty quick, even in super dense areas, as the number of poeple entering and leaving a AoI should be pretty small.
The text was updated successfully, but these errors were encountered: