NPC finding cover system

So I’ve working on a system that makes an NPC find and move to cover relative to the nearest player’s position. The moving to cover works fine but finding cover is difficult. I would like to know what would be the most effective way to locate the nearest cover from the NPC. I would also like to know how would I go about allowing the NPC to use cover standing or crouched based on height. (Note for a cover to be valid the NPC must be able to peek off it and get a clear line of sight to the player). Any help is appreciated.

1 Like

You can use something like a CollectionService to tag different kind of cover for example

image

The Yellow outline cover has a “Low” tag
The Green outline cover has a “High” tag

You can also tag the object that can be used as cover with the “cover” tag
This way the NPC can differentiate the map part and the cover part
Better yet. Because now the part has been tagged you can making so the NPC will be more likely to hide in a full cover which offers more protection

Thank for the reply but I don’t really want to manually tag all possible cover. Is there any way for the NPC to determine objects that can be used for cover in front of it without predetermining the possible cover?

you can use Vector3.Magnitude to check if the object is high enough to be considered half or full cover

I made a really basic find cover system a while back, its certainly not the most efficient way but it meant I didn’t have to go and tag stuff all over my game.

Basically the way it works is it would generate points in a circle around the NPC. It would then raycast from those points to wherever the danger is (e.g. enemy player or another NPC), if the raycast failed it meant there was no line of sight and therefore it was a ‘place of cover’.

If it couldn’t find any places for cover, it would increase the radius of the points in the circle around the npc and try again. Raycasts are cheap so this isn’t that bad. I would increase the radius about 4 studs each time.

You of course set an upper limit, say a radius of 10 studs. You can also get better accuracy if instead of using 1 raycast to check if its cover, if you instead use a few in more of a grid formation (or maybe use blockcast or spherecast) so that you can be more sure it covers the entire body.

You can also use raycasts to simply check if the cover is short or tall if you want them to crouch or stand.

