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.