الكشف عن الضربات هي عملية تحديد متى تصطدم الانفجارات باللاعبين، ثم تقليل صحتهم وفقًا لذلك. على مستوى عالٍ، يمكنك التفكير في هذا العمل على أنه إما:
- تحقق محاكى فعليًا عما إذا كانت المقذوفة قد أصابت الهدف.
- تحقق لحظي عما إذا كانت المسدس موجهة نحو الهدف.
يعتمد نوع الكشف عن الضربات الذي تستخدمه على متطلبات اللعب في تجربتك. على سبيل المثال، فإن التحقق المحاكى فعليًا مناسب لتجربة كرة الدodgeball حيث تحتاج الكرات إلى مغادرة اليد بسرعة معينة، والسقوط أثناء حركتها في الهواء، أو تغيير الاتجاه بسبب الأحوال الجوية. ومع ذلك، فإن التحقق اللحظي هو الأنسب لتجربة لعبة الليزر حيث يجب أن تكون الأشعة ذات سرعة قريبة من اللانهاية وتجاهل العوامل البيئية مثل الجاذبية وسرعة الرياح.
باستخدام تجربة لعبة الليزر النموذجية كمرجع، تعلمك هذه القسم من البرنامج التعليمي عن السكريبتات وراء الكشف عن الضربات في الفضاء ثلاثي الأبعاد، بما في ذلك الإرشادات حول:
- الحصول على اتجاه الانفجار من قيم الكاميرا الحالية ونوع مسدس اللاعب.
- إرسال أشعة في مسار مستقيم من المسدس أثناء الانفجار.
- التحقق من صحة الانفجار لمنع الاستغلال لبيانات المسدس.
- تقليل صحة اللاعب وفقًا لضرر الانفجار من كل نوع من المسدسات وعدد الأشعة التي أصابت اللاعب.
بعد انتهاء هذه القسم، يمكنك استكشاف موضوعات تطوير إضافية لتعزيز أسلوب اللعب الخاص بك، مثل الصوت والإضاءة والمؤثرات الخاصة.
الحصول على اتجاه الانفجار
بعد أن ينفجر اللاعب مسدسه، يقوم ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData باستدعاء وظيفتين لبدء عملية الكشف عن الضربات: rayDirections() و rayResults().
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
تكون المدخلات لـ rayDirections بسيطة: موقع الكاميرا وقيم الدوران الحالية ونوع مسدس اللاعب. إذا كانت تجربة لعبة الليزر النموذجية تعطي اللاعبين فقط مسدسات تنتج شعاع ليزر واحد، فإن ReplicatedStorage ⟩ LaserRay ⟩ getDirectionsForBlast ستصبح غير ضرورية لأنك يمكنك استخدام currentCamera.CFrame.LookVector لحساب الاتجاه للانفجار.
ومع ذلك، نظرًا لأن العينة توفر نوع مسدس إضافي ينتج عدة أشعة ليزر بتوزيع أفقي واسع، يجب على getDirectionsForBlast حساب الاتجاه لكل شعاع ليزر من التوزيع وفقًا لزواياها ضمن تكوين المسدس:
getDirectionsForBlastif numLasers == 1 then-- بالنسبة للأشعة الفردية، فإنها تستهدف مباشرةtable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 then-- بالنسبة للأشعة المتعددة، نشرها بالتساوي أفقيًا-- على مدى زاوية laserSpreadDegrees حول المركزlocal leftAngleBound = laserSpreadDegrees / 2local rightAngleBound = -leftAngleBoundlocal degreeInterval = laserSpreadDegrees / (numLasers - 1)for angle = rightAngleBound, leftAngleBound, degreeInterval dolocal direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVectortable.insert(directions, direction)endend
لإيضاح هذا المفهوم بشكل أكبر، إذا كنت ستقوم بتضمين نوع مسدس ثالث بانتشار عمودي واسع، يمكنك إنشاء سمة مسدس جديدة، مثل spreadDirection، ثم تعديل حساب CFrame لاستخدام محور مختلف. على سبيل المثال، لاحظ الفرق في حسابات direction في السكربت التالي لهذا النوع الثالث من المسدسات الافتراضية.
if numLasers == 1 thentable.insert(directions, originCFrame.LookVector)elseif numLasers > 1 thenlocal leftAngleBound = laserSpreadDegrees / 2local rightAngleBound = -leftAngleBoundlocal degreeInterval = laserSpreadDegrees / (numLasers - 1)for angle = rightAngleBound, leftAngleBound, degreeInterval dolocal directionif spreadDirection == "vertical" thendirection = (originCFrame * CFrame.Angles(math.rad(angle), 0, 0)).LookVectorelsedirection = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVectorendtable.insert(directions, direction)endendreturn directions
في النهاية، تقوم وظيفة rayDirections() بإرجاع جدول من Vectors التي تمثل اتجاه كل شعاع ليزر. إذا كان ذلك مفيدًا، يمكنك إضافة بعض التسجيلات للحصول على فكرة عما يبدو هذا البيانات.
generateBlastDatalocal rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)for _, direction in rayDirections do -- new lineprint(direction) -- new lineend -- new linelocal rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)
إرسال الأشعة
castLaserRay()، هي الوظيفة الثانية في ReplicatedStorage ⟩ Blaster ⟩ attemptBlastClient ⟩ blastClient ⟩ generateBlastData، تقوم بتنفيذ العمليات الأكثر تعقيدًا داخل السكريبت. تبدأ بتحديد المعلمات بحيث يمكنها إجراء استدعاءات Workspace:Raycast() لأغراض إرسال الأشعة. إرسال الأشعة هي عملية إرسال شعاع غير مرئي من نقطة Vector3 في اتجاه معين مع طول محدد، ثم التحقق من مساره لرؤية أين يتقاطع مع أشياء أخرى.
تكون هذه المعلومات مفيدة بشكل خاص لتجارب إطلاق النار من منظور الشخص الأول لأنها تتيح لك رؤية متى وأين تتقاطع الانفجارات مع اللاعبين أو البيئة. على سبيل المثال، توضح الصورة التالية شعاعين يرسلون متوازيين مع بعضهما البعض. وفقًا لنقطة انطلاقهما واتجاههما، تخفق الأشعة A في الاصطدام بالجدار وتستمر حتى تلتقي بأقصى مسافة لها، بينما تصطدم الأشعة B بالجدار. لمزيد من المعلومات حول هذه العملية، راجع إرسال الأشعة.

