بالتأكيد، يمكنني إنشاء صفحة ويب تحاكي هذا التأثير باستخدام HTML و CSS و JavaScript. سيتحرك عنصر داخل الصفحة بشكل عشوائي.
الآن، عند فتح الصفحة، سترى صندوقًا أزرق يتحرك بشكل عشوائي داخل نافذة المتصفح.
فهمت قصدك الآن. للأسف، لا يمكن لشيفرة JavaScript الموجودة داخل صفحة ويب أن تتحكم مباشرة في نافذة المتصفح الرئيسية (مثل نافذة جوجل كروم أو فايرفوكس) وتحريكها على شاشة المستخدم. هذا يعتبر إجراءً أمنيًا لمنع المواقع الضارة من التحكم في متصفحك.
التأثير الذي تصفه ("You Are An Idiot") كان يعتمد غالبًا على تقنيات قديمة أو برمجيات خبيثة تستغل ثغرات أمنية لم تعد موجودة في المتصفحات الحديثة، أو أنها كانت تفتح نوافذ منبثقة متعددة وتحاول تحريكها (وهو أمر مقيد بشدة الآن).
الحل البديل:
ما يمكننا فعله هو محاكاة هذا التأثير داخل صفحة الويب نفسها. سننشئ عنصرًا يشبه النافذة الصغيرة ونجعله يتحرك عشوائيًا داخل الصفحة الحالية. هذا سيعطي انطباعًا مشابهًا ولكن ضمن حدود الأمان للمتصفح.
الآن، سترى عنصرًا يشبه نافذة صغيرة يتحرك بشكل عشوائي داخل الصفحة. هذا أقرب ما يمكن تحقيقه لمحاكاة التأثير الذي تريده باستخدام تقنيات الويب القياسية والآمنة.
بالتأكيد! يمكننا استخدام PathfindingService في Roblox لإنشاء مسار ذكي للشخصية غير اللاعبة (NPC) لتتبع اللاعب، مع تجنب العقبات. سنقوم أيضًا بتشغيل رسوم متحركة للمشي.
المتطلبات:
Workspace.Humanoid و HumanoidRootPart.Humanoid على Animator. إذا لم يكن موجودًا، أضفه يدويًا في الاستوديو.Animation للمشي تم تحميلها إلى Roblox وحصلت على معرفها (ID).السكربت (ضعه داخل نموذج الـ NPC):
lua --[[ Script Name: NPChasePlayerWithPathfinding Description: Makes the NPC chase the nearest player using PathfindingService and plays a walk animation. Location: Place this script directly inside the NPC Model in Workspace. --]]
-- Services local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") local RunService = game:GetService("RunService")
-- NPC Components local npc = script.Parent local humanoid = npc:WaitForChild("Humanoid") local rootPart = npc:WaitForChild("HumanoidRootPart") local animator = humanoid:WaitForChild("Animator") -- Make sure this exists under Humanoid!
-- Animation Setup local walkAnimation = Instance.new("Animation") -- !!! استبدل هذا بالمعرف الخاص بأنيميشن المشي لديك !!! walkAnimation.AnimationId = "rbxassetid://YOUR_WALK_ANIMATION_ID_HERE" local walkAnimTrack = animator:LoadAnimation(walkAnimation) walkAnimTrack.Looped = true -- اجعل الأنيميشن يتكرر walkAnimTrack.Priority = Enum.AnimationPriority.Action -- أو Movement، حسب الحاجة
-- Configuration local CHASE_DISTANCE = 100 -- أقصى مسافة للمطاردة (بالـ studs) local STOP_DISTANCE = 5 -- المسافة التي يتوقف عندها الـ NPC عن اللاعب local PATH_RECOMPUTE_TIME = 1 -- كم مرة (بالثواني) يتم إعادة حساب المسار
-- Internal Variables local currentTarget = nil local currentPath = nil local currentWaypointIndex = 0 local timeSinceLastPathRecompute = 0
-- Function to find the nearest player within range local function findNearestPlayer() local nearestPlayer = nil local minDistance = CHASE_DISTANCE
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
if character then
local playerRoot = character:FindFirstChild("HumanoidRootPart")
local playerHumanoid = character:FindFirstChild("Humanoid")
if playerRoot and playerHumanoid and playerHumanoid.Health > 0 then
local distance = (rootPart.Position - playerRoot.Position).Magnitude
if distance < minDistance then
minDistance = distance
nearestPlayer = character
end
end
end
end
return nearestPlayer
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
if character then
local playerRoot = character:FindFirstChild("HumanoidRootPart")
local playerHumanoid = character:FindFirstChild("Humanoid")
if playerRoot and playerHumanoid and playerHumanoid.Health > 0 then
local distance = (rootPart.Position - playerRoot.Position).Magnitude
if distance < minDistance then
minDistance = distance
nearestPlayer = character
end
end
end
end
return nearestPlayer
end
-- Function to compute and follow the path local function followPath(targetCharacter) local targetRoot = targetCharacter:FindFirstChild("HumanoidRootPart") if not targetRoot then return end -- Target lost root part
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, -- Adjust based on NPC size
AgentHeight = 6,
AgentCanJump = true -- Allow NPC to jump if needed
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
currentWaypointIndex = 1 -- Start from the first waypoint
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
-- Start walking animation if not already playing
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
-- Move to the first *actual* waypoint (index 2, as 1 is usually the start)
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- If only one waypoint (target is very close/reachable directly)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- No waypoints generated, maybe target is unreachable or too close
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(targetRoot.Position) -- Try moving directly
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
end
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, -- Adjust based on NPC size
AgentHeight = 6,
AgentCanJump = true -- Allow NPC to jump if needed
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
currentWaypointIndex = 1 -- Start from the first waypoint
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
-- Start walking animation if not already playing
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
-- Move to the first *actual* waypoint (index 2, as 1 is usually the start)
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- If only one waypoint (target is very close/reachable directly)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- No waypoints generated, maybe target is unreachable or too close
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(targetRoot.Position) -- Try moving directly
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
end
end
-- Function to handle reaching a waypoint or path interruption local function onWaypointReached(reached) if reached and currentPath and currentWaypointIndex < #currentPath:GetWaypoints() then currentWaypointIndex = currentWaypointIndex + 1 humanoid:MoveTo(currentPath:GetWaypoints()[currentWaypointIndex].Position) else -- Reached end of path or path was interrupted if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end currentPath = nil -- Clear path so it recalculates end end
-- Connect MoveToFinished event humanoid.MoveToFinished:Connect(onWaypointReached)
-- Main loop using Heartbeat for smooth updates RunService.Heartbeat:Connect(function(deltaTime) timeSinceLastPathRecompute = timeSinceLastPathRecompute + deltaTime
currentTarget = findNearestPlayer()
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
if not targetRoot then
-- Target lost root part (maybe died or left)
currentTarget = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop moving
currentPath = nil
return
end
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(rootPart.Position) -- Stop moving precisely
currentPath = nil -- Clear path
return -- Don't recalculate path if already close
end
-- Recalculate path periodically or if no path exists
if timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME or not currentPath then
timeSinceLastPathRecompute = 0
followPath(currentTarget)
end
else
-- No target found
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Optional: Add idle behavior here (e.g., wander)
if humanoid.MoveDirection.Magnitude > 0 then -- If still moving from previous path
humanoid:MoveTo(rootPart.Position) -- Stop moving
end
currentPath = nil
end
currentTarget = findNearestPlayer()
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
if not targetRoot then
-- Target lost root part (maybe died or left)
currentTarget = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop moving
currentPath = nil
return
end
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(rootPart.Position) -- Stop moving precisely
currentPath = nil -- Clear path
return -- Don't recalculate path if already close
end
-- Recalculate path periodically or if no path exists
if timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME or not currentPath then
timeSinceLastPathRecompute = 0
followPath(currentTarget)
end
else
-- No target found
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Optional: Add idle behavior here (e.g., wander)
if humanoid.MoveDirection.Magnitude > 0 then -- If still moving from previous path
humanoid:MoveTo(rootPart.Position) -- Stop moving
end
currentPath = nil
end
end)
شرح:
"rbxassetid://YOUR_WALK_ANIMATION_ID_HERE" بالمعرف الصحيح للأنيميشن الخاص بك.findNearestPlayer: تبحث هذه الدالة عن أقرب لاعب ضمن مسافة CHASE_DISTANCE وتتأكد من أن اللاعب حي.followPath:
PathfindingService:CreatePath() لإنشاء كائن مسار.path:ComputeAsync() لحساب المسار من موقع الـ NPC إلى موقع اللاعب.waypoints).humanoid:MoveTo() للتحرك نحو نقطة الطريق التالية.onWaypointReached: يتم استدعاء هذه الدالة عندما يصل الـ NPC إلى نقطة طريق أو عندما يتم مقاطعة الحركة. إذا كان هناك المزيد من نقاط الطريق، فإنه يتحرك إلى النقطة التالية؛ وإلا فإنه يوقف الأنيميشن ويمسح المسار الحالي.RunService.Heartbeat):
PATH_RECOMPUTE_TIME ثانية أو إذا لم يكن هناك مسار حالي. هذا يضمن أن الـ NPC يتكيف مع حركة اللاعب.STOP_DISTANCE):
كيفية الاستخدام:
Humanoid, HumanoidRootPart, و Animator.Animation للمشي وقم بتحميلها إلى Roblox. انسخ معرفها (Asset ID).Script جديدًا داخل نموذج الـ NPC.YOUR_WALK_ANIMATION_ID_HERE في السكربت بمعرف أنيميشن المشي الخاص بك.حسنًا، سأقوم بتضمين معرف (ID) لأنيميشن مشي شائع من مكتبة Roblox كمثال في السكربت.
تذكر: هذا مجرد مثال. للحصول على أفضل نتيجة، يجب عليك استخدام معرف الأنيميشن الخاص بك الذي قمت بإنشائه أو العثور عليه ويتناسب مع شكل الـ NPC الخاص بك.
كيف تجد أو تحصل على معرف أنيميشن:
ReplicatedStorage)، وانسخ AnimationId من خصائصها.السكربت مع معرف أنيميشن كمثال:
lua --[[ Script Name: NPChasePlayerWithPathfinding Description: Makes the NPC chase the nearest player using PathfindingService and plays a walk animation. Location: Place this script directly inside the NPC Model in Workspace. --]]
-- Services local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") local RunService = game:GetService("RunService")
-- NPC Components local npc = script.Parent local humanoid = npc:WaitForChild("Humanoid") local rootPart = npc:WaitForChild("HumanoidRootPart") local animator = humanoid:WaitForChild("Animator") -- Make sure this exists under Humanoid!
-- Animation Setup local walkAnimation = Instance.new("Animation")
-- !!! مثال لمعرف أنيميشن مشي من Roblox - استبدله بالمعرف الخاص بك إذا أردت !!! walkAnimation.AnimationId = "rbxassetid://180426354" -- (R15 Walk Animation)
local walkAnimTrack = animator:LoadAnimation(walkAnimation) walkAnimTrack.Looped = true -- اجعل الأنيميشن يتكرر walkAnimTrack.Priority = Enum.AnimationPriority.Action -- أو Movement، حسب الحاجة
-- Configuration local CHASE_DISTANCE = 100 -- أقصى مسافة للمطاردة (بالـ studs) local STOP_DISTANCE = 5 -- المسافة التي يتوقف عندها الـ NPC عن اللاعب local PATH_RECOMPUTE_TIME = 1 -- كم مرة (بالثواني) يتم إعادة حساب المسار
-- Internal Variables local currentTarget = nil local currentPath = nil local currentWaypointIndex = 0 local timeSinceLastPathRecompute = 0
-- Function to find the nearest player within range local function findNearestPlayer() local nearestPlayer = nil local minDistance = CHASE_DISTANCE
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
if character then
local playerRoot = character:FindFirstChild("HumanoidRootPart")
local playerHumanoid = character:FindFirstChild("Humanoid")
if playerRoot and playerHumanoid and playerHumanoid.Health > 0 then
local distance = (rootPart.Position - playerRoot.Position).Magnitude
if distance < minDistance then
minDistance = distance
nearestPlayer = character
end
end
end
end
return nearestPlayer
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
if character then
local playerRoot = character:FindFirstChild("HumanoidRootPart")
local playerHumanoid = character:FindFirstChild("Humanoid")
if playerRoot and playerHumanoid and playerHumanoid.Health > 0 then
local distance = (rootPart.Position - playerRoot.Position).Magnitude
if distance < minDistance then
minDistance = distance
nearestPlayer = character
end
end
end
end
return nearestPlayer
end
-- Function to compute and follow the path local function followPath(targetCharacter) local targetRoot = targetCharacter:FindFirstChild("HumanoidRootPart") if not targetRoot then return end -- Target lost root part
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, -- Adjust based on NPC size
AgentHeight = 6,
AgentCanJump = true -- Allow NPC to jump if needed
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
currentWaypointIndex = 1 -- Start from the first waypoint
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
-- Start walking animation if not already playing
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
-- Move to the first *actual* waypoint (index 2, as 1 is usually the start)
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- If only one waypoint (target is very close/reachable directly)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- No waypoints generated, maybe target is unreachable or too close
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(targetRoot.Position) -- Try moving directly
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
end
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, -- Adjust based on NPC size
AgentHeight = 6,
AgentCanJump = true -- Allow NPC to jump if needed
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
currentWaypointIndex = 1 -- Start from the first waypoint
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
-- Start walking animation if not already playing
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
-- Move to the first *actual* waypoint (index 2, as 1 is usually the start)
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- If only one waypoint (target is very close/reachable directly)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- No waypoints generated, maybe target is unreachable or too close
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(targetRoot.Position) -- Try moving directly
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
end
end
-- Function to handle reaching a waypoint or path interruption local function onWaypointReached(reached) if reached and currentPath and currentWaypointIndex < #currentPath:GetWaypoints() then currentWaypointIndex = currentWaypointIndex + 1 humanoid:MoveTo(currentPath:GetWaypoints()[currentWaypointIndex].Position) else -- Reached end of path or path was interrupted if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end currentPath = nil -- Clear path so it recalculates end end
-- Connect MoveToFinished event humanoid.MoveToFinished:Connect(onWaypointReached)
-- Main loop using Heartbeat for smooth updates RunService.Heartbeat:Connect(function(deltaTime) timeSinceLastPathRecompute = timeSinceLastPathRecompute + deltaTime
currentTarget = findNearestPlayer()
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
if not targetRoot then
-- Target lost root part (maybe died or left)
currentTarget = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop moving
currentPath = nil
return
end
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(rootPart.Position) -- Stop moving precisely
currentPath = nil -- Clear path
return -- Don't recalculate path if already close
end
-- Recalculate path periodically or if no path exists
if timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME or not currentPath then
timeSinceLastPathRecompute = 0
followPath(currentTarget)
end
else
-- No target found
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Optional: Add idle behavior here (e.g., wander)
if humanoid.MoveDirection.Magnitude > 0 then -- If still moving from previous path
humanoid:MoveTo(rootPart.Position) -- Stop moving
end
currentPath = nil
end
currentTarget = findNearestPlayer()
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
if not targetRoot then
-- Target lost root part (maybe died or left)
currentTarget = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop moving
currentPath = nil
return
end
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(rootPart.Position) -- Stop moving precisely
currentPath = nil -- Clear path
return -- Don't recalculate path if already close
end
-- Recalculate path periodically or if no path exists
if timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME or not currentPath then
timeSinceLastPathRecompute = 0
followPath(currentTarget)
end
else
-- No target found
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Optional: Add idle behavior here (e.g., wander)
if humanoid.MoveDirection.Magnitude > 0 then -- If still moving from previous path
humanoid:MoveTo(rootPart.Position) -- Stop moving
end
currentPath = nil
end
end)
التغيير الرئيسي:
تم تغيير السطر:
walkAnimation.AnimationId = "rbxassetid://YOUR_WALK_ANIMATION_ID_HERE"
walkAnimation.AnimationId = "rbxassetid://YOUR_WALK_ANIMATION_ID_HERE"
إلى:
walkAnimation.AnimationId = "rbxassetid://180426354" -- (R15 Walk Animation)
walkAnimation.AnimationId = "rbxassetid://180426354" -- (R15 Walk Animation)
الآن السكربت جاهز للاستخدام مع أنيميشن المشي الافتراضي لـ R15. إذا كان الـ NPC الخاص بك يستخدم R6 أو لديك أنيميشن أفضل، فقط استبدل الرقم 180426354 بالمعرف الصحيح._
lua local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") local RunService = game:GetService("RunService")
local npc = script.Parent local humanoid = npc:WaitForChild("Humanoid") local rootPart = npc:WaitForChild("HumanoidRootPart") local animator = humanoid:WaitForChild("Animator")
local walkAnimation = Instance.new("Animation") walkAnimation.AnimationId = "rbxassetid://180426354"
local walkAnimTrack = animator:LoadAnimation(walkAnimation) walkAnimTrack.Looped = true walkAnimTrack.Priority = Enum.AnimationPriority.Action
local CHASE_DISTANCE = 100 local STOP_DISTANCE = 5 local PATH_RECOMPUTE_TIME = 1
local currentTarget = nil local currentPath = nil local currentWaypointIndex = 0 local timeSinceLastPathRecompute = 0
local function findNearestPlayer() local nearestPlayer = nil local minDistance = CHASE_DISTANCE for _, player in ipairs(Players:GetPlayers()) do local character = player.Character if character then local playerRoot = character:FindFirstChild("HumanoidRootPart") local playerHumanoid = character:FindFirstChild("Humanoid") if playerRoot and playerHumanoid and playerHumanoid.Health > 0 then local distance = (rootPart.Position - playerRoot.Position).Magnitude if distance < minDistance then minDistance = distance nearestPlayer = character end end end end return nearestPlayer end
local function followPath(targetCharacter) local targetRoot = targetCharacter:FindFirstChild("HumanoidRootPart") if not targetRoot then return end
local path = PathfindingService:CreatePath({
AgentRadius = 3,
AgentHeight = 6,
AgentCanJump = true
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
currentWaypointIndex = 1
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
humanoid:MoveTo(waypoints[1].Position)
end
else
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(targetRoot.Position)
end
else
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
end
local path = PathfindingService:CreatePath({
AgentRadius = 3,
AgentHeight = 6,
AgentCanJump = true
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
currentWaypointIndex = 1
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
humanoid:MoveTo(waypoints[1].Position)
end
else
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(targetRoot.Position)
end
else
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
end
end
local function onWaypointReached(reached) if reached and currentPath and currentWaypointIndex < #currentPath:GetWaypoints() then currentWaypointIndex = currentWaypointIndex + 1 humanoid:MoveTo(currentPath:GetWaypoints()[currentWaypointIndex].Position) else if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end currentPath = nil end end
humanoid.MoveToFinished:Connect(onWaypointReached)
RunService.Heartbeat:Connect(function(deltaTime) timeSinceLastPathRecompute = timeSinceLastPathRecompute + deltaTime currentTarget = findNearestPlayer()
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
if not targetRoot then
currentTarget = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position)
currentPath = nil
return
end
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
if distanceToTarget <= STOP_DISTANCE then
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(rootPart.Position)
currentPath = nil
return
end
if timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME or not currentPath then
timeSinceLastPathRecompute = 0
followPath(currentTarget)
end
else
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
if humanoid.MoveDirection.Magnitude > 0 then
humanoid:MoveTo(rootPart.Position)
end
currentPath = nil
end
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
if not targetRoot then
currentTarget = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position)
currentPath = nil
return
end
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
if distanceToTarget <= STOP_DISTANCE then
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
humanoid:MoveTo(rootPart.Position)
currentPath = nil
return
end
if timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME or not currentPath then
timeSinceLastPathRecompute = 0
followPath(currentTarget)
end
else
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
if humanoid.MoveDirection.Magnitude > 0 then
humanoid:MoveTo(rootPart.Position)
end
currentPath = nil
end
end) _
ضع السكربت مباشرة داخل نموذج (Model) الشخصية غير اللاعبة (NPC) التي تريدها أن تلاحق اللاعب.
خطوات:
Workspace).Insert Object (إدراج كائن).Script من القائمة.print("Hello world!")).الآن، عندما تبدأ اللعبة، سيتم تشغيل السكربت تلقائيًا وسيبدأ الـ NPC في البحث عن اللاعبين وملاحقتهم.
فهمت تمامًا. تريد سلوكًا أكثر مباشرة واستجابة، حيث يتحرك الـ NPC مباشرة نحو اللاعب طالما أن الطريق واضح، ولا يستخدم PathfindingService إلا عندما يصطدم بعائق أو يكون الطريق مسدودًا. هذا سيقلل من التأخير الملحوظ.
سنستخدم Raycasting (إطلاق الشعاع) للتحقق مما إذا كان هناك خط رؤية مباشر بين الـ NPC واللاعب.
السكربت المعدل (ضعه داخل نموذج الـ NPC):
lua --[[ Script Name: NPCDirectChaseWithObstacleAvoidance Description: Makes the NPC chase the nearest player directly when possible, using PathfindingService only when the direct path is blocked. Location: Place this script directly inside the NPC Model in Workspace. --]]
-- Services local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") local RunService = game:GetService("RunService") local Workspace = game:GetService("Workspace") -- Needed for Raycasting
-- NPC Components local npc = script.Parent local humanoid = npc:WaitForChild("Humanoid") local rootPart = npc:WaitForChild("HumanoidRootPart") local animator = humanoid:WaitForChild("Animator")
-- Animation Setup local walkAnimation = Instance.new("Animation") walkAnimation.AnimationId = "rbxassetid://180426354" -- Example R15 Walk ID local walkAnimTrack = animator:LoadAnimation(walkAnimation) walkAnimTrack.Looped = true walkAnimTrack.Priority = Enum.AnimationPriority.Action
-- Configuration local CHASE_DISTANCE = 100 -- Max distance to start chasing local STOP_DISTANCE = 6 -- Distance to stop at (slightly larger for direct move) local PATH_RECOMPUTE_TIME = 1.5 -- How often to re-check/re-calculate path if pathfinding local RAYCAST_DISTANCE_BUFFER = 5 -- How much further than the player to cast the ray
-- Raycast Parameters (to ignore the NPC itself) local raycastParams = RaycastParams.new() raycastParams.FilterType = Enum.RaycastFilterType.Exclude raycastParams.FilterDescendantsInstances = {npc} raycastParams.IgnoreWater = true
-- Internal Variables local currentTarget = nil local currentPath = nil local currentWaypointIndex = 0 local timeSinceLastPathRecompute = 0 local isPathfinding = false -- Track if currently using PathfindingService
-- Function to find the nearest player (same as before) local function findNearestPlayer() local nearestPlayer = nil local minDistance = CHASE_DISTANCE for _, player in ipairs(Players:GetPlayers()) do local character = player.Character if character then local playerRoot = character:FindFirstChild("HumanoidRootPart") local playerHumanoid = character:FindFirstChild("Humanoid") if playerRoot and playerHumanoid and playerHumanoid.Health > 0 then local distance = (rootPart.Position - playerRoot.Position).Magnitude if distance < minDistance then minDistance = distance nearestPlayer = character end end end end return nearestPlayer end
-- Function to compute and start following a path using PathfindingService local function computeAndFollowPath(targetCharacter) local targetRoot = targetCharacter:FindFirstChild("HumanoidRootPart") if not targetRoot then return end
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, AgentHeight = 6, AgentCanJump = true
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
isPathfinding = true -- Enter pathfinding mode
currentWaypointIndex = 1 -- Reset waypoint index
if not walkAnimTrack.IsPlaying then walkAnimTrack:Play() end -- Ensure animation plays
-- Move to the first *actual* waypoint (index 2) if available
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- Only one waypoint (target is very close/reachable directly via path)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- Path has no waypoints (target likely very close/unreachable?)
isPathfinding = false -- Exit pathfinding mode as it failed
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path is empty
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path fails
end
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, AgentHeight = 6, AgentCanJump = true
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
isPathfinding = true -- Enter pathfinding mode
currentWaypointIndex = 1 -- Reset waypoint index
if not walkAnimTrack.IsPlaying then walkAnimTrack:Play() end -- Ensure animation plays
-- Move to the first *actual* waypoint (index 2) if available
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- Only one waypoint (target is very close/reachable directly via path)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- Path has no waypoints (target likely very close/unreachable?)
isPathfinding = false -- Exit pathfinding mode as it failed
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path is empty
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path fails
end
end
-- Function to handle reaching a waypoint or path interruption during Pathfinding local function onWaypointReached(reached) -- Only process if we are currently in pathfinding mode and have a valid path if isPathfinding and currentPath then if reached and currentWaypointIndex < #currentPath:GetWaypoints() then -- Move to the next waypoint currentWaypointIndex = currentWaypointIndex + 1 humanoid:MoveTo(currentPath:GetWaypoints()[currentWaypointIndex].Position) else -- Reached the end of the path OR path was interrupted (reached == false) isPathfinding = false currentPath = nil -- Don't stop animation/movement here, let the main loop decide the next action -- based on line-of-sight check in the next frame. end end end
-- Connect MoveToFinished event humanoid.MoveToFinished:Connect(onWaypointReached)
-- Main loop using Heartbeat for smooth updates RunService.Heartbeat:Connect(function(deltaTime) timeSinceLastPathRecompute = timeSinceLastPathRecompute + deltaTime currentTarget = findNearestPlayer()
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
local targetHumanoid = currentTarget:FindFirstChild("Humanoid") -- Check if target is alive
-- Validate target
if not targetRoot or not targetHumanoid or targetHumanoid.Health <= 0 then
currentTarget = nil -- Target lost or died
-- Fall-through to the 'else' block below to stop NPC
else
-- Target is valid
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop precisely
return -- Exit Heartbeat iteration early
end
-- Check Line of Sight using Raycast
local origin = rootPart.Position
local direction = (targetRoot.Position - origin)
-- Cast slightly further than the actual distance to ensure we hit the player if path is clear
local rayDistance = direction.Magnitude + RAYCAST_DISTANCE_BUFFER
local raycastResult = Workspace:Raycast(origin, direction.Unit * rayDistance, raycastParams)
local directPathClear = false
if raycastResult then
-- Check if the ray hit the target character or something else
if raycastResult.Instance:IsDescendantOf(currentTarget) then
directPathClear = true -- Hit the target player, path is clear!
else
directPathClear = false -- Hit an obstacle
end
else
-- Ray hit nothing within the cast distance.
-- This implies a clear path *if* the target is actually within that distance.
if distanceToTarget < rayDistance then
directPathClear = true
end
end
-- === Movement Logic ===
if directPathClear then
-- DIRECT CHASE MODE
if isPathfinding then
-- If we were pathfinding, stop it now
isPathfinding = false
currentPath = nil
-- Don't stop MoveTo immediately, the new MoveTo will override
end
humanoid:MoveTo(targetRoot.Position)
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
timeSinceLastPathRecompute = 0 -- Reset path timer as we are moving directly
else
-- OBSTACLE DETECTED - PATHFINDING MODE
-- Recalculate path if not currently pathfinding, or if it's time to recompute
if not isPathfinding or timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME then
timeSinceLastPathRecompute = 0
computeAndFollowPath(currentTarget)
elseif isPathfinding and not walkAnimTrack.IsPlaying and humanoid.MoveDirection.Magnitude > 0.1 then
-- If already pathfinding but animation stopped (e.g. after brief stop), restart it
walkAnimTrack:Play()
end
end
return -- Exit Heartbeat iteration after processing target
end
end
-- === No Target Found or Target Invalidated ===
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Stop moving only if currently moving
if humanoid.MoveDirection.Magnitude > 0.1 then
humanoid:MoveTo(rootPart.Position)
end
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
local targetHumanoid = currentTarget:FindFirstChild("Humanoid") -- Check if target is alive
-- Validate target
if not targetRoot or not targetHumanoid or targetHumanoid.Health <= 0 then
currentTarget = nil -- Target lost or died
-- Fall-through to the 'else' block below to stop NPC
else
-- Target is valid
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop precisely
return -- Exit Heartbeat iteration early
end
-- Check Line of Sight using Raycast
local origin = rootPart.Position
local direction = (targetRoot.Position - origin)
-- Cast slightly further than the actual distance to ensure we hit the player if path is clear
local rayDistance = direction.Magnitude + RAYCAST_DISTANCE_BUFFER
local raycastResult = Workspace:Raycast(origin, direction.Unit * rayDistance, raycastParams)
local directPathClear = false
if raycastResult then
-- Check if the ray hit the target character or something else
if raycastResult.Instance:IsDescendantOf(currentTarget) then
directPathClear = true -- Hit the target player, path is clear!
else
directPathClear = false -- Hit an obstacle
end
else
-- Ray hit nothing within the cast distance.
-- This implies a clear path *if* the target is actually within that distance.
if distanceToTarget < rayDistance then
directPathClear = true
end
end
-- === Movement Logic ===
if directPathClear then
-- DIRECT CHASE MODE
if isPathfinding then
-- If we were pathfinding, stop it now
isPathfinding = false
currentPath = nil
-- Don't stop MoveTo immediately, the new MoveTo will override
end
humanoid:MoveTo(targetRoot.Position)
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
timeSinceLastPathRecompute = 0 -- Reset path timer as we are moving directly
else
-- OBSTACLE DETECTED - PATHFINDING MODE
-- Recalculate path if not currently pathfinding, or if it's time to recompute
if not isPathfinding or timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME then
timeSinceLastPathRecompute = 0
computeAndFollowPath(currentTarget)
elseif isPathfinding and not walkAnimTrack.IsPlaying and humanoid.MoveDirection.Magnitude > 0.1 then
-- If already pathfinding but animation stopped (e.g. after brief stop), restart it
walkAnimTrack:Play()
end
end
return -- Exit Heartbeat iteration after processing target
end
end
-- === No Target Found or Target Invalidated ===
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Stop moving only if currently moving
if humanoid.MoveDirection.Magnitude > 0.1 then
humanoid:MoveTo(rootPart.Position)
end
end)
التغييرات الرئيسية:
Workspace:Raycast() في كل دورة Heartbeat للتحقق من وجود عوائق بين الـ NPC واللاعب.raycastParams يتم إعداده لتجاهل الـ NPC نفسه.directPathClear صحيحًا (لا يوجد عائق):
Pathfinding جارية (isPathfinding = false, currentPath = nil).humanoid:MoveTo(targetRoot.Position) للتحرك مباشرة نحو اللاعب.directPathClear خاطئًا (يوجد عائق):
Pathfinding.computeAndFollowPath() لحساب المسار وتتبعه (فقط إذا لم يكن يتبع مسارًا بالفعل أو إذا حان وقت إعادة الحساب).isPathfinding): تم إضافة متغير isPathfinding لتتبع ما إذا كان الـ NPC يستخدم حاليًا PathfindingService أم يتحرك مباشرة. هذا يساعد في الانتقال السلس بين الوضعين.onWaypointReached: تم تبسيطها قليلاً، حيث أن القرار الرئيسي بالتبديل بين الأوضاع يحدث الآن في حلقة Heartbeat بناءً على الـ Raycast.كيف يعمل الآن:
PathfindingService لحساب طريق حول العائق.lua --[[ Script Name: NPCDirectChaseWithObstacleAvoidance Description: Makes the NPC chase the nearest player directly when possible, using PathfindingService only when the direct path is blocked. Location: Place this script directly inside the NPC Model in Workspace. --]]
-- Services local PathfindingService = game:GetService("PathfindingService") local Players = game:GetService("Players") local RunService = game:GetService("RunService") local Workspace = game:GetService("Workspace") -- Needed for Raycasting
-- NPC Components local npc = script.Parent local humanoid = npc:WaitForChild("Humanoid") local rootPart = npc:WaitForChild("HumanoidRootPart") local animator = humanoid:WaitForChild("Animator")
-- Animation Setup local walkAnimation = Instance.new("Animation") walkAnimation.AnimationId = "rbxassetid://180426354" -- Example R15 Walk ID local walkAnimTrack = animator:LoadAnimation(walkAnimation) walkAnimTrack.Looped = true walkAnimTrack.Priority = Enum.AnimationPriority.Action
-- Configuration local CHASE_DISTANCE = 100 -- Max distance to start chasing local STOP_DISTANCE = 6 -- Distance to stop at (slightly larger for direct move) local PATH_RECOMPUTE_TIME = 1.5 -- How often to re-check/re-calculate path if pathfinding local RAYCAST_DISTANCE_BUFFER = 5 -- How much further than the player to cast the ray
-- Raycast Parameters (to ignore the NPC itself) local raycastParams = RaycastParams.new() raycastParams.FilterType = Enum.RaycastFilterType.Exclude raycastParams.FilterDescendantsInstances = {npc} raycastParams.IgnoreWater = true
-- Internal Variables local currentTarget = nil local currentPath = nil local currentWaypointIndex = 0 local timeSinceLastPathRecompute = 0 local isPathfinding = false -- Track if currently using PathfindingService
-- Function to find the nearest player (same as before) local function findNearestPlayer() local nearestPlayer = nil local minDistance = CHASE_DISTANCE for _, player in ipairs(Players:GetPlayers()) do local character = player.Character if character then local playerRoot = character:FindFirstChild("HumanoidRootPart") local playerHumanoid = character:FindFirstChild("Humanoid") if playerRoot and playerHumanoid and playerHumanoid.Health > 0 then local distance = (rootPart.Position - playerRoot.Position).Magnitude if distance < minDistance then minDistance = distance nearestPlayer = character end end end end return nearestPlayer end
-- Function to compute and start following a path using PathfindingService local function computeAndFollowPath(targetCharacter) local targetRoot = targetCharacter:FindFirstChild("HumanoidRootPart") if not targetRoot then return end
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, AgentHeight = 6, AgentCanJump = true
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
isPathfinding = true -- Enter pathfinding mode
currentWaypointIndex = 1 -- Reset waypoint index
if not walkAnimTrack.IsPlaying then walkAnimTrack:Play() end -- Ensure animation plays
-- Move to the first *actual* waypoint (index 2) if available
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- Only one waypoint (target is very close/reachable directly via path)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- Path has no waypoints (target likely very close/unreachable?)
isPathfinding = false -- Exit pathfinding mode as it failed
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path is empty
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path fails
end
-- Create and compute the path
local path = PathfindingService:CreatePath({
AgentRadius = 3, AgentHeight = 6, AgentCanJump = true
})
local success, errorMessage = pcall(function()
path:ComputeAsync(rootPart.Position, targetRoot.Position)
end)
if success and path.Status == Enum.PathStatus.Success then
currentPath = path
local waypoints = currentPath:GetWaypoints()
if #waypoints > 0 then
isPathfinding = true -- Enter pathfinding mode
currentWaypointIndex = 1 -- Reset waypoint index
if not walkAnimTrack.IsPlaying then walkAnimTrack:Play() end -- Ensure animation plays
-- Move to the first *actual* waypoint (index 2) if available
if #waypoints > 1 then
currentWaypointIndex = 2
humanoid:MoveTo(waypoints[currentWaypointIndex].Position)
else
-- Only one waypoint (target is very close/reachable directly via path)
humanoid:MoveTo(waypoints[1].Position)
end
else
-- Path has no waypoints (target likely very close/unreachable?)
isPathfinding = false -- Exit pathfinding mode as it failed
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path is empty
end
else
-- Path computation failed
warn("Pathfinding error for NPC:", npc.Name, errorMessage or path.Status)
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop if path fails
end
end
-- Function to handle reaching a waypoint or path interruption during Pathfinding local function onWaypointReached(reached) -- Only process if we are currently in pathfinding mode and have a valid path if isPathfinding and currentPath then if reached and currentWaypointIndex < #currentPath:GetWaypoints() then -- Move to the next waypoint currentWaypointIndex = currentWaypointIndex + 1 humanoid:MoveTo(currentPath:GetWaypoints()[currentWaypointIndex].Position) else -- Reached the end of the path OR path was interrupted (reached == false) isPathfinding = false currentPath = nil -- Don't stop animation/movement here, let the main loop decide the next action -- based on line-of-sight check in the next frame. end end end
-- Connect MoveToFinished event humanoid.MoveToFinished:Connect(onWaypointReached)
-- Main loop using Heartbeat for smooth updates RunService.Heartbeat:Connect(function(deltaTime) timeSinceLastPathRecompute = timeSinceLastPathRecompute + deltaTime currentTarget = findNearestPlayer()
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
local targetHumanoid = currentTarget:FindFirstChild("Humanoid") -- Check if target is alive
-- Validate target
if not targetRoot or not targetHumanoid or targetHumanoid.Health <= 0 then
currentTarget = nil -- Target lost or died
-- Fall-through to the 'else' block below to stop NPC
else
-- Target is valid
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop precisely
return -- Exit Heartbeat iteration early
end
-- Check Line of Sight using Raycast
local origin = rootPart.Position
local direction = (targetRoot.Position - origin)
-- Cast slightly further than the actual distance to ensure we hit the player if path is clear
local rayDistance = direction.Magnitude + RAYCAST_DISTANCE_BUFFER
local raycastResult = Workspace:Raycast(origin, direction.Unit * rayDistance, raycastParams)
local directPathClear = false
if raycastResult then
-- Check if the ray hit the target character or something else
if raycastResult.Instance:IsDescendantOf(currentTarget) then
directPathClear = true -- Hit the target player, path is clear!
else
directPathClear = false -- Hit an obstacle
end
else
-- Ray hit nothing within the cast distance.
-- This implies a clear path *if* the target is actually within that distance.
if distanceToTarget < rayDistance then
directPathClear = true
end
end
-- === Movement Logic ===
if directPathClear then
-- DIRECT CHASE MODE
if isPathfinding then
-- If we were pathfinding, stop it now
isPathfinding = false
currentPath = nil
-- Don't stop MoveTo immediately, the new MoveTo will override
end
humanoid:MoveTo(targetRoot.Position)
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
timeSinceLastPathRecompute = 0 -- Reset path timer as we are moving directly
else
-- OBSTACLE DETECTED - PATHFINDING MODE
-- Recalculate path if not currently pathfinding, or if it's time to recompute
if not isPathfinding or timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME then
timeSinceLastPathRecompute = 0
computeAndFollowPath(currentTarget)
elseif isPathfinding and not walkAnimTrack.IsPlaying and humanoid.MoveDirection.Magnitude > 0.1 then
-- If already pathfinding but animation stopped (e.g. after brief stop), restart it
walkAnimTrack:Play()
end
end
return -- Exit Heartbeat iteration after processing target
end
end
-- === No Target Found or Target Invalidated ===
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Stop moving only if currently moving
if humanoid.MoveDirection.Magnitude > 0.1 then
humanoid:MoveTo(rootPart.Position)
end
if currentTarget then
local targetRoot = currentTarget:FindFirstChild("HumanoidRootPart")
local targetHumanoid = currentTarget:FindFirstChild("Humanoid") -- Check if target is alive
-- Validate target
if not targetRoot or not targetHumanoid or targetHumanoid.Health <= 0 then
currentTarget = nil -- Target lost or died
-- Fall-through to the 'else' block below to stop NPC
else
-- Target is valid
local distanceToTarget = (rootPart.Position - targetRoot.Position).Magnitude
-- Stop if close enough
if distanceToTarget <= STOP_DISTANCE then
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then walkAnimTrack:Stop() end
humanoid:MoveTo(rootPart.Position) -- Stop precisely
return -- Exit Heartbeat iteration early
end
-- Check Line of Sight using Raycast
local origin = rootPart.Position
local direction = (targetRoot.Position - origin)
-- Cast slightly further than the actual distance to ensure we hit the player if path is clear
local rayDistance = direction.Magnitude + RAYCAST_DISTANCE_BUFFER
local raycastResult = Workspace:Raycast(origin, direction.Unit * rayDistance, raycastParams)
local directPathClear = false
if raycastResult then
-- Check if the ray hit the target character or something else
if raycastResult.Instance:IsDescendantOf(currentTarget) then
directPathClear = true -- Hit the target player, path is clear!
else
directPathClear = false -- Hit an obstacle
end
else
-- Ray hit nothing within the cast distance.
-- This implies a clear path *if* the target is actually within that distance.
if distanceToTarget < rayDistance then
directPathClear = true
end
end
-- === Movement Logic ===
if directPathClear then
-- DIRECT CHASE MODE
if isPathfinding then
-- If we were pathfinding, stop it now
isPathfinding = false
currentPath = nil
-- Don't stop MoveTo immediately, the new MoveTo will override
end
humanoid:MoveTo(targetRoot.Position)
if not walkAnimTrack.IsPlaying then
walkAnimTrack:Play()
end
timeSinceLastPathRecompute = 0 -- Reset path timer as we are moving directly
else
-- OBSTACLE DETECTED - PATHFINDING MODE
-- Recalculate path if not currently pathfinding, or if it's time to recompute
if not isPathfinding or timeSinceLastPathRecompute >= PATH_RECOMPUTE_TIME then
timeSinceLastPathRecompute = 0
computeAndFollowPath(currentTarget)
elseif isPathfinding and not walkAnimTrack.IsPlaying and humanoid.MoveDirection.Magnitude > 0.1 then
-- If already pathfinding but animation stopped (e.g. after brief stop), restart it
walkAnimTrack:Play()
end
end
return -- Exit Heartbeat iteration after processing target
end
end
-- === No Target Found or Target Invalidated ===
isPathfinding = false
currentPath = nil
if walkAnimTrack.IsPlaying then
walkAnimTrack:Stop()
end
-- Stop moving only if currently moving
if humanoid.MoveDirection.Magnitude > 0.1 then
humanoid:MoveTo(rootPart.Position)
end
end) *_