Making an AI prioritize something it can directly see

Over the past few days, I have been working on an AI that utilizes pathfinding and rays for a project. My current and final step of the AI (i hope) is making the AI prioritize something it can directly see with a ray over something it has to pathfind to (my plan is to make it pathfind if it can see it or not, just incase it needs to jump or is walking on a path with lots of holes).

I have already worked on a script but I am confused on how I would implement this feature. My current idea is adding everything that requires pathfinding to a table, and checking if it is in that table, and if so, to ignore. I am not sure how I will implement this though, and I need help.

Here is the script I have made so far for finding nearest torso:

function findNearestTorso(pos)
	local list = game.Workspace:children()
	local torso = nil
	local dist = 200
	local temp = nil
	local human = nil
	local temp2 = nil
	for x = 1, #list do
		temp2 = list[x]
		if(temp2.className == "Model") and (temp2 ~= script.Parent) and temp2.Name ~= "SCP049" and temp2:FindFirstChild("SCP049-2") == nil then
			if (temp ~= nil) and (human ~= nil) and (human.Health > 0) and (temp.Position - pos).magnitude < dist then
				temp = temp2:findFirstChild("HumanoidRootPart")
				local ray = Ray.new(AI.HumanoidRootPart.Position, (temp.Position - AI.HumanoidRootPart.Position).Unit * dist)
				local hit, position = workspace:FindPartOnRayWithIgnoreList(ray, {script.Parent})
				if hit then --always prioritize something it can directly see
					if hit:IsDescendantOf(temp.Parent) then
						return temp
					else --something behind a wall  
						temp.Parent.Name = ignore[#ignore + 1]
						return temp
					end
				end
			end
		end
	end
end

I know something is wrong with this script because it is constantly printing “nil” instead of the target when in range of the AI.

1 Like

Oh man, that looks like an old script mixed with some new stuff. Before I help out a bit - can you confirm if you want it to only find players’ torsos or do you have other NPCs it can target?

2 Likes

Only torsos is the plan, and yea it is a pretty old script found in alot of roblox’s official AI models

1 Like

I’ll assume you mean the torsos of other players then. I’d write it like this. This will give you two tables. One with characters it can see, and one with characters you need to pathfind to.

local AI: (Model) = script.Parent -- You can redefine this
local AIRootPart: (BasePart) = AI.PrimaryPart
local maxDistance: (number) = 200

local function findAllCharactersInDistance(distance: (number)): (table)
	local characters: (table) = {}
	local players: (table) = game.Players:GetPlayers()
	for _, player: (Player) in pairs(players) do
		local char: (Model?) = player.Character
		if (char) then
			local humanoid: (Humanoid?) = char:FindFirstChild("Humanoid")
			if (humanoid and humanoid.Health > 0) then
				local distance: (number) = player:DistanceFromCharacter(AI.PrimaryPart)
				if (distance <= distance) then
					table.insert(characters, char)
				end
			end
		end
	end
	return characters
end

--[[
* Will return two tables. The first will be characters the AI can see
]]
local function getPriorityCharacters(allCharacters: (table)): (table, table)
	local priorityChars: (table) = {}
	local secondaryChars: (table) = {}
	local params: (RaycastParams) = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Blacklist
	
	for _, char: (Model) in pairs(allCharacters) do
		local vectorToChar: (Vector3) = (char.PrimaryPart.Position - AIRootPart.Position)
		params.FilterDescendantsInstances = {AI, char}
		local result: (RaycastResult) = workspace:Raycast(char.PrimaryPart.Position, vectorToChar, params)
		
		if (not result) then
			table.insert(priorityChars, char)
		else
			table.insert(secondaryChars, char)
		end
	end
	
	return priorityChars, secondaryChars
end

local allChars: (table) = findAllCharactersInDistance(maxDistance)
local priorityChars: (table), secondaryChars: (table) = getPriorityCharacters(allChars)
3 Likes

Thankyou so much, I had the right idea but did not know how to execute it

1 Like
local list = game.Workspace:children()

This is going to kill your performance if you have an unorganised workspace. It looks as though you only want the NPC to kill players?

If so, just loop through the player list and find those with characters that are alive, and then raycast towards them.

e.g.


local players = game:GetService 'Players'

-- checks whether player is alive
local function isAlive(player)
	local char = player.Character
	if not char or not char:IsDescendantOf(game.Workspace) or not char:FindFirstChild 'Humanoid' or char.Humanoid:GetState() == Enum.HumanoidStateType.Dead then
		return
	end
	
	return char
end

-- pass it:
--   (1) the Vec3 pos of wherever you want to check from
--   (2) the distance of how far it can see
--   (3) what you want the ray to ignore
local function findClosestVisibleTorso(rootPos, dist, ignore)
	local curDis
	local curChar
	
	-- find all the players in the server
	for _, player in ipairs(players:GetPlayers()) do
		-- check if they're alive
		local character = isAlive(player)
		-- this is called a 'ternary operator' (kind of) - it will return the primary part of the character if it's there, or return nil
		local primary   = character and character.PrimaryPart or nil
		if primary then
			-- get the distance from our starting point and the position of the character we're looking at
			local dis = (primary.Position - rootPos).magnitude
			-- if we currently don't have a known distance reference, or if the new distance is smaller than our last, then continue
			if not curDis or dis <= curDis then
				-- raycast from our start position to the character, ignoring anything that was passed to us through 'ignore'
				local ray = Ray.new(rootPos, (primary.Position - rootPos).unit * dist)
				local hit, pos = game.Workspace:FindPartOnRay(ray, ignore)
				-- have we hit something, and is that thing part of the character we're looking at?
				if hit and hit:IsDescendantOf(character) then
					-- yes it is, so let's set it as our new distance reference + character as it's the closest one so far in our iterative steps
					curDis  = dis
					curChar = character
				end
			end
		end
	end
	
	return curChar, curDis
end

-- now for usage...
local closestCharacter, theDistance = findClosestVisibleTorso(AI.HumanoidRootPart.Position, 200, script.Parent)
if closestCharacter then
	-- we found the closest character that's visible to us!
	-- do something
else
	-- we haven't found anyone visible
	-- maybe we should pathfind?
end
``
2 Likes

Thankyou also! I can use both of these scripts and look at them

1 Like

I have a question:
What is the purpose of curDis and curChar if it has no value and it is being assigned no value? And if it has no value, how is it being used in

if not curDis or dis <= curDis then

You’re right, initially the variable is undefined, but it is assigned a value when the following conditions are met:

  1. If curDis is still undefined or if it has been defined in a previous iteration and the dis < curDis
  2. If it hits the character we’re currently examining on that particular iteration

You can see it getting assigned here:

					-- yes it is, so let's set it as our new distance reference + character as it's the closest one so far in our iterative steps
					curDis  = dis
					curChar = character

Essentially, in the if statement:

if not curDis or dis <= curDis then

I am saying: If ‘curDis’ is undefined, or if the new ‘dis’ is less or equal to the curDis then continue. The latter part of that question, “if the new 'dis is less or equal to the curDis” won’t be checked unless ‘not curDis’ returns false e.g. when it has been defined.

The reason I initialised both values outside of the scope of the loop is so that I can track which of the visible players are closest.

p.s. it should actually say if not curDis or dis < curDis then - I accidentally threw an equal op in there. Similarly, you could also check whether dis <= dist as a condition for that if statement to avoid an unnecessary raycast

1 Like

Oh I understand now, thankyou for clarifying