For some reason, the NPC is able to walk only on land. It cannot seem to jump onto or off anything. The pathfinding works and generates a reasonable path, but for some reason it just does not move to the waypoints. (Each set of waypoints has a different colour)
I’m sure I set AgentCanJump to true for this NPC. In fact, sometimes the NPC is unable to follow paths even if it does not consist of jumping – it can fail to follow paths if an obstacle is between two waypoints (which should not happen in the first place?)
Full Code
(This is very long, apologies; there’s a simplified algorithm below)
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local PathfindingService = game:GetService("PathfindingService")
local SoundLibrary = require(game.ReplicatedStorage.WeaponSoundService)
local Enumerations = require(game.ReplicatedStorage.Enumerations)
local guard = script.Parent.Parent
local animations = require(script.Parent.Parent.Animations.AnimationHandler)
local firearm_data = require(guard.Firearm.WeaponData)
local isSuspicious = false
local isFiring = false
local firingCoro: coroutine
local facingCoro: coroutine
local suspicion = guard.Behaviour.Suspicion -- decimal, percentage: 0 is nothing, 1 is full
local lastKnownPos: Vector3 = nil
local MAX_RANGE = 40
local PHYSICAL_RANGE = 5
local GUARD_FOV = 70
local state = Enumerations.GuardState.PEACEFUL
local path = Enumerations.getGuardPath(guard.Behaviour.GuardType.Value)
local waypoints
local nextWaypointIndex = 2
local reachedConnection
local blockedConnection
local function followPath(character, path, destination: Vector3)
local success, errorMessage = pcall(function()
path:ComputeAsync(character.HumanoidRootPart.Position, destination)
end)
if success and path.Status == Enum.PathStatus.Success then
waypoints = path:GetWaypoints()
-- Detect if path becomes blocked
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
if blockedWaypointIndex >= nextWaypointIndex then
blockedConnection:Disconnect()
followPath(character, path, character.HumanoidRootPart.Position)
end
end)
local humanoid = character:FindFirstChild("Humanoid")
-- Detect when movement to next waypoint is complete
if not reachedConnection then
reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
if reached and nextWaypointIndex < #waypoints then
-- Increase waypoint index and move to next waypoint
nextWaypointIndex += 1
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
else
reachedConnection:Disconnect()
blockedConnection:Disconnect()
end
end)
end
-- Initially move to second waypoint (first waypoint is path start; skip it)
nextWaypointIndex = 2
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
local c = Color3.new(math.random(0, 255), math.random(0, 255), math.random(0, 255))
for _, waypoint in waypoints do
local part = Instance.new("Part", workspace)
part.Size = Vector3.new(1, 1, 1)
part.Anchored = true
part.CanCollide = false
part.Transparency = 0
part.Position = waypoint.Position
part.Material = Enum.Material.Neon
part.Color = c
task.delay(10, function() part:Destroy() end)
end
else
warn("Path not computed!", errorMessage)
end
end
local function raycastToPlayer(player)
local distance: Vector3 = player.Character.HumanoidRootPart.CFrame.Position - guard.HumanoidRootPart.CFrame.Position
local relPos = guard.HumanoidRootPart.CFrame:PointToObjectSpace(player.Character.HumanoidRootPart.CFrame.Position)
local raycast_origin = guard.Head.CFrame.Position
local raycast_direction = (player.Character.Head.CFrame.Position - raycast_origin).Unit
local raycast_params = RaycastParams.new()
raycast_params.FilterDescendantsInstances = {player.Character, guard, workspace.BulletHoles}
raycast_params.FilterType = Enum.RaycastFilterType.Exclude
raycast_params.IgnoreWater = true
return workspace:Raycast(raycast_origin, raycast_direction * distance.Magnitude, raycast_params), distance
end
local function withinFOV(distance: Vector3)
return guard.Head.CFrame.LookVector:Dot(distance.Unit) > math.cos(math.rad(GUARD_FOV))
end
local function findAngle(positionA, positionB)
local cf = CFrame.new(positionA, positionB)
local targetX, targetY, targetZ = cf:ToEulerAnglesXYZ()
return CFrame.Angles(targetX, targetY, targetZ)
end
function play_sound(sound: string)
local sound: Sound = SoundLibrary.get_sound(firearm_data.metadata.identifier, sound)
sound.Parent = guard.Firearm.MuzzleFlash
sound.Name = "NPCSound"
sound.RollOffMinDistance = 5
sound.RollOffMaxDistance = 60
sound.Ended:Connect(function() sound:Destroy() end)
sound:Play()
end
function fire()
animations.animation_table.fire:Play()
guard.Firearm.MuzzleFlash.ParticleEmitter:Emit(1)
guard.Firearm.MuzzleFlash.PointLight.Enabled = true
play_sound("fire")
task.delay(0.025, function() guard.Firearm.MuzzleFlash.PointLight.Enabled = false end)
end
-- Periodically check for physical contact
while wait(0.25) and state == Enumerations.GuardState.PEACEFUL do
for _, player in Players:GetPlayers() do
local result, distance = raycastToPlayer(player)
if not result then
-- No intersections / guard can see player
if distance.Magnitude <= PHYSICAL_RANGE then
isSuspicious = true
suspicion.Value = 1
elseif withinFOV(distance) then
if distance.Magnitude <= MAX_RANGE then
isSuspicious = true
suspicion.Value += 0.5 * math.clamp(1 - distance.Magnitude / MAX_RANGE, 0, 1)
end
end
end
suspicion.Value = math.clamp(suspicion.Value, 0, 1)
if suspicion.Value == 1 then
lastKnownPos = player.Character.Head.CFrame.Position
followPath(guard, path, lastKnownPos)
animations.animation_table.aim:Play()
state = Enumerations.GuardState.SEARCH
task.delay(animations.animation_table.aim.Length, function()
animations:stop_animations()
animations.animation_table.aim_hold:Play()
end)
break
end
end
if not isSuspicious then
suspicion.Value -= 0.05
end
isSuspicious = false
end
isSuspicious = true
while wait(0.25) do
if state == Enumerations.GuardState.SEARCH then
local result: RaycastResult
local distance: Vector3
for _, player in Players:GetPlayers() do
result, distance = raycastToPlayer(player)
if not result and distance.Magnitude <= MAX_RANGE then
if distance.Magnitude <= PHYSICAL_RANGE then
lastKnownPos = player.Character.Head.CFrame.Position
isSuspicious = true
suspicion.Value = 1
break
elseif withinFOV(distance) then
lastKnownPos = player.Character.Head.CFrame.Position
isSuspicious = true
suspicion.Value += math.clamp(1 - distance.Magnitude / MAX_RANGE, 0.35, 1)
break
end
end
end
if not isSuspicious then
suspicion.Value -= 0.05
end
suspicion.Value = math.clamp(suspicion.Value, 0, 1)
if suspicion.Value >= 1 then
state = Enumerations.GuardState.ATTACK
end
isSuspicious = false
elseif state == Enumerations.GuardState.ATTACK then
local result: RaycastResult
local distance: Vector3
for _, player in Players:GetPlayers() do
result, distance = raycastToPlayer(player)
if not result and ((withinFOV(distance) and distance.Magnitude <= MAX_RANGE)
or distance.Magnitude <= PHYSICAL_RANGE) then
lastKnownPos = player.Character.Head.CFrame.Position
if not isFiring then
isFiring = true
firingCoro = task.spawn(function()
while wait(60 / firearm_data.statistics.rpm) do
fire()
guard.Behaviour.TargetPosition.Value = player.Character.Head.CFrame.Position
end
end)
guard.AlignOrientation.Attachment1 = player.Character.Torso.NeckAttachment
guard.Behaviour.LineOfSight.Value = true
end
break
end
end
if result or not withinFOV(distance) or distance.Magnitude > MAX_RANGE then
isFiring = false
if firingCoro then task.cancel(firingCoro) end
guard.AlignOrientation.Attachment1 = nil
guard.Behaviour.LineOfSight.Value = false
if lastKnownPos then
followPath(guard, path, lastKnownPos)
end
end
end
end
Pathfinding Parameters
...
return PathfindingService:CreatePath({
AgentRadius = 1,
AgentHeight = 6,
AgentCanJump = true,
AgentCanClimb = true,
Costs = {
Climb = 5
}
})
Guard Logic
- Guard starts in peaceful mode, and once alerted, it switches to search mode
- If alerted in search mode by a player, they switch to attack mode and start attacking the player
- If a player is in sight (raycast + FOV checks) the guard attacks
- If a player is not in sight, the guard goes to the player’s last known position
- Guard should stop walking, even when in the middle of a path, to attack if they see a player