اكتشاف الضربات

*This content is translated using AI (Beta) and may contain errors. To view this page in English, click here.

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

  1. فحص محاكي جسدي لمعرفة ما إذا كانت المقذوفة قد أصابت الهدف.
  2. فحص فوري لمعرفة ما إذا كان البندقية موجهة نحو الهدف.

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

باستخدام لعبة وسم الليزر النموذجية كمرجع، يعلمك هذا القسم من البرنامج التعليمي حول السكربتات وراء اكتشاف الضربات في الفضاء ثلاثي الأبعاد، بما في ذلك الإرشادات حول:

  • الحصول على اتجاه الانفجار من قيم الكاميرا الحالية ونوع بندقية اللاعب.
  • إرسال أشعة في مسار مستقيم من البندقية أثناء الانفجار.
  • التحقق من صحة الانفجار لمنع استغلال بيانات البندقية.
  • تقليل صحة اللاعب وفقًا لضرر الانفجار من كل نوع من البنادق وعدد الأشعة التي أصابت اللاعب.

بعد الانتهاء من هذا القسم، يمكنك استكشاف مواضيع تطوير إضافية لتعزيز طريقة اللعب الخاصة بك، مثل الصوت، والإضاءة، والتأثيرات الخاصة.

احصل على اتجاه الانفجار

بعد أن ينفجر اللاعب بندقيته، يقوم ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData باستدعاء وظيفتين لبدء عملية اكتشاف الضربات: rayDirections() و rayResults().

generateBlastData
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)

مدخلات rayDirections بسيطة: قيم موضع الكاميرا الحالية والدوران، ونوع بندقية اللاعب. إذا كانت لعبة وسم الليزر النموذجية تعطي اللاعبين بنادق تنتج شعاع ليزر واحد فقط، فإن ReplicatedStorageLaserRaygetDirectionsForBlast ستكون غير ضرورية لأنه يمكنك استخدام currentCamera.CFrame.LookVector لحساب الاتجاه للانفجار.

ومع ذلك، نظرًا لأن العينة توفر نوع بندقية إضافي ينتج عدة أشعة ليزر مع انتشار أفقي واسع، يجب على getDirectionsForBlast حساب الاتجاه لكل شعاع ليزر من الانتشار وفقًا لزواياها داخل تكوين البندقية:

getDirectionsForBlast
if numLasers == 1 then
-- بالنسبة للأشعة الفردية، فإنها تهدف مباشرة
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
-- بالنسبة للأشعة المتعددة، انشرها بالتساوي أفقيًا
-- على مدى زاوية laserSpreadDegrees حول المركز
local leftAngleBound = laserSpreadDegrees / 2
local rightAngleBound = -leftAngleBound
local degreeInterval = laserSpreadDegrees / (numLasers - 1)
for angle = rightAngleBound, leftAngleBound, degreeInterval do
local direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVector
table.insert(directions, direction)
end
end

لتوضيح هذا المفهوم بشكل أكبر، إذا كنت ستضيف نوع بندقية ثالث مع انتشار عمودي واسع، يمكنك إنشاء سمة بندقية جديدة، مثل spreadDirection، ثم ضبط حساب CFrame لاستخدام محور مختلف. على سبيل المثال، لاحظ الفرق في حسابات direction في السكربت التالي لهذا النوع الافتراضي الثالث من البندقية.

if numLasers == 1 then
table.insert(directions, originCFrame.LookVector)
elseif numLasers > 1 then
local leftAngleBound = laserSpreadDegrees / 2
local rightAngleBound = -leftAngleBound
local degreeInterval = laserSpreadDegrees / (numLasers - 1)
for angle = rightAngleBound, leftAngleBound, degreeInterval do
local direction
if spreadDirection == "vertical" then
direction = (originCFrame * CFrame.Angles(math.rad(angle), 0, 0)).LookVector
else
direction = (originCFrame * CFrame.Angles(0, math.rad(angle), 0)).LookVector
end
table.insert(directions, direction)
end
end
return directions

في النهاية، تعيد وظيفة rayDirections() جدولًا من Vectors التي تمثل اتجاه كل شعاع ليزر. إذا كان ذلك مفيدًا، يمكنك إضافة بعض السجلات للحصول على فكرة عن شكل هذه البيانات.

generateBlastData
local rayDirections = getDirectionsForBlast(currentCamera.CFrame, blasterConfig)
for _, direction in rayDirections do -- new line
print(direction) -- new line
end -- new line
local rayResults = castLaserRay(localPlayer, currentCamera.CFrame.Position, rayDirections)

إرسال الأشعة

تقوم castLaserRay(), الوظيفة الثانية في ReplicatedStorageBlasterattemptBlastClientblastClientgenerateBlastData, بأداء العمليات الأكثر تعقيدًا داخل السكربت. تبدأ بتحديد المعلمات بحيث يمكنها إجراء استدعاءات Workspace:Raycast() لأغراض إرسال الأشعة. إرسال الأشعة هو عملية إرسال شعاع غير مرئي من نقطة Vector3 في اتجاه محدد بطول محدد، ثم التحقق من مساره لمعرفة أين يتقاطع مع كائنات أخرى.

