Issues with Pathfinding (Brackens type NPC)

Hello fellow devs, to keep it short, I would like to make a Hallucination of a player’s friend that kind of acts like Bracken from lethal company, with the differences being:

It does not kill you
It stalks the player at a specified range
The whole goal is to, stalk the player → if the player can see It then → look at player → play laughing animation(animation bit not done yet) → select a tile whose “TileCenter” is not seen → run to that tile until It cannot be seen by the player → if line of sight is broken → remove(working on a respawn method)

This is basically it, the comments in the script describe it better some of the more important parts.

Issue:

The pathfinding works along with “Tiles” that I’ve made (Each tile is a model. every tile has a “TileCenter” Part, every tile belongs to workspace.Map)

Because of this, It uses constantly the “CheckRaycastSight()” function, which I suspect to be faulty.
“CheckRaycastSight()”, on the scope of " if state == ‘wandering’ " and “if state == ‘escaping’” is used to check if the “TileCenter” of SelectedTile can be seen → if it is seen, then → just move to it. → else → call GetPath(), which returns a path from “my_root” to “goal”.

The undesired behaviour: Character moving to "TileCenter"s which are blocked by a wall(pathfiding is possible, of course.) but is just running up to walls, and sometimes does not even move at all.

What I’ve tried:
Changing the script completely over 3 times, but never recognised that it may have been a Raycasting problem. (Since, before the changes, it seemed to work fine for “if state == ‘wandering’”)

The code is quit lengthy, I did try my best to comment on it. Please excuse my beginner commenter and variable-naming skills as I’ve only commented on weeks-old-code just recently.

Reminder: Raycasting may be the faulty bit. (not sure)

