🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Hide non-rendered objects

Started by
6 comments, last by itay9 4 years, 2 months ago

hello guys!
I have a question regarding cleaning objects:
I'm working on a networked game where the server send each client states of objects that are nearby that player on each tick, which the client interpolates and visualizes said objects.
I want to clean (hide) objects that are not in the client's update radius.
I came up with 2 possible solutions:


1. Create a "cleaning" task which receives a `CancelationTokenSource` and will wait for a timeout (100 MS top) and then call the cleaning action. if the task is canceled - (meaning an update for that object had arrived) then the task will be recreated and restarted.

(Will performance be an issue for such a mechanism where I might end up with dozens or hundreds of tasks running at the same time?)

2. Whenever an object leaves the update radius raise an event which go over all of the nearby objects (can be taxing, it depends on how many objects are clustered nearby), and on every tick send (Reliably) a packet which contains the “renderObjects” (only ids) and “”hideObjects" (only ids)

If you guys have another suggestion, or you could recommend between the 2 approaches I've listed it would be very helpful

Advertisement

Generally, the operation of visibility management is known to be n-squared complexity. This means that games with 200 players spend four times as much time managing visibility, as games with 100 players. If the world is very large, and you can get away with segmenting it in “sub-areas” then you can cut down the n-squared “N” value to “number of players in visible sub-areas” rather than “number of players in world” – for MMOs, this is a huge win!

Yes, visibility will typically add an object to the visible list as soon as it's clear that the user will need to see it, and will remove it after it's been not-visible for some time. You may want a longer timeout than 100 ms, to avoid too much churn for objects close to the visibility limit, unless your game is very much susceptible to net-sniffing cheats.

Typically, yes, you will have some task that cycles through all objects currently visible to a particular player, and prunes the ones that have not been visible for some time. You may want to run this task for X players per network iteration, rather than try to use some kind of thread to run it at a fixed time interval; this will scale better with more players, because the cost of pruning is only X * N, rather than N * N.

Whether this is too slow or not, depends entirely on your particular implementation, server hardware, and number of players. It can only be benchmarked, not guessed at.

enum Bool { True, False, FileNotFound };

@hplus0603 Thanks for your reply!

I should mention that my game is a Co-Op RPG up to 4 players connected though I aim to have at least 100-200 AI NPCs at the background, and that it already uses an indexed `QuadTree` to query a GameState and as such I only send data that is necessary for a client.

Regarding your answer, I couldn't quite understand if you were in favour of the 1st solution I've suggested (the tasks) or the 2nd option (server commands through network).

Or were you in favour of a solution that combines both approaches?

For simplicity's sake, you can assume that the client's state is a partition relevant to him of the server-state.

I don't understand what a “Task” is. To me, that's a kind of threading work work queue system.

A “command through network” sounds to me like any network packet that updates clients with state.

So, clearly you have to have some way to schedule sending data on the network. Is this already a task? Or is it done by calling a function inside your main loop? No matter how the code runs, it needs to send the appropriate updates.

As for “how often do I calculate visibility for each player,” that's up to you. You could, for example, calculate visibility for one player every 100 ms. This means each player has objects go away after at most 800 ms of invisibility, if you have 4 players. How, exactly, you schedule the running of the code that calculates objects out-of-scope, is up to you, and has little to do with how you send the “this object is no longer visible” command to the client.

You could just have a bit of code in your main loop:

  if (now() - lastVisibilityTime >= milliseconds(100)) {
      calculate_visibility_for_player(visibilityPlayerIndex);
      visibilityPlayerIndex = (visibilityPlayerIndex + 1) % numConnectedPlayers;
      lastVisibilityTime = now();
  }
  if (now() - lastNetworkSendTime >= milliseconds(20)) {
      send_queued_packets_to_all_players();
      lastNetworkSendTime = now();
  }

Or you could schedule a task to repeat at appropriate times.

Calculating visibility would set the “visible” flag to false for any object that's not within line of sight, and if that flag was already set for that object, mark it for removal. Sending packets to players would send removal messages for all marked-for-removal objects. Meanwhile, when objects become visible, they are immediately marked as “visible” and an update should immediately be queued for that player. Similarly, each time an object moves, you can check if it's invisible for a player, and if it is, detect whether it becomes visible, and if so, set its visibility flag and send the update to make it visible again. Active visibility – passive invisibility.

