Using legacy enemy follow code without lag

Heya (my first post here, woo)!

So, my inquiry. I am currently developing for a game that is intensive with NPCs, but these NPCs cause lag, so I’ve been looking to migrate them from free model code to new code. A developer we once had stated that PathfindingService should be used but I’m not big on the whole pathfinding part since I haven’t looked into it yet and I’m still a huge fan of the “legacy behavior” of enemy NPCs where they lock onto someone’s torso and move to the torso’s location.

I was wondering if there is a possible way to keep this behavior and recode it so it doesn’t lag without implementing pathfinding.

Follow Logic: Lua code - 43 lines - codepad

(Thanks for any replies!)

I am not very experienced with this but here are my 2 cents:

You could make it so intead of searching through the whole list of workspace’s children, you search for each player.Character

If you want your NPCs to be able to fight with each other you can also place them inside a folder in workspace and search there for the nearest torso.

My best guesses are that the lag comes from, as you said, the script traversing through all the workspace’s children (currently 5.4k children, not including the instances inside folders/models/containers and models inside models, etc.).

Thanks for the reply, I’ll try applying this change when I have the opportunity.

https://blog.roblox.com/2012/05/using-wait-wisely/

Or consider making a “master” script that controls all NPCs at once and do the calculations sparingly every few seconds. Also creating a game that relies on large numbers of NPCs and I can easily go over 70+ NPCs without a frame drop.

1 Like

If you run all your NPCs from 1 script it works just fine

There are quite a few optimizations that can be made with the classic zombie behavior. The intention is that they always follow the nearest living player. One slow point is that it’s repeatedly iterating through everything when it doesn’t need to. Most of the time it’s already doing what it’s supposed to be doing, and you can likely time how long to wait before considering doing something else.

Controlling every NPC from a single script is one improvement. Having a bunch of smaller scripts doing the same thing is inefficient. Iterating through everything in workspace is also inefficient. Assuming they are only intended to attack real players (not just anything with a humanoid that is called “Humanoid”), you can limit the iterating to just the set of characters. With an exta optimization, you can use the same list all the time, changing it whenever a character spawns/leaves/dies.

The biggest optimization would be not recalculating a target 10 times a second. The NPC only wants to change targets when the second closest player becomes closer than the current closest player. If you assume everybody is traveling at the same speed, then you can divide the difference in distance from the closest player and second closest player by 32 to get a minimum possible wait time before the order might switch. Recalculation would become frequent again once the two closest players are similar distances away, and an optimization exists for that case as well, but I think it would be excessive and would only make sense if you had thousands of potential targets at once.

For an easy fix just change wait(0.1) to wait(1). Everything I’m thinking of is probably just perfectionism.

https://hastebin.com/raw/sitiyoqobo
here’s some code I wrote for a faster zombie framework. I stopped before setting them up to recalculate targets whenever somebody spawns, leaves, dies or teleports because it was getting out of hand.

5 Likes

On top of all the help already posted here, it’s quite helpful to remove behaviour of the humanoids that you dont use, such as “Climbing”, which raycasts pretty much every frame to check for ladders.

humanoid:SetStateEnabled(Enum.HumanoidStateType.Climbing, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Swimming, false)

I could double the amount of NPCs I had when I just disabled these

3 Likes

A centralized script sounds VERY appealing. I’ll give this a shot after I try a few solutions.

An endgame I have; for this game to support as many NPCs as necessary without lag.

@1waffle1 Thanks for the help! I’ll try these solutions.

@Elmuowo Forgot that Humanoid states are also available. It says that it should be used in a local script to work as expected online though, is it still available to server-side scripts for NPCs?

Centralized script for NPCs is a good idea. Zombies without a target don’t need to search every 0.1 seconds, as noted. Even more importantly, zombies that do have a target should check even less frequently, or they could get erratically indecisive near crowds of moving players and appear glitchy (because changing target every frame is bad). Consider the case of two players walking away from one zombie in opposite directions, but in a way such that they alternate which is farthest. The zombie will be trapped halfway between them no matter how far they get from it.

There is no need to loop over children of the workspace or to use FindFirstChild which is expensive particularly for deeply-nested models with no Torsos. You care about torsos, so keep a table of player torsos that you update when a player’s character loads, their appearance loads, or their character gets an AddChild with a replacement torso part. Then just have your central NPC script loop over the list of torsos (or character references, if you can find other uses for this list).

I doubt you’ll ever have enough zombies and players combined that an O(N*M) naive search is a problem if all you’re doing is looping over the zombies and finding each a player to target. Especially once you’re doing this less often, and mostly for zombies that have no target, it’s easy to distribute the search over multiple frames (using a simple zombie number modulo some zombie group number). For example, you could have 1000 zombies in groups of 100, and each frame you update on group of 100, taking 10 successive frames to update them all, so that you’re never looping over more than 100 of them per frame. This is simpler than a grid-based spatial partitioning, which is what you’d use for a very large world with thousands of zombies or players.

6 Likes

For a static map, set nodes that they can follow. These nodes would be at every corner, and have them work from node to node. Calculate the node that is most relevant to the player using magnitude + raycast, then use a simple implementation of A* to get there.

I have a module and plugin for this if OP is interested



1 Like

I’m highly against using pathfinding for the place I’m working on (believe it or not), which is why I asked for tips on how to make the legacy zombie behavior not laggy as apposed to how to work out kinks with pathfinding plugins or the PathfindingService.

Thank you for the plugin link though, I’ll check it out in a spare pocket of time.

For your original core question that hasn’t been answered, using pathfindingService to calculate a path would take up even mire resources than just directly moving to the closest player.