--[[
	WORKFLOW:
	
	Look for a player.
	Steal one of the players friend's look.
	Spawn where the Player does not see.
	Go to a nearby tile.
	wander around in the target's general location.
	When It the player spots this character, laugh.
	
	When laughing animation ends, enable "escaping" state.
	Once line of sight is broken, delete and set state to "inactive".
	Restart.`
	
	This is a client hallucination.
	This should spawn on client.
	
	ISSUE: CHARACTER DOES NOT MOVE
		Located at "if state == "wander", might be happening in other places too.
		Pay special Attention to "CheckSightRaycast()" and "CheckSightAbsolute()"
--]]

repeat
	task.wait()
until #game.Players:GetPlayers() > 0

task.wait(5)

print("loaded")

local player = game.Players.LocalPlayer
local player_character = player.Character
local camera = workspace.CurrentCamera
local my_char = game.Workspace.Friend:Clone()
my_char.Parent = workspace
local my_humanoid = my_char.Humanoid
local my_root = my_char.HumanoidRootPart

local PathfindingService = game:GetService("PathfindingService")
local IsPathfindingInProgress = false

local stalk_scout_range = 1901


--debugpart = Instance.new("Part",workspace)
--debugpart.Anchored = true
--debugpart.CanCollide = false
--debugpart.Transparency = 0.5
--debugpart.CanQuery = false


function GetTilesWithinRangeOfPos(pos : Vector3, range : number, map)
	local tiles = map:GetChildren()
	local near_tiles = {}
	for _, tile in tiles do
		if (pos - tile.TileCenter.Position).magnitude < range then
			table.insert(near_tiles, tile)
		end
	end

	return near_tiles
end

function GetDotTargetInLookerFOV(looker, target)
	local lookerToTarget =  (target.Position-looker.Position).Unit
	local dot = lookerToTarget:Dot(looker.CFrame.LookVector)
	return dot
end

function GetNearestTile(tilesList : {any})
	local old_dist = (my_root.Position - tilesList[1].TileCenter.Position).Magnitude
	local nearest_tile = nil

	for _, tile in tilesList do
		local dist = (my_root.Position - tile.TileCenter.Position).Magnitude
		if  dist < old_dist and dist > 20 then -- tile size is 20
			old_dist = (my_root.Position - tile.TileCenter.Position).Magnitude
			nearest_tile = tile
		end
	end
	
	if nearest_tile then
		nearest_tile.TileCenter.Transparency = 0
		nearest_tile.TileCenter.BrickColor = BrickColor.Yellow()
	end

	return nearest_tile
end

function CheckRaycastSight(looker, target, ignore)
	local rayCheck = false

	local rayParams = RaycastParams.new()
	rayParams.FilterType = Enum.RaycastFilterType.Exclude
	rayParams.FilterDescendantsInstances = ignore

	local ray = workspace:Raycast(
		looker.Position, 
		(target.Position - looker.Position).Unit*999, 
		rayParams)

	if ray then
		---debugpart.Position = ray.Position
		if ray.Instance then
			if target.Parent:FindFirstChildOfClass("Humanoid") then
				if ray.Instance:IsDescendantOf(target) then
					rayCheck = true
				end
			else
				if ray.Instance == target then
					rayCheck = true
				end
			end
		end
	end
	
	print("RAYCHECK FOR LOOKER ".. looker.Name.. " TO TARGET ".. target.Name.. " RETURNS: ".. tostring(rayCheck))
	return rayCheck
end

function CheckSightAbsolute(looker, target, ignore) -- raycast AND FoV
	local InFoV = GetDotTargetInLookerFOV(looker, target)
	local rayCheck = CheckRaycastSight(looker, target, ignore)
	
	if rayCheck == true and InFoV > 0 then
		return true
	else
		return false
	end
end

function GatherAllCenterTiles(map, exeption)
	local tiles = map:GetChildren()
	local centers = {}
	for _, tile in tiles do
		if exeption ~= nil then
			if exeption ~= tile then
				table.insert(centers, tile.TileCenter)
			end
		end
	end
	return centers
end

function GetPath(goal)
	local Path = PathfindingService:CreatePath({
		AgentHeight = 6,
		AgentRadius = 6,
		AgentCanJump = false
	})
	
	Path:ComputeAsync(my_root.Position, goal.Position)
	
	return Path
end

function SpawnToTile(tile)
	my_root.CFrame = tile.TileCenter.CFrame
end

function Disguise()
	local PlayersFriends = {}

	local success, page = pcall(function() return game.Players:GetFriendsAsync(player.UserId) end)
	if success then
		repeat
			task.wait()
			local info = page:GetCurrentPage()
			for i, friendInfo in pairs(info) do
				table.insert(PlayersFriends, friendInfo)
			end
			if not page.IsFinished then 
				page:AdvanceToNextPageAsync()
			end
		until page.IsFinished
	end

	if #PlayersFriends > 0 then
		local selected_friend = PlayersFriends[1]
		local disguise_appearance = game.Players:GetHumanoidDescriptionFromUserId(selected_friend.Id)
		my_humanoid:ApplyDescription(disguise_appearance)
		print("diguised as: ".. selected_friend.Username)
	else
		warn("Player has no friends..")
	end
end

local SelectedTile = nil
local escape_tile = nil
local spawned = false
local state : string = "inactive" -- [inactive, wandering, laughing, escaping]
local unseen_tiles = {}

function _UpdateUnseenTilesThread()
	while true do
		task.wait()
		local tiles = workspace.Map:GetChildren()
		for _, tile in tiles do
			local IsSeen = CheckSightAbsolute(player_character.PrimaryPart, tile.TileCenter, {my_char, player_character, GatherAllCenterTiles(workspace.Map, tile)}) == true
			if IsSeen == false then
				table.insert(unseen_tiles, tile)
			elseif table.find(unseen_tiles, tile) and IsSeen == true then
				table.remove(unseen_tiles, table.find(unseen_tiles, tile))
			end
		end
	end
end

function _AI_MainThread()
	while true do
		task.wait()
		print("state: ".. state)
		local CanPlayerSeeMe = CheckSightAbsolute(my_root, player_character.PrimaryPart, player_character:GetChildren()) == true
		if state == "wandering" then
			if SelectedTile ~= nil and CanPlayerSeeMe == false then
				--[[
				Pathfind to selected tile.
				If the tile is seen by a raycast, then no need to use pathfinding.
				If Player can see this character then initiate laughing state.
				--]]
				local t_center = SelectedTile.TileCenter
				local distance = (my_root.Position - t_center.Position).Magnitude
				
				if CheckRaycastSight(my_root, t_center, my_char:GetChildren()) == true then
					print("i can see em")
					my_humanoid:MoveTo(t_center.Position)
				else
					local path = GetPath(t_center)
					if path.Status == Enum.PathStatus.Success then
						local path_waypoints = path:GetWaypoints()

						for i = 2, #path_waypoints do -- ditch the 1st index.
							if CheckRaycastSight(my_root, t_center, my_char:GetChildren()) == true or CheckSightAbsolute(player_character.PrimaryPart, my_root, player_character:GetChildren()) == true then
								-- found t_center by raycast, no need to pathfind.
								-- if the player is looking, break pathfinding.
								path:Destroy()
								break
							else
								my_humanoid:MoveTo(path_waypoints[i].Position)
							end
						end
					end
				end
				
				if distance < 5 then
					SelectedTile = nil -- this will make teh script select another tile.
				end
			elseif SelectedTile == nil and CanPlayerSeeMe == false then
				-- select a tile if tile is null
				local potential_tiles = GetTilesWithinRangeOfPos(
					my_root.Position, -- where it should start looking from.
					stalk_scout_range, -- the range
					workspace.Map -- the map which constains the tiles.
				)
				
				SelectedTile = potential_tiles[math.random(1, #potential_tiles)]
			elseif CanPlayerSeeMe == true then
				SelectedTile = nil
				state = "laughing"
			end
		end
		
		if state == "laughing" then
			-- play laughing animation and turn to player
			state = "escaping"
			task.wait(4)
		end
		
		if state == "escaping" then
			--[[
			Select any tile that the player cannot see,
			preferably, a tile that is not directly behind the player.
			when a tile is found: 
				pathfind to that tile until character is not longer seen by CheckSightAbsolute()
			--]]
			
			local potential_tiles = unseen_tiles
			local preferable_tiles = {}
			local chosen_tiles = {} -- the chosen tiles.
			
			--[[
			preferable_tiles are tiles that are not directly behind the player.
			These are found by using raycasting.
			--]]
			
			for _, p_tile in potential_tiles do
				if CheckRaycastSight(player_character.PrimaryPart, p_tile.TileCenter, {player_character}) == false then
					table.insert(preferable_tiles, p_tile)
				end
			end
			
			if SelectedTile == nil then
				if #preferable_tiles > 0 then
					chosen_tiles = preferable_tiles
				else
					chosen_tiles = potential_tiles
				end
				
				SelectedTile = GetNearestTile(chosen_tiles)
				
			elseif SelectedTile ~= nil and CanPlayerSeeMe == true then
				local path = GetPath(SelectedTile.TileCenter.Position)

				if path.Status == Enum.PathStatus.Success then
					local waypoints = path:GetWaypoints()
					for i = 2, #waypoints do -- so it starts at index 2. index 1 is useless.
						if CanPlayerSeeMe == true then
							my_humanoid:MoveTo(waypoints[i].Position)
							my_humanoid.MoveToFinished:Wait()
						else
							path:Destroy()
							break
						end
					end
				end
			elseif CanPlayerSeeMe == false then
				my_char:Remove()
				print("woosh..")
				state = "null"
			end
		end
	end
end

task.delay(0, _UpdateUnseenTilesThread)

repeat
	task.wait()
until #unseen_tiles > 0

if state == "inactive" and spawned == false then
	print("state is inactive")
	Disguise()
	task.wait(1)
	SpawnToTile(unseen_tiles[math.random(1, #unseen_tiles)])
	state = "wandering"
	spawned = true
	print("set to active")
	
	task.delay(0,_AI_MainThread)
	
	print("threads started.")
end

Hey… I wouldn’t mind if you found other issues too. :slight_smile:

1 Like

Little clarification: the raycasting seems to be working according to my handy Debug Part, maybe it’s the way I implemented it to other things? Unsure.

[This is also in a LocalScript in StarterCharacterScripts]

1 Like

Problem solved!
Turns out, the pathfinding, of all things was just slightly off. It was being called, and called again and again. it was also failing to complete itself(I don’t know why), but anyhow, my_humanoid.MoveToFinished:Wait() along with some extra fine-tuning seemed to have resolved it.

¯_(ツ)_/¯

1 Like

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