تكون هذه المعلومات مفيدة بشكل خاص لألعاب إطلاق النار من منظور الشخص الأول لأنها تتيح لك رؤية متى وأين تتقاطع الانفجارات مع اللاعبين أو البيئة. على سبيل المثال، توضح الصورة التالية شعاعين يتم إرسالهما بالتوازي مع بعضهما البعض. وفقًا لنقطة الأصل والاتجاه، يفوت الشعاع A الجدار ويستمر حتى يصل إلى أقصى مسافة له، بينما يصطدم الشعاع B بالجدار. لمزيد من المعلومات حول هذه العملية، راجع إرسال الأشعة.

رسم توضيحي حيث يستمر الشعاع A عبر الجدار، ويصطدم الشعاع B بالجدار.

تحدد معلمات castLaserRay() أنه يجب على استدعاءات Raycast() أن تأخذ في الاعتبار كل جزء في مساحة العمل باستثناء الشخصية التي أطلقت الانفجار. ثم يقوم السكربت بإرسال شعاع لكل اتجاه في جدول directions. إذا أصاب شعاع شيئًا ما، فإنه ينشئ RaycastResult، الذي يحتوي على خمس خصائص:

  • Distance – المسافة بين نقطة أصل الشعاع ونقطة التقاطع.
  • InstanceBasePart أو خلية Terrain التي يتقاطع معها الشعاع.
  • MaterialEnum.Material عند نقطة التقاطع.
  • Position – موضع Vector3 للتقاطع بين الشعاع وInstance.
  • NormalVector3 للمتجه العمودي للوجه الذي يتقاطع معه الشعاع.

تعتبر قيمة Instance هي الأكثر أهمية من بين هذه الخصائص لأسلوب اللعب في لعبة وسم الليزر النموذجية لأنها توضح متى تصطدم الأشعة بلاعبين آخرين. لاسترداد هذه المعلومات، تستخدم اللعبة وظيفة المساعدة ReplicatedStorageLaserRaycastLaserRaygetPlayerFromDescendant. إذا أعادت nil، فهذا يعني أن الكائن ليس جزءًا من لاعب، مما يعني أن الشعاع أصاب كائنًا غير حي داخل البيئة.

ثم تستخدم castLaserRay() Position و Normal لإنشاء CFrame جديدة تسميها وجهة الشعاع. كل شعاع له وجهة، وهي إما حيث أصاب الشعاع في الفضاء ثلاثي الأبعاد، أو النقطة في نهاية أقصى مسافة له. اعتمادًا على مدى دقة تصويب لاعبيك، قد تكون العديد أو معظم قيم taggedPlayer هي nil.

castLaserRay
if result then
-- أصاب الانفجار شيئًا ما، تحقق مما إذا كان لاعبًا.
destination = CFrame.lookAt(result.Position, result.Position + result.Normal)
taggedPlayer = getPlayerFromDescendant(result.Instance)
else
-- لم يصب الانفجار أي شيء، لذا فإن وجهته هي
-- النقطة عند أقصى مسافة له.
local distantPosition = origin + rayDirection * MAX_DISTANCE
destination = CFrame.lookAt(distantPosition, distantPosition - rayDirection)
taggedPlayer = nil
end

تحقق من صحة الانفجار

