Need assistance with NPC AI

Greetings Developers, I’m trying to create my own Monster AI, but there are a few problems I can’t solve. The script itself Is pretty basic but I’m struggling to figure out how to implement these.

AI SCRIPT

local pathfindingservice = game:GetService("PathfindingService")
local runservice = game:GetService("RunService")
local players = game:GetService("Players")

local npc = script.Parent
local humanoid = npc.Humanoid
local head = npc.Head
local hrp = npc.HumanoidRootPart

local settings = require(npc.Settings)

hrp:SetNetworkOwner(nil)

function Patrol()
	humanoid.WalkSpeed = settings.PatrolSpeed
	
	local patrolfolder = workspace.PatrolArea:GetChildren()
	
	local randompatrol = patrolfolder[math.random(1,#patrolfolder)]
	
	local path = pathfindingservice:CreatePath(settings.agentparams)
	path:ComputeAsync(hrp.Position, randompatrol.Position)
	
	local waypoints = path:GetWaypoints()
	
	if path.Status == Enum.PathStatus.Success then
		for index, waypoint in pairs(waypoints) do
			
			if waypoint.Action == Enum.PathWaypointAction.Jump then
				humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
			end
			
			humanoid:MoveTo(waypoint.Position)
			humanoid.MoveToFinished:Wait()
		end
	else
		warn("Path not computed!", path.Status)
	end
	
end

function findTarget()
	local nearestTarget = nil
	local nearestDistance = math.huge

	for i,v in game.Players:GetPlayers() do
		local character = v.Character
		local humanoid = character and character:FindFirstChildOfClass("Humanoid")

		if humanoid and humanoid.Health > 0 then
			local Distance = (character.HumanoidRootPart.Position - hrp.Position).Magnitude

			if Distance < nearestDistance then
				if settings.checkTarget(character) then
					nearestTarget = v
					nearestDistance = Distance
					return nearestTarget
				end
			end
		end
	end
end

function AttackTarget(target)
	
	if target then
		-- found target
		local char = target.Character
		
		
	else
		warn("no target given.")
	end
end

function targetAngle(character)
	local npctocharacter = (character.Head.Position-head.Position).Unit
	local npclook = head.CFrame.LookVector
	
	local dot = npctocharacter:Dot(npclook)
	
	local maxdist = 10
	
	local dist = (character.Head.Position-head.Position).Magnitude
	
	if dot > .5 then
		if dist <= maxdist then
			-- char is in fov
			return true
		else
			return false
		end
	else
		-- char is not in fov
		return false
	end
end

function followTarget()
	
	local target = findTarget()
	
	if target then
		
		local char = target.Character
		local charhead = char.Head
		
		local path = pathfindingservice:CreatePath(settings.agentparams)
		path:ComputeAsync(hrp.Position, char.HumanoidRootPart.Position)
		local waypoints = path:GetWaypoints()
		
		if path.Status == Enum.PathStatus.Success then
			for _, waypoint in pairs(waypoints) do
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
				humanoid:MoveTo(waypoint.Position)
				humanoid.MoveToFinished:Wait()
			end
		else
			warn("Path not found!")
		end
		
	end
	
end

runservice.Heartbeat:Connect(function(dt)
	
	print("running")
	
	local target = findTarget()
	
	if target then
		local angle = targetAngle(target.Character)
		if angle then
			followTarget()
		else
			Patrol() -- ALSO DOES NOT YIELD FOR FUNCTIONS
		end
	else
		Patrol() -- DOES NOT YIELD FOR FUNCTIONS
	end
end)

The first thing I need help with is yielding for functions. The loop does not yield for the Patrol() function, so it keeps running making the NPC stutter. I’ve tried to use coroutines, but I’m still learning those.

The next is the AI so it goes to the 2nd Floor. The map I’m currently using is a small map with a second floor:


(Image illuminated to illustrate)

I put a transparent wedge on the stairs and cut off the top part using union:
image

Now, I don’t have any issues with adding a small wedge to the stairs, but for some reason, it does not seem to work anymore.

image

This is the model I’m working with however, the agentparams size is pretty small, so I don’t see why that would be an issue.

The pathfinding I’m using does not detect the Y-axis, which I’ve learned from another post, so is there any solid way to implement that? For both the Patrol and Player-chase system.

Finally, if there’s any way to improve the code, I’m trying to make it so it has an angle of detection, a patrol, a chase and some other features soon.

Thank you for taking your time to read this.

1 Like

Can you show a video of the AI stuttering?

External Media

Really sorry for the terrible quality, but what I’ve figured is that since the Patrol() function is in a RunService, it keeps on calling the function, generating different directions. But how do I yield for the function so it runs once?

You could use a debounce, or if it’s a function, use :Once()

1 Like

I don’t think I can run Patrol:Once(). Even if so, it wouldn’t probably do anything since it’s In a RunService, causing it to run again. As for the debounce, I think I can figure something out, but it would be more or less impractical, with multiple debounces, I was hoping if there were any other ways, since I would assume yielding for a function is necessary.

1 Like

I think I’ve fixed the errors??

Let me know:


local pathfindingservice = game:GetService("PathfindingService")
local runservice = game:GetService("RunService")
local players = game:GetService("Players")

local npc = script.Parent
local humanoid = npc.Humanoid
local head = npc.Head
local hrp = npc.HumanoidRootPart

local canpatrol = true

hrp:SetNetworkOwner(nil)

function Patrol()
	if canPatrol then
		humanoid.WalkSpeed = 16

		local patrolfolder = workspace.PatrolArea:GetChildren()
		local randompatrol = patrolfolder[math.random(1,#patrolfolder)]

		local path = pathfindingservice:CreatePath({
			AgentRadius = 2,
			AgentHeight = 3,
			AgentCanJump = true,
			AgentCanClimb = false,
			WaypointSpacing = 3
		})
		path:ComputeAsync(hrp.Position, randompatrol.Position)

		local waypoints = path:GetWaypoints()

		if path.Status == Enum.PathStatus.Success then
			for _, waypoint in pairs(waypoints) do
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
				humanoid:MoveTo(waypoint.Position)
				humanoid.MoveToFinished:Wait()
			end
		else
			warn("Path not computed!", path.Status)
		end
	end
end


function findTarget()
	local nearestTarget = nil
	local nearestDistance = math.huge

	for _,v in pairs(players:GetPlayers()) do
		local character = v.Character
		local humanoid = character and character:FindFirstChildOfClass("Humanoid")

		if humanoid and humanoid.Health > 0 then
			local distance = (character.HumanoidRootPart.Position - hrp.Position).Magnitude
			if distance < nearestDistance then
				nearestDistance = distance
				nearestTarget = v
			end
		end
	end
	return nearestTarget
end


function AttackTarget(target)
	if target then
		local char = target.Character
	
	else
		warn("no target given.")
	end
end


function targetAngle(character)
	local npctocharacter = (character.Head.Position - head.Position).Unit
	local npclook = head.CFrame.LookVector

	local dot = npctocharacter:Dot(npclook)
	local maxdist = 45
	local dist = (character.Head.Position - head.Position).Magnitude

	if dot > 0.5 then
		if dist <= maxdist then
			return true 
		else
			return false
		end
	else
		return false 
	end
end

function followTarget(target)
	if target then
		local char = target.Character
		local charhead = char.Head

		local path = pathfindingservice:CreatePath({
			AgentRadius = 2,
			AgentHeight = 3,
			AgentCanJump = true,
			AgentCanClimb = false,
			WaypointSpacing = 3
		})
		path:ComputeAsync(hrp.Position, char.HumanoidRootPart.Position)
		local waypoints = path:GetWaypoints()

		if path.Status == Enum.PathStatus.Success then
			for _, waypoint in pairs(waypoints) do
				if waypoint.Action == Enum.PathWaypointAction.Jump then
					humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
				end
				humanoid:MoveTo(waypoint.Position)
				humanoid.MoveToFinished:Connect(function()
		
					if not targetAngle(target.Character) then
						canPatrol = true
					end
				end)
			end
		else
			warn("Path not found!")
			canPatrol = true  
		end
	end
end


while true do
	local target = findTarget()
	if target and targetAngle(target.Character) then
		script.Parent.AlertSound:Play()
		
		canPatrol = false
		task.spawn(followTarget, target) 
	else
		script.Parent.AlertSound:Stop()
		canPatrol = true
		Patrol()
	end
	wait(1)  
end
1 Like

Where did you get the AlertSound? But anyway, the patrolling did work, but the monster doesn’t detect the player, even if I stand in front of him.

I just placed the sound so I can check if the monster chases me.

For me, the detecting does work. Maybe error on my end.

It’s not working for me, I even tried to rewrite parts of it. Not to mention that the AI is still buggy when the player goes to the second floor.

Can’t it move itself to second floor by stairs? Or it moves but can’t go up to second floor because of unknown blocking?

It can go to the second floor, however it cannot follow the player when the player is on the 2nd floor but the monster is on the first floor.

Could you make a fake part on stairs to make sure that bot doesn’t calculate stairs part? Bot may calculate a direction if the fake part is simple-shaped.