This worked really well for me, hope it does for you too (:

1 Like

Thanks for your reply and sorry for the really delayed response but could you give me an explanation of how the grid system would work? I tried creating a semi circle and raycasting toward the player but my results weren’t accurate and sometimes completely wrong when the player stands at certain locations.

Thanks for the reply I will keep in mind to use this when it comes to determining the height of the cover. Although Right now I am basically trying to create procedurally generated cover points that an NPC can use.

1 Like

I am not sure what you mean.

It is not a grid system.

Here is some pseudo code that might help?

>generate points in a circle around character
>raycast from each point to the target to hide from
>if raycast is nil, it means there is line of sight, not a good place to hide
>if raycast is not nil, it means it is hidden from the target, return this point

You can generate points in a circle around a character by just

local character = ...
local nPoints = 8
local radius = 4

for i = 1, nPoints do
   local angle = (math.pi*2 / nPoints) * i -- math.pi is 180 degrees in radians, so 2 pi is 360 degrees
   local point = character.PrimaryPart.CFrame * CFrame.Angles(0, angle, 0) * CFrame.new(0, 0, radius)
end

Oh sorry I meant raycasting in a grid formation like you said earlier, I just wanted to know more on that. And also like I said when I tried raycasting in a circle around the character and towards the player the result was not accurate.

For raycasting in a grid, I mean like blockcast WorldRoot | Documentation - Roblox Creator Hub

What do you mean not accurate? Can I see your code.

I saw blog that actually uses raycast in a grid formation to find cover points so i assumed you meant that. Anyways here is the code:

local Players = game:GetService("Players")

local NoRayCast = 180 
local humRoot
local maxDistance = 100

local Cover = {}

function FindNearestPlr(NPC)
	local nearestPlayer, nearestDistance, nearestChar
	for _, player in pairs(Players:GetPlayers()) do
		local character = player.Character
		local distance = player:DistanceFromCharacter(humRoot.Position)
		if not character or 
			distance > maxDistance or
			(nearestDistance and distance >= nearestDistance)
		then
			continue
		end
		nearestDistance = distance
		nearestPlayer = player
		nearestChar = player.Character
	end
	print("The nearest player is", nearestPlayer)
	FindCover(NPC, nearestChar)
	return nearestChar
end

function FindCover(NPC, ClosestPlr)
	if ClosestPlr then
		local Config = NPC.Config
		local NPC_CFrame = NPC.HumanoidRootPart.CFrame
		local Radius = 100
		
		local RayPrams = RaycastParams.new()
		RayPrams.FilterType = Enum.RaycastFilterType.Exclude
		RayPrams.FilterDescendantsInstances = {game.Workspace.RayCastObj:GetChildren(), game.Workspace.SWAT_NPC:GetChildren()}
		
		for r = 0,1, 1/NoRayCast do
			local part = Instance.new("Part", workspace.RayCastObj) 
			part.Anchored = true
			part.Size = Vector3.new(1, 1, 1)
			part.BrickColor = BrickColor.new("Really red")
			part.CFrame = NPC_CFrame * CFrame.Angles(0, math.rad(r*180 - 90), 0) * CFrame.new(0,0, Radius)
			
			local Result = workspace:Raycast(part.Position, ClosestPlr.HumanoidRootPart.Position - part.Position, RayPrams)
			if Result and Result.Instance:IsDescendantOf(ClosestPlr) == false then
				table.insert(Cover, Result.Instance)
			end
		end
		print(Cover)
	end
	return Cover
end

for i, NPC in ipairs(game.Workspace.SWAT_NPC:GetChildren()) do
	wait(5)
	humRoot = NPC.HumanoidRootPart
	FindNearestPlr(NPC)
end

The Code creates a Circle from the NPCs position and a raycast is created at each part that points towards the player. When I say that the result is not accurate i meant that sometimes when there is cover that have different rotations or when the player is too close to cover the raycast will return a cover that will not cover the NPC. The table that is printed Contains all the raycast results. (The creation of parts are just for visualization btw.)

So there are a few odd things going on in your code but I figure its easier for me to just give you a basic working example and let you take your time and look at that, feel free to send me any questions.

In a localscript:

local RunService = game:GetService("RunService")

local character = workspace:WaitForChild("Askavix") -- change this
local target = workspace:WaitForChild("Rig") -- change this

-- this is just for showing where the cover position is
local coverBeacon = Instance.new("Part")
coverBeacon.Size = Vector3.new(1, 1, 1)
coverBeacon.Anchored = true
coverBeacon.Color = Color3.fromRGB(49, 183, 255)
coverBeacon.CanCollide = false
coverBeacon.Name = "CoverBeacon"
coverBeacon.Parent = workspace

local function findCover(hider, seeker, raycastParams)
	-- hider is the person trying to find cover
	-- seeker is the target to find cover from

	local hiderRoot = hider:FindFirstChild("HumanoidRootPart")
	local seekerRoot = seeker:FindFirstChild("HumanoidRootPart")

	if not hiderRoot then
		return
	end

	if not seekerRoot then
		return
	end

	local radius = 4 -- how far we expand the search each time
	local nPoints = 8 -- how many points we will check in a circle
	local maxRadiusSteps = 5 -- how many times it will increase the radius before giving up

	local radiusStep = 0 -- counter for the amount of times we have expanded the search

	local coverPosition

	repeat
		radiusStep += 1

		for i = 1, nPoints do
			local angle = math.pi*2/nPoints * i

			local cf = hiderRoot.CFrame * CFrame.Angles(0, angle, 0) * CFrame.new(0, 0, radius*radiusStep)

			local origin = cf.Position

			local direction = (seekerRoot.Position - origin)

			if not raycastParams then
				raycastParams = RaycastParams.new()
				raycastParams.FilterType = Enum.RaycastFilterType.Exclude
				raycastParams.FilterDescendantsInstances = {hider, seeker, coverBeacon}
			end 

			local raycastResult = workspace:Raycast(origin, direction, raycastParams)
			-- using blockcast would be better for a player shape, but you would want to make sure it isnt clipping into the ground at all, otherwise you will get inaccurate results

			if raycastResult then -- meaning we dont have line of sight, so its a suitable cover position
				coverPosition = origin
				break
			end
		end	

	until coverPosition or radiusStep > maxRadiusSteps

	return coverPosition
end

-- visualize the cover
RunService.Heartbeat:Connect(function()
	local coverPosition = findCover(character, target)

	if coverPosition then
		coverBeacon.Position = coverPosition
		coverBeacon.Transparency = 0
	else
		coverBeacon.Transparency = 1
	end
end)