Ideas on how to make a smart AI?

More specifically, a good way of determining if a player is in the AI’s line of sight.

I cannot think of a good solution. If I were to use raycasting, what if the ray hits a small part that technically isn’t obstructing the AI"s vision to the player? e.g, a chair. Sure, I could use multiple rays, but I don’t see how that’d be a liable solution.

Essentially I’d love for the AI to detect players within 45 degrees of its facing direction, so it’s more realistic, just not really sure right now how I could implement something that works well, also taking into account if the player is hidden behind a wall, in that case the AI obviously wouldn’t be able to see them.

Thanks for any suggestions.

4 Likes

Keep raycasting and if anything is on the way, and on the left there’s a dead end too, for example go right if there isn’t anything, however you have to write lots of lines to just detect if there’s an object left,right,forward.

2 Likes

Your best bet is to use dot product, As seen here
It allows you to set an “angle radius” which is a cone where the npc can see, I do not recommend using raycast as it’d only allow the npc to see in certain directions.

1 Like

I hope I understand the question correctly, but couldn’t you do something like this:


(The semi-transparent part represents the NPC’s line of sight)
If a player touches the semi-transparent part, they are in the NPC’s field of vision.

2 Likes

This is inefficient, Why have another instance when dot product allows you to do the same without a part.

Plus: Touched is very bad and sometimes doesn’t even register at all.

2 Likes

GetPartBoundsInBox.

A better touched module exists

Why would you go as far as getting a module when dot product solved the problem with 3 lines of code?

You can use dot product:

local RunService = game:GetService("RunService")
local npc = workspace.NPC
local character = game.Players.LocalPlayer.Character
local characterIsInFov = false
local npcSight = .9
local characterIsInSight = false
local npcRange = 100

RunService.RenderStepped:Connect(function()
	local npcToCharacter = (character.Head.Position - npc.Head.Position).Unit
	local npcLook = npc.Head.CFrame.LookVector
	local dotProduct = npcToCharacter:Dot(npcLook)
	
	if dotProduct > npcSight then
		--character is in field of view
		characterIsInFov = true
	else
		-- character is not in field of view
		characterIsInFov = false
	end
	
	-- Now we can determine if the player is in the field of view or not. Now let's prevent the npc being able to see the player behind walls:
	
	local raycastParams = RaycastParams.new()
	
	raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
	
	local list = {}
	
	for i,v in pairs(workspace:GetDescendants()) do -- This will ignore all the chairs (you would have to set a custom state for all the chairs in your game)
		if 
			v.Name == "Chair" -- It can be something simple like this!
		then		
			table.insert(list, table.getn(list) + 1, v)
		end
	end
	
	raycastParams.FilterDescendantsInstances = list
	raycastParams.IgnoreWater = true
	
	local ray = workspace:Raycast(npc.PrimaryPart.Position, character.PrimaryPart.Position, raycastParams)
	
	if ray and (ray.Instance.Position - npc.PrimaryPart.Position).Magnitude <= npcRange then -- In real life, 
		-- somebody can't see someone else from 1000 studs away even if it's a precise straight line! This is why we need a range.
		characterIsInSight = true
	else
		characterIsInSight = false
	end
	
	print("Character is in fov: " .. tostring(characterIsInFov), "Character is in sight: " ..  tostring(characterIsInSight))
end)
3 Likes

Using the dot product approach seems sensible, so that you can at least check if the target is within the AI’s view range. Then you can raycast to see if that view is obstructed or not. Raycasts are generally expensive, but doing 1 per step or renderstep should be ok in terms of performance.

You can always choose to ignore the chair when raycasting. And/Or you can raycast towards a random direction within the AI’s view on each update. This way, the AI has a chance of looking around that small part on every update.

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local NPC = workspace.NPC
local Head = NPC:WaitForChild("Head")

local function viewIsObstructed(direction)
	-- Do some raycasting with Head.Position and direction
	-- You can also randomly offset the direction within the AI's view range
	-- to increase the chance of looking around small parts
end

local function getVisiblePlayers()
	local visiblePlayers = {}
	for _, Player in pairs(Players:GetPlayers()) do
		if Player.Character then
			local PlayerHead = Player.Character:FindFirstChild("Head")
			if PlayerHead then
				local direction = (PlayerHead.Position - Head.Position).Unit
				local dotProduct = direction:Dot(Head.CFrame.LookVector)
				-- Where 0.5 is a magic number that represents the range of view
				if dotProduct > 0.5 and not viewIsObstructed(direction) then
					table.insert(visiblePlayers, Player)
				end
			end
		end
	end
	
	return visiblePlayers
end
2 Likes