تحدد معلمات castLaserRay() أن استدعاءات Raycast() يجب أن تأخذ في الاعتبار كل جزء في مساحة العمل باستثناء الشخصية التي انفجرت. ثم يقوم السكربت بإرسال شعاع لكل اتجاه في جدول directions. إذا أصاب شعاع شيئًا، فإنه يولد RaycastResult، الذي يحتوي على خمس خصائص:
- Distance – المسافة بين أصل الشعاع ونقطة التقاطع.
- Material – Enum.Material عند نقطة التقاطع.
تعتبر قيمة Instance هي الأكثر أهمية من بين هذه الخصائص لتجربة لعبة الليزر النموذجية لأن gameplay لأنها تخبرك بموعد اصطدام الأشعة مع لاعبين آخرين. لاسترجاع هذه المعلومات، تستخدم التجربة وظيفة المساعدة ReplicatedStorage ⟩ LaserRay ⟩ castLaserRay ⟩ getPlayerFromDescendant. إذا أعادت nil، فإن الـ instance ليست جزءًا من لاعب، مما يعني أن الشعاع أصاب كائنًا غير حي داخل البيئة.
ثم يستخدم castLaserRay() Position و Normal لإنشاء CFrame جديدة تسمي وجهة الشعاع. كل شعاع له وجهة، وهي إما حيث أصاب الشعاع في الفضاء ثلاثي الأبعاد، أو النقطة في نهاية أقصى مسافة له. اعتمادًا على مدى دقة تصويب اللاعبين لديك، قد تكون العديد أو معظم قيم taggedPlayer هي nil.
castLaserRayif result then-- أصاب الانفجار شيئًا، تحقق مما إذا كان لاعبًا.destination = CFrame.lookAt(result.Position, result.Position + result.Normal)taggedPlayer = getPlayerFromDescendant(result.Instance)else-- لم يصب الانفجار أي شيء، لذا فإن وجهته هي-- النقطة في أقصى مسافته.local distantPosition = origin + rayDirection * MAX_DISTANCEdestination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)taggedPlayer = nilend
تحقق من الانفجار
لمنع الغش، يوضح الفصل السابق تنفيذ المسدسات كيف يقوم blastClient بإخطار الخادم بالانفجار باستخدام RemoteEvent حتى يتمكن من التحقق من جميع البيانات التي يرسلها كل عميل، مثل ما إذا كانوا قد أصابوا لاعبًا آخر بمسدسهم أو لا. تحدث عملية تحقق الشعاع هذه في ServerScriptService ⟩ LaserBlastHandler ⟩ getValidatedBlastData ⟩ getValidatedRayResults، ويرتبط كل تحقق بسكربت وحدة متداخلة:
أولاً، يستدعي getValidatedRayResults validateRayResult للتحقق مما إذا كانت كل مدخل في جدول rayResults من العميل هي CFrame ولاعب (أو nil).
بعد ذلك، يستدعي isRayAngleFromOriginValid لمقارنة الزوايا المتوقعة لتوزيع الليزر بتلك التي من العميل. تُظهر هذه القطعة من الكود على وجه الخصوص ميزة استخدام ReplicatedStorage نظرًا لأن الخادم يمكنه استدعاء getDirectionsForBlast بنفسه، وتخزين النتيجة كبيانات "متوقعة"، ثم مقارنتها بالبيانات من العميل.
تمامًا كما هو الحال في تحقق المسدس من الفصل السابق، يعتمد isRayAngleFromOriginValid على قيمة تحمل لتحديد ما يشكل "فرقًا مفرطًا" في الزوايا:
isRayAngleFromOriginValidlocal claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unitlocal directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREESتقوم Roblox بأبستراكت الأجزاء الأكثر تعقيدًا في الرياضيات، لذا فإن النتيجة هي وظيفة مساعدة قصيرة، قابلة لإعادة الاستخدام عاليًا مع قابلية التطبيق عبر مجموعة من التجارب:
getAngleBetweenDirectionslocal function getAngleBetweenDirections(directionA: Vector3, directionB: Vector3)local dotProduct = directionA:Dot(directionB)local cosAngle = math.clamp(dotProduct, -1, 1)local angle = math.acos(cosAngle)return math.deg(angle)endالخطة التالية هي الأكثر بديهية. بينما يستخدم getValidatedBlastData DISTANCE_SANITY_CHECK_TOLERANCE_STUDS للتحقق من أن اللاعب الذي انفجر كان قريبًا من نقطة انطلاق الشعاع، فإن isPlayerNearPosition يستخدم منطقًا مماثلًا للتحقق مما إذا كان اللاعب المصاب قريبًا من وجهة الشعاع:
isPlayerNearPositionlocal distanceFromCharacterToPosition = position - character:GetPivot().Positionif distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS thenreturn falseendتستخدم الفحص النهائي isRayPathObstructed نوعًا مختلفًا من عملية إرسال الأشعة للتحقق مما إذا كانت وجهة الشعاع خلف جدار أو عائق آخر من موقع العميل. على سبيل المثال، إذا كان هناك لاعب خبيث يقوم بإزالة كل الجدران من التجربة بشكل منهجي لتصويب الضربات على لاعبين آخرين، سيتحقق الخادم ويؤكد أن الأشعة غير صالحة لأنه يعرف مواقع كل الأجسام ضمن البيئة.
isRayPathObstructedlocal scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude
لا توجد استراتيجية مضادة للاستغلال شاملة، ولكن من المهم التفكير في كيفية اقتراب اللاعبين الخبيثين من تجربتك حتى تتمكن من وضع الضوابط المناسبة في مكان يمكن للخادم تشغيلها للإشارة إلى السلوك المشبوه.
تقليل صحة اللاعب
بعد التحقق من أن لاعبًا قد أصاب لاعبًا آخر، تكون الخطوات النهائية لاستكمال الحلقة الرئيسية للعبة في تجربة لعبة الليزر النموذجية هي تقليل صحة اللاعب المصاب، وزيادة حساب النقاط، وإعادة ظهور اللاعب في اللعبة.
بدءًا بتقليل صحة اللاعب المصاب، تغطي إعادة الظهور التمييز بين Player وPlayer.Character، بشكل محدد أن الشخصية هي نموذج Humanoid. تحتوي نماذج Humanoid على خاصية Health بقيمة افتراضية تبلغ 100. بدلاً من تنفيذ نظامها الخاص، تستخدم تجربة لعبة الليزر النموذجية هذه الخاصية المدمجة لتتبع مقدار الضرر الذي يحتاجه اللاعب ليتم الإشارة إليه بالخروج من الجولة.
تخزن التجربة قيم الضرر في سمة damagePerHit لكل مسدس. على سبيل المثال، المسدس الذي يطلق شعاع ليزر واحد يسبب 10 نقاط من الضرر، لذا يحتاج الأمر لعملية عشر انفجارات بهذا المسدس لإخراج لاعب آخر. لبدء عملية إخراج اللاعب، تستدعي LaserBlastHandler ServerScriptService ⟩ LaserBlastHandler ⟩ processTaggedPlayers، التي تتحقق من جدول rayResults الآن الذي تم التحقق منه للاعبين وتقوم بتمرير damagePerHit إلى onPlayerTagged.