لمنع الغش، يشرح الفصل السابق تنفيذ البنادق كيف يقوم blastClient بإخطار الخادم بالانفجار باستخدام RemoteEvent حتى يتمكن من التحقق من جميع البيانات التي يرسلها كل عميل، مثل ما إذا كانوا قد قاموا بالفعل بوسم لاعب آخر ببندقيتهم. تحدث عملية التحقق من صحة الأشعة هذه في ServerScriptServiceLaserBlastHandlergetValidatedBlastDatagetValidatedRayResults، وترتبط كل فحص بسكربت وحدة متداخلة:

  1. أولاً، تستدعي getValidatedRayResults validateRayResult للتحقق من أن كل إدخال في جدول rayResults من العميل هو CFrame و Player (أو nil).

  2. بعد ذلك، تستدعي isRayAngleFromOriginValid لمقارنة الزوايا المتوقعة لانتشار الليزر بتلك القادمة من العميل. يظهر هذا الكود بشكل خاص ميزة استخدام ReplicatedStorage لأن الخادم يمكنه استدعاء getDirectionsForBlast بنفسه، وتخزين العائد كبيانات "متوقعة"، ثم مقارنتها بالبيانات من العميل.

    تمامًا مثل التحقق من صحة البندقية من الفصل السابق، تعتمد isRayAngleFromOriginValid على قيمة تحمل لتحديد ما يشكل "فرقًا مفرطًا" في الزوايا:

    isRayAngleFromOriginValid
    local claimedDirection = (rayResult.destination.Position - originCFrame.Position).Unit
    local directionErrorDegrees = getAngleBetweenDirections(claimedDirection, expectedDirection)
    return directionErrorDegrees <= ToleranceValues.BLAST_ANGLE_SANITY_CHECK_TOLERANCE_DEGREES

    تقوم Roblox بإخفاء أكثر أجزاء الرياضيات تعقيدًا، لذا فإن النتيجة هي وظيفة مساعدة قصيرة وقابلة لإعادة الاستخدام بدرجة عالية مع قابلية التطبيق عبر مجموعة من الألعاب:

    getAngleBetweenDirections
    local 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
  3. الفحص التالي هو الأكثر بديهية. بينما تستخدم getValidatedBlastData DISTANCE_SANITY_CHECK_TOLERANCE_STUDS للتحقق من أن اللاعب الذي أطلق كان قريبًا من نقطة أصل الشعاع، تستخدم isPlayerNearPosition منطقًا مماثلًا للتحقق مما إذا كان اللاعب الموصوم قريبًا من وجهة الشعاع:

    isPlayerNearPosition
    local distanceFromCharacterToPosition = position - character:GetPivot().Position
    if distanceFromCharacterToPosition.Magnitude > ToleranceValues.DISTANCE_SANITY_CHECK_TOLERANCE_STUDS then
    return false
    end
  4. الفحص النهائي isRayPathObstructed يستخدم نسخة من عملية إرسال الأشعة للتحقق مما إذا كانت وجهة الشعاع خلف جدار أو عائق آخر من موقع العميل. على سبيل المثال، إذا كان هناك لاعب خبيث يقوم بإزالة جميع الجدران من اللعبة بشكل منهجي لوسم لاعبين آخرين، سيتحقق الخادم ويؤكد أن الأشعة غير صالحة لأنه يعرف موقع كل كائن داخل البيئة.

    isRayPathObstructed
    local scaledDirection = (rayResult.destination.Position - blastData.originCFrame.Position)
    scaledDirection *= (scaledDirection.Magnitude - 1) / scaledDirection.Magnitude

لا توجد استراتيجية لمكافحة الاستغلال شاملة، ولكن من المهم التفكير في كيفية اقتراب اللاعبين الخبيثين من لعبتك حتى تتمكن من وضع فحوصات في مكانها يمكن للخادم تشغيلها للإشارة إلى سلوك مشبوه.

تقليل صحة اللاعب

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

بدءًا من تقليل صحة اللاعب الموصوم، يغطي الإحياء وإعادة الإحياء التمييز بين Player و Player.Character، تحديدًا أن الشخصية هي نموذج Humanoid. تحتوي نماذج Humanoid على خاصية Health بقيمة افتراضية تبلغ 100. بدلاً من تنفيذ نظامها الخاص، تستخدم لعبة وسم الليزر النموذجية هذه الخاصية المدمجة لتتبع مقدار الضرر الذي يحتاجه اللاعب قبل أن يتم وسمه خارج الجولة.

تخزن اللعبة قيم الضرر في سمة damagePerHit لكل بندقية. على سبيل المثال، البندقية التي تطلق شعاع ليزر واحد تلحق 10 نقاط من الضرر، لذا يستغرق الأمر عشرة انفجارات بهذه البندقية لوسم لاعب آخر. لبدء عملية وسم لاعب خارجًا، تستدعي LaserBlastHandler ServerScriptServiceLaserBlastHandlerprocessTaggedPlayers، التي تتحقق من جدول 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 يضمن عدم انخفاض الصحة إذا كانت ForceField نشطة
humanoid:TakeDamage(damage)
if humanoid.Health <= 0 then
-- منح playerBlasted نقطة لوسم playerTagged
Scoring.incrementScore(playerBlasted, 1)
end
end
end

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

تغطي الفصول الخمسة في هذه المنهجية حلقة اللعب الأساسية في اللعبة، ولكن لا تزال هناك العديد من المجالات للاستكشاف، مثل:

  • مرئيات البندقية: انظر ReplicatedStorageFirstPersonBlasterVisuals و ServerScriptServiceThirdPersonBlasterVisuals.
  • الصوت: انظر ReplicatedStorageSoundHandler.
  • الأوضاع المخصصة: كيف يمكنك تعديل هذه اللعبة لتقديم أنواع جديدة من الأهداف، مثل تسجيل أكبر عدد من النقاط قبل انتهاء الوقت؟

للحصول على منطق لعب ممتد للعبة وسم الليزر، بالإضافة إلى أصول بيئية قابلة لإعادة الاستخدام وعالية الجودة، راجع وسم الليزر القالب.

©2026 شركة Roblox Corporation. تُعد منصّة Roblox، وشعار Roblox وشعار "توسيع حدود المخيلة"، من ضمن علاماتنا التجارية المسجّلة وغير المسجّلة في الولايات المتحدة وبلدان أخرى.