I’m making a stupid game with my friend and attempted to obtain
an AI script… with the help of AI.
I know a little bit about scripting just not about AI scripting specifically. So I asked… AI to help me ![]()
Well womp womp. After literal hours of messing around with our little game, we’ve decided to actually try to make something at least slightly fun… So we did. We made a very basic horror game.
But…
As I started to finish up on our map, I decided to try to improve the AI. Wellll. It worked. But barely.
I would be very happy if someone could help me understand what is wrong with this awful looking disaster. Please ![]()
SCRIPT:
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local PathfindingService = game:GetService("PathfindingService")
-- Monster settings
local MONSTER_SPEED = {
Normal = 12,
Chasing = 24
}
local VISION_RANGE = 50
local VISION_ANGLE = 90
local KILL_RANGE = 3 -- Currently informational, kill is by Touched event
local WAYPOINT_REACH_DISTANCE = 5
local PATH_UPDATE_INTERVAL = 1 -- Seconds between path updates
local STUCK_THRESHOLD = 2 -- Seconds without moving to be considered stuck
local SEARCH_MODE_DURATION = 10 -- How long to search after losing player
-- Get references
local monster = script.Parent
local humanoid = monster:WaitForChild("Humanoid")
local rootPart = monster:WaitForChild("HumanoidRootPart")
local waypointsFolder = monster.Parent:WaitForChild("Waypoints")
local waypoints = waypointsFolder:GetChildren()
-- State variables
local currentTarget = nil
local isChasing = false
local isSearching = false
local lastPlayerKilled = nil
local lastKillTime = 0
local lastPathUpdate = 0
local currentPath = nil
local waypointIndex = 1
local lastPosition = rootPart.Position
local lastMoveTime = os.time()
local lastSightTime = 0
local lastKnownPlayerPosition = nil
-- Initialize pathfinding
local pathfinder = PathfindingService:CreatePath({
AgentRadius = 2,
AgentHeight = 5,
AgentCanJump = true,
AgentCanClimb = true
})
-- Follow the computed path
local function followPath(destination)
if not rootPart or not humanoid or humanoid.Health <= 0 then return false end
-- Compute new path if needed
local needsNewPath = (os.time() - lastPathUpdate > PATH_UPDATE_INTERVAL) or (not currentPath)
if needsNewPath then
local success, errorMessage = pcall(function()
pathfinder:ComputeAsync(rootPart.Position, destination)
end)
if not success then
warn("Pathfinding ComputeAsync error for "..monster.Name..": "..errorMessage)
currentPath = nil
return false
end
lastPathUpdate = os.time()
if pathfinder.Status == Enum.PathStatus.Success then
currentPath = pathfinder:GetWaypoints()
waypointIndex = 1
else
-- Path computation failed (e.g., NoPath)
-- warn("Pathfinding failed for "..monster.Name.." to destination: "..tostring(destination).." Status: "..tostring(pathfinder.Status))
currentPath = nil
return false -- Indicate failure to establish a path
end
end
-- Follow current path waypoints
if currentPath and waypointIndex <= #currentPath then
local nextWaypoint = currentPath[waypointIndex]
-- Only update move target if not already moving there
if (humanoid:GetState() ~= Enum.HumanoidStateType.Running) or
((humanoid.MoveDirection - (nextWaypoint.Position - rootPart.Position).Unit).Magnitude > 0.1) then
humanoid:MoveTo(nextWaypoint.Position)
end
-- Check if reached waypoint
if (rootPart.Position - nextWaypoint.Position).Magnitude < WAYPOINT_REACH_DISTANCE then
waypointIndex = waypointIndex + 1
end
-- Movement detection
local movedDistance = (rootPart.Position - lastPosition).Magnitude
if movedDistance > 0.1 then -- Threshold for detecting movement
lastMoveTime = os.time()
lastPosition = rootPart.Position
elseif (os.time() - lastMoveTime) > STUCK_THRESHOLD then
-- Stuck detection
-- warn(monster.Name.." appears to be stuck, recomputing path to "..tostring(destination))
currentPath = nil -- Force path recompute by clearing currentPath
-- lastPathUpdate is NOT reset here, to allow PATH_UPDATE_INTERVAL to govern frequent recomputes if truly stuck.
-- Or, reset lastPathUpdate = 0 to force immediate recompute next call. Let's keep it as is for now.
return false -- Indicate failure to make progress
end
return true -- Made progress or still on path
end
-- If no currentPath or path is exhausted
return false
end
-- Vision cone check
local function isInVisionCone(player)
local character = player.Character
if not character then return false end
local humanoidRoot = character:FindFirstChild("HumanoidRootPart")
if not humanoidRoot then return false end
local toPlayer = (humanoidRoot.Position - rootPart.Position)
local distance = toPlayer.Magnitude
if distance > VISION_RANGE then return false end
if distance == 0 then return true end -- Already at the player
local monsterForward = rootPart.CFrame.LookVector
local toPlayerDirection = toPlayer.Unit
local dotProduct = monsterForward:Dot(toPlayerDirection)
if dotProduct <= 0 and VISION_ANGLE < 180 then return false end
if dotProduct > 1 then dotProduct = 1 elseif dotProduct < -1 then dotProduct = -1 end
local angle = math.deg(math.acos(dotProduct))
if angle > (VISION_ANGLE / 2) then return false end
local raycastParams = RaycastParams.new()
raycastParams.FilterDescendantsInstances = {monster}
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
local raycastResult = workspace:Raycast(
rootPart.Position,
toPlayerDirection * distance,
raycastParams
)
return not raycastResult or raycastResult.Instance:IsDescendantOf(character)
end
local function canSeePlayer(player)
if player == lastPlayerKilled and (os.time() - lastKillTime) < 5 then
return false
end
return isInVisionCone(player)
end
local function selectSearchWaypoint()
if #waypoints == 0 then
warn("Pathfinding script in "..monster.Name..": No waypoints found in "..waypointsFolder.Name..". Monster will be idle unless a player is seen.")
return nil
end
if isSearching and lastKnownPlayerPosition then
local closestWaypoint = nil
local closestDistance = math.huge
for _, waypoint in waypoints do
if waypoint:IsA("BasePart") then
local distance = (waypoint.Position - lastKnownPlayerPosition).Magnitude
if distance < closestDistance then
closestDistance = distance
closestWaypoint = waypoint
end
end
end
if closestWaypoint then
return closestWaypoint
end
end
local potentialTargets = {}
for _, waypoint in waypoints do
if waypoint:IsA("BasePart") and waypoint ~= currentTarget then
table.insert(potentialTargets, waypoint)
end
end
if #potentialTargets > 0 then
return potentialTargets[math.random(1, #potentialTargets)]
end
if #waypoints > 0 then
return waypoints[math.random(1, #waypoints)]
end
return nil
end
local function updateTarget()
if humanoid.Health <= 0 then return end
local closestPlayer = nil
local closestDistance = VISION_RANGE + 1
for _, player in Players:GetPlayers() do
if player.Character and player.Character:FindFirstChild("HumanoidRootPart") and player.Character:FindFirstChildOfClass("Humanoid") and player.Character.Humanoid.Health > 0 then
if canSeePlayer(player) then
local distance = (rootPart.Position - player.Character.HumanoidRootPart.Position).Magnitude
if distance < closestDistance then
closestDistance = distance
closestPlayer = player
end
end
end
end
if closestPlayer then
currentTarget = closestPlayer.Character.HumanoidRootPart
isChasing = true
isSearching = false
humanoid.WalkSpeed = MONSTER_SPEED.Chasing
currentPath = nil
lastSightTime = os.time()
lastKnownPlayerPosition = closestPlayer.Character.HumanoidRootPart.Position
else
if isChasing then
isChasing = false
isSearching = true
humanoid.WalkSpeed = MONSTER_SPEED.Normal
currentTarget = selectSearchWaypoint()
currentPath = nil
lastPathUpdate = 0
elseif isSearching then
if (os.time() - lastSightTime) > SEARCH_MODE_DURATION then
isSearching = false
end
if not currentTarget or (currentTarget:IsA("BasePart") and (rootPart.Position - currentTarget.Position).Magnitude < WAYPOINT_REACH_DISTANCE) then
currentTarget = selectSearchWaypoint()
currentPath = nil
lastPathUpdate = 0
end
else
if not currentTarget or (currentTarget:IsA("BasePart") and (rootPart.Position - currentTarget.Position).Magnitude < WAYPOINT_REACH_DISTANCE) then
currentTarget = selectSearchWaypoint()
currentPath = nil
lastPathUpdate = 0
end
end
end
if not currentTarget and #waypoints > 0 then
currentTarget = selectSearchWaypoint()
currentPath = nil
lastPathUpdate = 0
end
end
-- Kill handling
monster.HumanoidRootPart.Touched:Connect(function(hit)
if not isChasing or humanoid.Health <= 0 then return end
local hitModel = hit.Parent
if not hitModel then return end
local player = Players:GetPlayerFromCharacter(hitModel)
if player and player.Character then
local targetHumanoid = player.Character:FindFirstChildOfClass("Humanoid")
if targetHumanoid and targetHumanoid.Health > 0 then
if (rootPart.Position - player.Character.HumanoidRootPart.Position).Magnitude <= KILL_RANGE + 2 then
targetHumanoid.Health = 0
lastPlayerKilled = player
lastKillTime = os.time()
isChasing = false
humanoid.WalkSpeed = MONSTER_SPEED.Normal
currentTarget = nil
currentPath = nil
lastPathUpdate = 0
task.spawn(updateTarget)
end
end
end
end)
-- Main loop
RunService.Heartbeat:Connect(function(deltaTime)
if not rootPart or not humanoid or humanoid.Health <= 0 then return end
if os.time() % 0.5 < (deltaTime * 1.1) then
updateTarget()
end
if currentTarget then
if isChasing then
if currentTarget and currentTarget.Parent and currentTarget:IsA("BasePart") then
local targetPosition = currentTarget.Position
local directionToTarget = (targetPosition - rootPart.Position)
if directionToTarget.Magnitude > 0.5 then
if humanoid:GetState() ~= Enum.HumanoidStateType.Running or
(humanoid.MoveDirection - directionToTarget.Unit).Magnitude > 0.2 then
humanoid:MoveTo(targetPosition)
end
end
else
isChasing = false
currentTarget = nil
currentPath = nil
lastPathUpdate = 0
humanoid.WalkSpeed = MONSTER_SPEED.Normal
updateTarget()
end
else -- Patrolling or searching
if currentTarget and currentTarget:IsA("BasePart") then
local pathSuccess = followPath(currentTarget.Position)
if not pathSuccess then
-- followPath returned false. This means either:
-- 1. Monster is stuck (currentPath was cleared by followPath, will recompute for SAME target next tick).
-- 2. PathfindingService:ComputeAsync failed (e.g., NoPath).
-- In this case, pathfinder.Status reflects the failure, and currentPath is nil.
-- Only select a NEW currentTarget if the path was definitively impossible (NoPath).
if currentPath == nil and pathfinder.Status == Enum.PathStatus.NoPath then
-- warn(monster.Name .. " found NoPath to " .. currentTarget.Name .. ". Selecting new waypoint.")
currentTarget = selectSearchWaypoint()
-- currentPath is already nil from followPath's failure.
lastPathUpdate = 0 -- Force immediate path computation for the new target.
end
-- If it was just "stuck" (and pathfinder.Status wasn't NoPath),
-- currentPath is nil (set by followPath's stuck detection or compute failure).
-- The monster will keep the same currentTarget and followPath will try to recompute on the next Heartbeat.
end
elseif not currentTarget and #waypoints > 0 then
-- No current target, but waypoints exist. Try to get one.
currentTarget = selectSearchWaypoint()
currentPath = nil
lastPathUpdate = 0
end
end
else
if humanoid:GetState() == Enum.HumanoidStateType.Idle or humanoid:GetState() == Enum.HumanoidStateType.None then
updateTarget()
end
end
end)
-- Initial target setup
if #waypoints > 0 then
currentTarget = selectSearchWaypoint()
else
warn(monster.Name .. " has no waypoints for initial setup. Will wait for player or target update.")
end
updateTarget()
This is the freshly baked AI version. I know most people hate AI, but we were just trying to have fun. I would genuinely love to see this AI work efficiently, and learn something about AI in the process. Just the AI that we’re using has taught me a lot of basic stuff about Pathfinding.
Thanks a tons!