لا تقبل Health قيم سلبية، لذا يحتوي onPlayerTagged على بعض المنطق للحفاظ على صحة اللاعب عند أو فوق الصفر. بعد التحقق من أن صحة اللاعب فوق الصفر، يقارن الصحة بـ damagePerHit ويستخدم الأصغر من القيمتين. على سبيل المثال، إذا كان لدى اللاعب 10 نقاط صحة وتم إصابته بشعاع ليزر يسبب 15 ضررًا، فسيؤدي الشعاع فقط 10 نقاط من الضرر.
قد يبدو هذا الأسلوب في حل المشكلة معقدًا بعض الشيء. على سبيل المثال، لماذا لا تحدد فقط صحة اللاعب إلى صفر إذا كانت ستصبح سلبية؟ السبب هو أن تعيين قيم الصحة يتجنب حقل القوة. يضمن استخدام أسلوب Humanoid:TakeDamage() ألا يتعرض اللاعبون للضرر بينما تكون حقول القوة نشطة.
onPlayerTagged
local function onPlayerTagged(playerBlasted: Player, playerTagged: Player, damageAmount: number)
local character = playerTagged.Character
local isFriendly = playerBlasted.Team == playerTagged.Team
-- يمنع النيران الصديقة
if isFriendly then
return
end
local humanoid = character and character:FindFirstChild("Humanoid")
if humanoid and humanoid.Health > 0 then
-- تجنب الصحة السلبية
local damage = math.min(damageAmount, humanoid.Health)
-- TakeDamage يضمن عدم انخفاض الصحة إذا كانت حقل القوة نشطًا
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- منح playerBlasted نقطة مقابل إخراج playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end
الخطوة التالية هي زيادة حساب النقاط. قد يبدو أنه لا لزوم له أن يقوم LaserBlastHandler بإضافة اللاعب الذي انفجر جنبًا إلى جنب مع بيانات الانفجار، ولكن بدون هذه المعلومات، لا يمكن للتجربة منح اللاعب الفضل في إخراج شخص ما. أخيرًا، يُعاد اللاعب الذي خرج إلى الجولة، والذي يمكنك مراجعته في إعادة الظهور.
تغطي الفصول الخمسة في هذه المنهجية الحلقة الرئيسية لتجربة اللعبة، ولكن لا يزال هناك الكثير من المجالات للاستكشاف، مثل:
- مرئيات المسدس: انظر إلى ReplicatedStorage ⟩ FirstPersonBlasterVisuals و ServerScriptService ⟩ ThirdPersonBlasterVisuals.
- الصوت: انظر إلى ReplicatedStorage ⟩ SoundHandler.
- الأوضاع المخصصة: كيف يمكنك تعديل هذه التجربة لإدخال أنواع جديدة من الأهداف، مثل تسجيل أكبر عدد من النقاط قبل انتهاء الوقت؟
للحصول على منطق أثناء اللعب الممتد لتجربة لعبة الليزر، وكذلك أصول بيئية عالية الجودة قابلة لإعادة الاستخدام، راجع تجربة لعبة الليزر.