If however by “network command” you mean the CLIENT would request that the server does pruning, that doesn't seem reasonable – the server should be in charge of what the client sees.

enum Bool { True, False, FileNotFound };

@hplus0603

I think I understand better your answer now, it was very helpful.

When I suggested using “tasks” I did refer to the threading mechanism, I also meant using them on the Client Side only (meaning the client will “guess” which objects are no longer updated).

I ended up using solution 2, where the server does declare which objects are new and which are out of sight, and I aim to create a debounce/throttle on this operation so that I wouldn't send unnecessary data.

I see you updated the question to clarify – great, it makes more sense now!

The client generally shouldn't have to guess anything, as you seem to have realized. That being said, it's useful for the client to “time out” objects that it hasn't gotten an update for in some time. While this should “never” happen, there are of course always bugs in software, especially distributed software, and removing “stuck” objects is likely to help deliver a better playing experience.

Regarding the “task” system, keeping tons of objects and pointers around in lists and hash tables, causes lots of cache pressure and pointer-chasing. When you have hundreds of thousands of objects, this matters – your CPU will be on its knees trying to keep all of these pointers consistent. The locking necessary to deal with pointer-referenced objects in a threaded system also hurts performance. Thus, it's generally better to keep all the bits of state in a single memory blob for that object – a “fat struct,” as it were. Or perhaps in many arrays of structs, each of which has some aspect. The index into the “SoundMakingObjects" struct is the object ID, just like the index into the “HealthpackReceicingObjects” is the same object ID. You pre-allocate enough structs for all objects in all subsystems. “Wasteful" in memory, but might help in robustness and performance.

If you only have, say, a thousand objects, chasing those pointers will still have a cost, but the cost will be small enough that it's probably fine for your game.

enum Bool { True, False, FileNotFound };

@hplus0603

hplus0603 said:

I see you updated the question to clarify – great, it makes more sense now!

The client generally shouldn't have to guess anything, as you seem to have realized. That being said, it's useful for the client to “time out” objects that it hasn't gotten an update for in some time. While this should “never” happen, there are of course always bugs in software, especially distributed software, and removing “stuck” objects is likely to help deliver a better playing experience.

I see, that sounds logical, though I think I will let the client decide only on visual things by its own, and let the server full authority regarding state.

hplus0603 said:

I see you updated the question to clarify – great, it makes more sense now!

The client generally shouldn't have to guess anything, as you seem to have realized. That being said, it's useful for the client to “time out” objects that it hasn't gotten an update for in some time. While this should “never” happen, there are of course always bugs in software, especially distributed software, and removing “stuck” objects is likely to help deliver a better playing experience.

Regarding the “task” system, keeping tons of objects and pointers around in lists and hash tables, causes lots of cache pressure and pointer-chasing. When you have hundreds of thousands of objects, this matters – your CPU will be on its knees trying to keep all of these pointers consistent. The locking necessary to deal with pointer-referenced objects in a threaded system also hurts performance. Thus, it's generally better to keep all the bits of state in a single memory blob for that object – a “fat struct,” as it were. Or perhaps in many arrays of structs, each of which has some aspect. The index into the “SoundMakingObjects" struct is the object ID, just like the index into the “HealthpackReceicingObjects” is the same object ID. You pre-allocate enough structs for all objects in all subsystems. “Wasteful" in memory, but might help in robustness and performance.

If you only have, say, a thousand objects, chasing those pointers will still have a cost, but the cost will be small enough that it's probably fine for your game.

I actually do have a “GameState” though in my case it is a class, in my tests it performs better (per implementation), though all of my packets are structs. The server also has an indexed QuadTree so that I can query for snapshots by geometry easier, I didn't try making any of my classes threaded yet, I'm not quite sure I need it for the network syncing. (I expect at most 300 objects that are sent to various players).

Thanks for sharing your knowledge

This topic is closed to new replies.

Advertisement