How to make a NPC strafe around a player in a circle-like motion when in range?

According to the title, I am attempting to replicate the “strafing” system of that in Elden Ring.
In Elden Ring, typically, enemies with large shields will strafe around you in a circle-like pattern until attacking.

I’m absolutely HORRID at math, so the best I could do is this.

run_service = game:GetService("RunService")

--^ services

ai = script.Parent
target = workspace.FakePlayer
face_point = ai.HumanoidRootPart.FacePoint -- align orientation

--^ essentials

start_point = nil
last_strafe = "-x"

--^ other

strafe_pause = 0
radius = 10

--^ settings

run_service.Heartbeat:Connect(function(delta_time)
	face_point.CFrame = CFrame.lookAt(
		ai.PrimaryPart.Position, Vector3.new(
			target.PrimaryPart.Position.X,
			ai.PrimaryPart.Position.Y,
			target.PrimaryPart.Position.Z
		)
	)
	
	if (target.PrimaryPart.Velocity.Magnitude) < 1 and strafe_pause <= 0 then -- target is standing still, wait for opening
		start_point = target.PrimaryPart.CFrame * CFrame.new(0, 0, -10)
		local strafe_distance
		strafe_pause = 1
		
		--^ strafe data
		
		if last_strafe == "-x" then
			last_strafe = "x"
			strafe_distance = math.random(-10, -8)
		else
			last_strafe = "-x"
			strafe_distance = math.random(8, 10)
		end
		
		local end_pos = start_point * CFrame.new(strafe_distance, 0, 0)
		end_pos = end_pos.Position
		
		--^ strafe randomizer to avoid repetition
		
		ai.Humanoid:MoveTo(
			end_pos
		)
		ai.Humanoid.MoveToFinished:Wait()
		
		--^ strafing
		
		strafe_pause = 0
	end
end)

As you can see, it strafes left to right based off of the direction that the player is facing. This is obviously very flawed. What I want to achieve, is, to imagine a “circle” around the player, pick the nearest point, then start to follow that imaginary circle (slowly).

This sounds too complicated for my small brain. Is anybody able to assist?

Can’t believe I did it, but I actually did it.

---> Imports
local values = require(script.Parent.values)
local visualize = require(script.visualize_vertex)

---> Config
local data = {
	["vertex_count"] = 50;
	["visualize_path"] = true;
}
local blacklist = {}

---> Strafe function
local function strafe(humanoid: Humanoid, target: Model, ai: Model)
	values.strafe = true  -- Start strafe
	local connection, reached
	local npc_position = humanoid.Parent.HumanoidRootPart.Position
	local nearest_distance = math.huge
	local nearest_vertex = 1
	local last_target_position = target.HumanoidRootPart.Position
	local initial_distance = (npc_position - target.HumanoidRootPart.Position).Magnitude

	-- Find nearest vertex
	for i = 1, data.vertex_count do
		local angle = math.pi * 2 * (i / data.vertex_count)
		local direction = Vector3.new(math.cos(angle), 0, math.sin(angle))
		local pos = target.HumanoidRootPart.Position + (direction * initial_distance)
		local distance = (npc_position - pos).Magnitude

		if table.find(blacklist, i) then continue end

		if distance < nearest_distance then
			nearest_distance = distance
			nearest_vertex = i
		end
	end

	-- Move between vertices
	if values.strafe == nil then return end

	local current_vertex = nearest_vertex
	if table.find(blacklist, current_vertex) then return end

	local current_distance = (humanoid.Parent.HumanoidRootPart.Position - target.HumanoidRootPart.Position).Magnitude
	local angle = math.pi * 2 * current_vertex / data.vertex_count
	local direction = Vector3.new(math.cos(angle), 0, math.sin(angle))
	local pos = target.HumanoidRootPart.Position + (direction * current_distance)

	if data.visualize_path then visualize(pos) end  -- Visualize path

	humanoid:MoveTo(pos)
	connection = humanoid.MoveToFinished:Connect(function()
		reached = true
		connection:Disconnect()
	end)

	local random = math.random(math.floor(data.vertex_count/5), data.vertex_count)
	table.insert(blacklist, current_vertex)
	if #blacklist >= random then
		table.clear(blacklist)
	end

	while reached ~= true do
		task.wait()
		if values.strafe == nil then return end
	end

	values.strafe = nil  -- End strafe
end

return strafe

Just like in theory, It creates an imaginary circle around a stationary player, (resetting it each time they move), and follows the closest vertexes of that circle, increases the index by 1, and the next time the function is called, it repeats. Hope this helps for anyone else.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.