@Tomarty has some great suggestions. A few principles apply here:
We want to run our code as little as possible. This means that we should use events rather than polling. We can further reduce the number of times our code is called by using “debounced events”. Instead of using :Connect(), use :Wait() on the signal in a loop with a call to wait(timeout) somewhere in it. 0.1 - 0.5 would be plenty of timeout. Further, we only need to perform a full update when a player moves a significant amount. So, inside this debounced event loop, we’d find the squared distance or even quicker Manhattan distance from the last update location. If it is larger than some threshold, then we compute all the distances to other NPCs/Players. We also only need to figure out the positions of players; NPCs positions are known / created by the server at runtime.
We want to divide the problem and perform as little work as necessary. As @Tomarty said, we want a spatial partitioning system. If a NPC is in another area, we know we don’t need to calculate the distance to it. This may be rooms, buildings, or any other way of partitioning your map. A player would only need to find the distances to other NPCs in their region. This greatly reduces the amount of work performed and allows the game the scale unbounded. To transition players between regions, simply add invisible wall(s) between the regions and detect when they are touched.
All of these suggestions will be premature optimizations that are likely to slow things down for the number of NPCs mentioned by the OP. As @colbert2677’s noted, player-to-NPC distance checks are cheap. You need hundreds–if not thousands–of NPCs before the overhead and complexity of something like a quad-tree becomes worth considering. The correct approach, IMHO, is to implement it as simple distance checks, and consider optimizations as-needed (which for 10-15 NPCs they never will be).
What you describe is not related to debouncing, it’s just a mechanism for polling less frequently, and not really the most precise or cheapest option. Debouncing of events refers to rejecting duplicate events that occur in quick succession, but aren’t actually meant to be interpreted as separate events. The terminology comes from the problem of hardware switch contacts bouncing off each other and being misinterpreted as multiple intended actuations of the switch. Debouncing mechanisms can look just like certain rate-limiting schemes, which leads to a bit of conflation of the two concepts, but there is still a difference in that debouncing properly only refers to rejecting unintentional duplicates.
If you just want to run something every X frames, or T milliseconds, just count frames or elapsed time in your heartbeat loop, and run the code when the counter rolls over. No need to start new coroutines or create new Bindables, just use what’s already running. You don’t make your code more efficient by making it “event based”, because your threads with wait(n) are being resumed by the task scheduler… which is polling and comparing timestamps, so the apparent absence of polling is an illusion.
There seems to be 10 classes of people in this thread: those who believe that finding the distances is slow and should be done on clients, and those who believe it is super cheap and wonder why this question is even being asked. I agree with you, this is overkill in most cases. It is assumed that as the cost of finding distances to NPCs becomes a burden, these methods can be implemented independently as needed. Using these methods one can theoretically extend this to tens of thousands of NPCs without a hint of lag.
Yeeeeaaaah, technically. Its close enough to its intended meaning in my mind. You and I could probably have a good discussion on the semantics.
I was thinking of a script per a player using the StarterScripts.
Using a cooperative scheduler instead of continuous polling is more efficient. The reason is that a cooperative scheduler can put multiple timestamps on a queue and only check the first every step. If the first time has passed, then it checks the next and so on. The result is that instead of checking 100 timestamp differences every scheduler step, only a couple values are checked (and very often, only one).
I think what the OP was right to start with this on the server, centralized. That’s the simplest and most tidy solution. IMO, you need some other justification to offload game logic onto the client, because of how it exposes both the source code (for exploiters and cheats to inspect), and possibly the game state undesirably (for example, if the client knowing all locations of NPCs enables cheating). And anything the client works out that potentially changes game state still needs to be checked again on the server to validate.
You’ll see no measurable benefit in creating a dedicated thread to run your combination of wait(n) and block of code, vs driving off a common event like Heartbeat, which nearly every game is already going to have a connection to. Theoretical efficiency is not likely to be a factor in which approach makes more sense. In server-side code you lose some control over fine tuning the timing, ordering, and synchronization of the tasks that run periodically when you give everything its own timing thread.
The wait(n) in a thread approach doesn’t scale so well either, particularly when a bunch of the events happen frequently and regularly coincide: now instead of some light condition-testing logic in a single function, you’ve got a bunch of threads being resumed on the same tick, and executing code, all with at least the overhead of a function call. It’s not quite correct to assume this approach is always inherently more efficient than polling. I’m not saying it’s never the best option, it’s just not as simple as it being a clear win.
Sorry, but DO NOT do this. It will make performance worse. RenderStepped & Heartbeat run at 60 times per second and Wait() runs at 30 times per second. Running a loop twice as often will make performance twice as bad.
Now to do a little bit of explaining:
No it isn’t. It fires twice as fast. Allow me to explain.
Roblox runs at a maximum of 60fps provided your computer is capable of doing so.
Wait() fires every other frame, so if you are running at 60fps, wait will run 30 times per second, whereas RenderStepped fires once every frame.
However, if you need something to run at 60 fps, use Heartbeat instead as it also runs at 60fps or the current frame rate, so it’s “just as fast”, but instead runs concurrently with other tasks in the thread pipeline:
So it’s better to use Heartbeat unless you are doing something that involves camera movement or UI, but don’t use either for this scenario because they would make performance worse in this case.
There was another thread a while back that asked a similar question and I think the solution to the thread could help you
Woot3’s answer (which was selected as the solution) to that thread:
So, at some point you will want to check the direct distance between the player and the NPC but there are optimizations you can make before then.
Firstly, for any NPCs which do not move, you can split them into regions. The regions are essentially cubes of N×N×N studs. When a player enters a region you can perform a distance check on the region they are in and any neighboring regions.
For moving NPCs, you can assign them to these zones as they move around the map, or if they only stay in one zone while moving they can be treated the same way as a static NPC.
You also only need to check the distance between one part of the character and one part of the NPC.
There is a big difference between faster and more efficient.
What you are thinking of is the type of faster associated with running a loop more times per second.
wait(0.5) is “faster” then wait(1). Wait(1) wait 1 second before executing code, wait(0.5) waits 0.5 seconds before running code, RenderStepped & Heartbeat are about ~0.016 seconds of delay.
All you are doing by using RenderStepped and Heartbeat is lowering the delay between how often the code runs, which means your code will run MORE often and suck up MORE cpu time, which can mean the game runs slower and is less efficient.
To my knowledge it wasn’t. That’s not “too bad”, you just said something that was incorrect and would have made things worse, and I was politely correcting you. We didn’t make you delete your post, we were just clarifying misinformation.
All of this is pretty moot. If you need to do something approximately once per second, you can do it with wait(1) calls or you can check a timer on Heartbeat. The performance difference is negligible and should not be what drives this choice. Using heartbeat does not mean that you run all your code 60 times a second, and there are a lot of reasons why you might want code to execute on Heartbeat in a real game, the most important of which is that it lets you update any number of game subsystems in an explicit order, synchronized, with more precise timing than you can get from using waits, and in step with the game engine. If you start a bunch of coroutines all using wait statements, you have a bunch of asynchronous executions that you can’t even guarantee the relative ordering of. If your code is all totally parallelizable, great, have at it, but when is that ever true in a server’s main game loop?
Wait() does not resume every other frame, it’s way less reliable than that, which is why developers try to avoid it.
For the record, the post I replied to was deleted and originally was saying to use renderstepped because it’s “faster” then making a bunch of wait() loops
Anyway, I’m aware of this, which is why I was simply trying to point out the fact that render stepped will not make the code “twice as fast” and I also explained why it’s generally not a good idea to use Render stepped.
Zeuxcg himself (technical director) did a presentation at RDC about when to not use renderstepped and what to use in place of it.