إيجاد المسار هو العملية التي يتم من خلالها تحريك شخصية أو كائن (وكيل) على طول مسار منطقي حول العقبات للوصول إلى وجهة، مع تجنب المواد الخطرة أو المناطق المعينة عند الحاجة.
تصور التنقل
لمساعدة في تخطيط وإصلاح إيجاد المسار، يمكن لـ Studio عرض شبكة التنقل وتسميات المعدلات. لتفعيلها، قم بتفعيل شبكة التنقل و معدلات إيجاد المسار من أداة خيارات التصوير في الزاوية العليا اليمنى من نافذة العرض الثلاثي الأبعاد.

مع تفعيل شبكة التنقل، تُظهر المناطق الملونة الأماكن التي قد تمشي أو تسبح فيها الشخصية. تُشير السهام الصغيرة إلى الأماكن التي ستحاول الشخصية الوصول إليها عن طريق القفز.

التنفيذ
على الرغم من أنه يمكن تنفيذ إيجاد المسار بطرق مختلفة من خلال PathfindingService وطرقها المرتبطة مثل CreatePath(), ستستخدم هذه الفقرة نص إيجاد المسار التالي لشخصية اللاعب.
لاختبار أثناء القراءة:
- هامفي المستكشف، حدد حاوية StarterPlayer. ثم، في نافذة الخصائص، قم بتعيين كل من DevComputerMovementMode و DevTouchMovementMode إلى Scriptable.


