While developing a single-player FPS, and therefore focusing on AI enemies with Pathfindingservice, I’ve encountered a major issue: ranged enemies would eventually get on one single path, creating something that looks like a herd of sheep, hence easy to kill. With explosives existing in the game, the aspect takes away all fun, so I’ve started looking for a solution and found a very easy one.
The solution consists of 2 pointers:
- Ranged enemies only using Pathfinding if not close enough to target
- Strafing
In this short tutorial I’ll focus on the latter topic, as it is up to you to tweak the first behaviour.
So, assuming you have implemented some conditions which stop the NPC’s from following if they’re close enough, you’d notice that now they just stand still, which doesn’t help the cause. A real player would never stand still in a face-to-face battle, so strafing it is.
Strafing is essentially movement to left or right, but what is left or right on something with always changing position and orientation? At this question, it sparked for me: attachments.
Attachments are relative to their parent part, and are by default used in regular Roblox characters.
Therefore, we can call MoveTo on an NPC’s humanoid with the sole parameter being the HumanoidRootPart’s RootRigAttachment + a vector3 with a random X value (i.e. multiplying an integer by -1, 0, or 1: left, stand still, or right?
Here’s a real-life example:
-- variables for reading convenience
local hrp = char.HumanoidRootPart
local rootAtt = hrp.RootRigAttachment
local random = Random.new() -- better than math.random since you can choose whether you want numbers or integers
local distanceToStrafe = 20
if sight(enemy, target) == true or distance > minDist then -- can the enemy see us, or is it too far?
path:ComputeAsync(enemy, target) -- compute the path
spread = defaultSpread
... -- handle movement according to computed path
elseif ... then --[[ the enemy does not need to move to us anymore, but is it already strafing?
up to you to choose a condition for this, I personally went with checking the humanoid's MoveDirection magnitude: if it is around 1 then the hum is moving, 0 if standing still,
but I don't recommend it because magnitude is expensive in terms of performance when used frequently]]
spread = defaultSpread/2 -- improve enemy's "aim" while strafing
local strafeTarget = rootAtt.WorldPosition + Vector3.new(distanceToStrafe*random:NextInteger(-1,1),0,0)
-- multiply strafe distance arbitrarily by -1, 0, or 1 for the element of surprise
-- if no movement is possible I also recommend conditioning the line below with it for performance
hum:MoveTo(strafeTarget) -- fireworks
end
Edit: you should also set AutoRotate on Humanoids to false if you do not want the NPC’s to rotate to the direction they strafe at, for them to also look at the player I recommend using the AlignOrientation constraint.
If you’re new to Pathfinding, check out this article to get started: Character Pathfinding | Documentation - Roblox Creator Hub
I hope you’ve learned something from this short tutorial, feel free to share your thoughts.
Lastly, here are the before and afters, room to improve but still a major difference:
Before - Watch strafing_before | Streamable
After - Watch strafing_after | Streamable