انسخ الكود التالي إلى LocalScript داخل StarterCharacterScripts, أو احصل على هذه الحزمة وألقها في StarterCharacterScripts.
PlayerPathFollow (LocalScript in StarterCharacterScripts)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {Water = 20}})local character = script.Parentlocal humanoid = character:WaitForChild("Humanoid")local waypointslocal nextWaypointIndexlocal blockedConnectionlocal currentWaypointReachedConnectionlocal currentWaypointPlaneNormal = Vector3.zerolocal currentWaypointPlaneDistance = 0local pathfinderWorking = falselocal function disconnectCurrentWaypointReachedConnection()if not currentWaypointReachedConnection then return endcurrentWaypointReachedConnection:Disconnect()currentWaypointReachedConnection = nilendlocal function isCurrentWaypointReached()if humanoid.FloorMaterial == Enum.Material.Air thenreturn falseendlocal reached = falseif currentWaypointPlaneNormal ~= Vector3.zero then-- حساب المسافة من humanoid إلى الطائرة الوجهةlocal dist = currentWaypointPlaneNormal:Dot(humanoid.RootPart.Position) - currentWaypointPlaneDistance-- حساب مكون سرعة humanoid المتجه نحو الطائرةlocal velocity = -currentWaypointPlaneNormal:Dot(humanoid.RootPart.Velocity)-- حساب العتبة من الطائرة الوجهة بناءً على سرعة humanoidlocal threshold = math.max(1.0, VELOCITY_MULTIPLIER * velocity)-- اعتبار النقطة الوجهة قد تم الوصول إليها إذا كانت أقل من العتبة أمام الطائرةreached = dist < thresholdelsereached = trueendif reached thencurrentWaypointPlaneNormal = Vector3.zerocurrentWaypointPlaneDistance = 0moveToNextWaypoint()endendlocal function calculateNextWaypointApproach()nextWaypointIndex += 1if nextWaypointIndex > #waypoints thenreturn falseendlocal currentWaypoint = waypoints[nextWaypointIndex - 1]local nextWaypoint = waypoints[nextWaypointIndex]-- بناء الطائرة الوجهة من النقطة الوجهة التالية نحو الحاليةcurrentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position-- تعيين العمودي لنقطة Y عندما لا نتسلق لأعلىif nextWaypoint.Label ~= "Climb" thencurrentWaypointPlaneNormal = Vector3.new(currentWaypointPlaneNormal.X, 0, currentWaypointPlaneNormal.Z)endif currentWaypointPlaneNormal.Magnitude > 0.000001 thencurrentWaypointPlaneNormal = currentWaypointPlaneNormal.UnitcurrentWaypointPlaneDistance = currentWaypointPlaneNormal:Dot(nextWaypoint.Position)endreturn trueendlocal function resetWaypointData()humanoid:Move(Vector3.zero)currentWaypointPlaneNormal = Vector3.zerocurrentWaypointPlaneDistance = 0disconnectCurrentWaypointReachedConnection()pathfinderWorking = falseendlocal function waitForGround()while humanoid.FloorMaterial == Enum.Material.Air dotask.wait(GROUND_WAIT)endendfunction moveToNextWaypoint()if calculateNextWaypointApproach() thendisconnectCurrentWaypointReachedConnection()currentWaypointReachedConnection = RunService.Heartbeat:Connect(isCurrentWaypointReached)local nextWaypointPosition = waypoints[nextWaypointIndex].Positionlocal nextWaypointAction = waypoints[nextWaypointIndex].Actionhumanoid:Move(nextWaypointPosition - humanoid.RootPart.Position)if waypoints[nextWaypointIndex + 1] and waypoints[nextWaypointIndex + 1].Label == "UseBoat" thennextWaypointIndex += 1-- اتصل بوظيفتك المخصصة لجعل الوكيل يستخدم القاربelseif nextWaypointAction == Enum.PathWaypointAction.Jump thenhumanoid:ChangeState(Enum.HumanoidStateType.Jumping)while humanoid.FloorMaterial ~= Enum.Material.Air dotask.wait(GROUND_WAIT)endhumanoid:Move(nextWaypointPosition - humanoid.RootPart.Position)endelseresetWaypointData()endendlocal function findStartingPoint(waypoints)nextWaypointIndex = 1while nextWaypointIndex + 1 <= #waypoints dolocal dist = waypoints[nextWaypointIndex + 1].Position - humanoid.RootPart.Positiondist = Vector3.new(dist.X, 0, dist.Z)if dist.magnitude >= 2 thenreturnendnextWaypointIndex += 1endendlocal function followPath()-- حساب المسارpathfinderWorking = truewaitForGround()local success, errorMessage = pcall(function()path:ComputeAsync(character.PrimaryPart.Position, DESTINATION)end)if not success or path.Status ~= Enum.PathStatus.Success thenwarn("لم يُحسب المسار!", errorMessage)returnend-- الحصول على نقاط الطريقwaypoints = path:GetWaypoints()-- كشف ما إذا كان المسار محجوزًاblockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)-- تحقق مما إذا كانت العقبة موجودة في مكانٍ ما بعيدًا في المسارif blockedWaypointIndex >= nextWaypointIndex then-- توقف عن كشف حجز المسار حتى يتم إعادة حساب المسارblockedConnection:Disconnect()resetWaypointData()-- استدعاء وظيفة لإعادة حساب مسار جديدfollowPath()endend)findStartingPoint(waypoints)moveToNextWaypoint()endfollowPath()قم بتحرير متغير DESTINATION (
السطر 5) إلى وجهة Vector3 ضمن العالم الثلاثي الأبعاد التي يمكن أن تصل إليها شخصية اللاعب.تابع من خلال الأقسام التالية لتتعلم عن حساب المسار وحركة الشخصية.
إنشاء المسار
يتم بدء إيجاد المسار من خلال PathfindingService وطريقة CreatePath() (
| المفتاح | الوصف | النوع | الإفتراضي |
|---|---|---|---|
| AgentRadius | نصف قطر الوكيل، بالسدادات. مفيد لتحديد الحد الأدنى من المسافة من العقبات. | عدد صحيح | 2 |
| AgentHeight | ارتفاع الوكيل، بالسدادات. سيُوسم الفضاء الفارغ الأصغر من هذه القيمة، مثل الفضاء تحت الدرج، بأنه غير قابل للعبور. | عدد صحيح | 5 |
| AgentCanJump | تحدد ما إذا كانت القفزات مسموحة أثناء إيجاد المسار. | boolean | true |
| AgentCanClimb | تحدد ما إذا كان التسلق في TrussParts مسموحًا أثناء إيجاد المسار. يتم تعيين مسار يمكن تسلقه بعلامة Label تدعى Climb وتكون التكلفة لمسار يمكن تسلقه هي 1 بشكل افتراضي. | boolean | false |
| WaypointSpacing | تباعد بين نقاط الطريق المتوسطة في المسار. إذا تم تعيينها إلى math.huge، فلن تكون هناك نقاط طريق متوسطة. | رقم | 4 |
| Costs | جدول المواد أو PathfindingModifiers المعرفة وتكاليفها للعبور. مفيد لجعل الوكيل يفضل مواد/مناطق معينة على الأخرى. راجع المعدلات للحصول على التفاصيل. | جدول | nil |
حساب المسار
بعد إنشاء مسار صالح باستخدام CreatePath(), يجب حسابه من خلال استدعاء Path:ComputeAsync() مع Vector3 لكل من نقطة البدء والوجهة (

بمجرد حساب Path, ستحتوي على سلسلة من نقاط الطريق التي تتبع المسار من البداية إلى النهاية. يمكن جمع هذه النقاط باستخدام طريقة Path:GetWaypoints() (

حركة المسار
يتكون كل PathWaypoint من كل من Position (Vector3) و Action (PathWaypointAction). لتحريك شخصية تحتوي على Humanoid, مثل الشخصية العادية في Roblox, فإن أفضل طريقة هي استدعاء Humanoid:Move() من نقطة الطريق إلى نقطة الطريق واستخدام رد استدعاء isCurrentWaypointReached() في السكربت (
المسارات المحجوزة
تعتبر العديد من عوالم Roblox ديناميكية؛ قد تتحرك الأجزاء أو تسقط وقد تسقط الأرضيات. يمكن أن تمنع هذه الأشياء المسار المحسوب وتمنع الشخصية من الوصول إلى وجهتها. للتعامل مع هذا، يمكنك الاتصال بحدث Path.Blocked وإعادة حساب المسار حول ما قام بحجبه (
معدلات إيجاد المسار
بشكل افتراضي، تقوم Path:ComputeAsync() بإرجاع المسار الأقصر بين نقطة البدء ونقطة الوجهة، باستثناء أنها تحاول تجنب القفزات. قد يبدو هذا غير طبيعي في بعض الحالات؛ على سبيل المثال، قد يمر المسار عبر مياه المستنقع بدلاً من حوله ببساطة لأن المسار من خلال المياه هو الأقصر هندسيًا.

لتحسين إيجاد المسار بشكل أفضل، يمكنك تنفيذ معدلات إيجاد المسار لحساب مسارات أذكى عبر مواد مختلفة المواد، أو حول [المناطق] (#configure-regions) المعينة، أو لتجاهل العقبات.
تكاليف المواد
عند العمل مع مواد Terrain وBasePart, يمكنك تضمين جدول Costs داخل CreatePath() لجعل مواد معينة أكثر قابلية للعبور من غيرها. جميع المواد لها تكلفة افتراضية قدرها 1 ويمكن تعريف أي مادة على أنها غير قابلة للعبور عن طريق تعيين قيمتها إلى math.huge.
يجب أن تمثل المفاتيح في جدول Costs أسماء سلسلة تمثل أسماء Enum.Material، على سبيل المثال Water لـ Enum.Material.Water أو CrackedLava لـ Enum.Material.CrackedLava.
PlayerPathFollow (LocalScript)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {Water = 20, CrackedLava = 100, Slate = 20}})
تكوين المناطق
في بعض الحالات، لا تكون تفضيل المواد كافية. على سبيل المثال، قد ترغب في تجنب الشخصيات منطقة معينة، بغض النظر عن المواد تحت الأقدام. يمكن تحقيق ذلك عن طريق إضافة كائن PathfindingModifier إلى جزء.
قم بإنشاء جزء Anchored حول المنطقة واضبط خاصية CanCollide إلى false.

أدخل مثيل PathfindingModifier على الجزء، وابحث عن خاصية Label، وضعي اسمًا ذا مغزى مثل DangerZone.

أدرج مفتاح DangerZone مطابق وقيمة عددية مرتبطة ضمن جدول Costs لـ CreatePath(). يمكن تعريف معدل على أنه غير قابل للعبور بتعيين قيمته إلى math.huge.
PlayerPathFollow (LocalScript)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {DangerZone = math.huge, Water = 20, CrackedLava = 20, Slate = 20}})
تجاهل العقبات
في بعض الحالات، يكون من المفيد إجراء إيجاد المسار من خلال العقبات الصلبة كما لو كانت غير موجودة. يسمح لك هذا بحساب مسار خلال عوائق مادية معينة، بدلاً من أن تفشل الحسابة تمامًا.
قم بإنشاء جزء Anchored حول الكائن واضبط خاصية CanCollide إلى false.

أدخل مثيل PathfindingModifier على الجزء وقم بتمكين خاصية PassThrough.

الآن، عندما يتم حساب مسار من NPC الزومبي إلى شخصية اللاعب، يمتد المسار إلى ما وراء الباب ويمكنك توجيه الزومبي لتجاوزه. حتى إذا لم يتمكن الزومبي من فتح الباب، فإنه يتفاعل كما لو "سمع" الشخصية خلف الباب.

روابط إيجاد المسار
في بعض الأحيان، يكون من الضروري العثور على مسار عبر مساحة لا يمكن عبورها عادةً، مثل عبر وادٍ، وتنفيذ إجراء مخصص للوصول إلى نقطة الطريق التالية. يمكن تحقيق ذلك من خلال كائن PathfindingLink.
باستخدام المثال من أعلاه، يمكنك جعل الوكيل يستخدم قاربًا.

لإنشاء PathfindingLink باستخدام هذا المثال:
- اختياريقم بتفعيل روابط إيجاد المسار من أداة خيارات التصوير الموجودة في الزاوية العليا اليمنى من نافذة العرض الثلاثي الأبعاد. يساعد ذلك في التصوير وإصلاح الأخطاء عند تنفيذ روابط إيجاد المسار.
أنشئ Attachments, واحد على مقعد القارب وآخر بالقرب من نقطة هبوط القارب.

أنشئ كائن PathfindingLink في مساحة العمل، ثم عيّن خاصيتي Attachment0 وAttachment1 للمرفقات بدءًا ونهايةً على التوالي.


عيّن اسمًا ذا مغزى مثل UseBoat لخاصية Label. يُستخدم هذا الاسم كعلامة في سكربت إيجاد المسار لبدء إجراء مخصص عندما يصل الوكيل إلى نقطة بداية الارتباط.

أضف جدول Costs ضمن CreatePath() يحتوي على مفتاح Water ومفتاح مخصص مطابق لاسم خاصية Label. عيّن المفتاح المخصص قيمة أقل من تلك الخاصة بـ Water.
PlayerPathFollow (LocalScript)local PathfindingService = game:GetService("PathfindingService")local Players = game:GetService("Players")local RunService = game:GetService("RunService")local DESTINATION = Vector3.new(20, 0.5, 20)local GROUND_WAIT = 0.01local VELOCITY_MULTIPLIER = 0.0625local path = PathfindingService:CreatePath({AgentCanClimb = true,Costs = {UseBoat = 2, Water = 20}})في دالة moveToNextWaypoint() (
الأسطر 93–114), يمكن استخدام تحقق مخصص من اسم معدل Label لاتخاذ إجراء مختلف عن Humanoid:Move()؛ في هذه الحالة، قد تستدعي دالة لتجلس الوكيل في القارب، وتحرك القارب عبر الماء، وتخرج الوكيل عند نقطة هبوط القارب، ثم تتابع مسار الوكيل إلى وجهته النهائية.
التوافق مع التدفق
يعتبر [تدفق الواجهات] (../workspace/streaming/index.md) في اللعبة ميزة قوية تقوم بتحميل وإلغاء تحميل المحتويات الثلاثية الأبعاد ديناميكيًا مع تحرك شخصية اللاعب عبر العالم. أثناء استكشافهم للفضاء الثلاثي الأبعاد، يتم بث مجموعة جديدة من الفضاء إلى جهازهم وقد تتدفق بعض المجموعات الموجودة.
خذ في اعتبارك أفضل الممارسات التالية لاستخدام PathfindingService في الألعاب المدعومة بالتدفق:
يمكن أن تؤدي التدفقات إلى حجز أو إلغاء حجز مسار معين أثناء حركة الشخصية على طول ذلك. على سبيل المثال، بينما تجري شخصية عبر غابة، قد تتدفق شجرة في مكان ما أمامهم وتعيق الطريق. لجعل إيجاد المسار يعمل بسلاسة مع التدفق، من المستحسن بشدة أنك تستخدم تقنية التعامل مع المسارات المحجوزة وإعادة حساب المسار عند الضرورة.
نهج شائع في إيجاد المسار هو استخدام إحداثيات الكائنات الموجودة لالحساب، مثل تعيين وجهة المسار إلى موضع نموذج TreasureChest الموجود في العالم. هذا النهج متوافق تمامًا مع Scripts من جانب الخادم نظرًا لأن الخادم لديه رؤية كاملة للعالم طوال الوقت، ولكن قد تفشل LocalScripts وModuleScripts التي تعمل على العميل إذا حاولت حساب مسار إلى كائن لم يتم التدفق إليه.
لمواجهة هذه المشكلة، فكر في تعيين الوجهة إلى موضع BasePart داخل نموذج ثابت. يتم تحميل النماذج الثابتة بعد فترة وجيزة من انضمام اللاعب ولا تتدفق أبدًا، لذا يمكن لسكريبت جهة العميل الاتصال بحدث PersistentLoaded والوصول بأمان إلى النموذج لإنشاء نقاط الطريق بعد إطلاق الحدث.
القيود وعوامل الفشل
يتضمن محرك إيجاد المسار قيودًا معينة لضمان معالجة فعالة وأداء مثالي. بالإضافة إلى ذلك، يمكن أن تفشل حسابات إيجاد المسار لأسباب متنوعة كما هو موضح أدناه.