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

بدء الحلقة
ServerScriptService ⟩ Gameplay ⟩ Rounds تدير معظم المنطق لتنفيذ الجولات، وتبدأ من خلال استدعاء دالة startRoundLoopAsync() لتحديد بداية حلقة الجولة. أثناء انضمام اللاعبين إلى اللوبي وانتظار فرزهم إلى فريق، تقوم startRoundLoopAsync() باستدعاء دالة resetScores() في ServerScriptService ⟩ Gameplay ⟩ Scoring لإعادة تعيين كل من لوحة المتصدرين ونقاط الفريق.
function Scoring.resetScores()
for _, player in Players:GetPlayers() do
player.leaderstats.Points.Value = 0
end
for _, team in Teams:GetTeams() do
team:SetAttribute(GuiAttribute.teamPoints, 0)
end
endالآن بعد أن بدأ الجميع من نقاط صفر، تقوم startRoundLoopAsync() بعد ذلك بتعيين خاصية موقع الظهور Neutral Neutral إلى false بحيث يمكن للاعبين الذين لديهم نفس خاصية Team.Color كموقع الظهور أن يظهروا هناك. نظرًا لأن خاصية TeamColor لموقع الظهور تم تعيينها على الأبيض بدلاً من فرق المينت أو الوردي، فإن هذا التكوين يمنع جميع اللاعبين من الظهور أو إعادة الظهور هناك بينما الجولة نشطة.
بالنسبة لللاعبين المتواجدين حاليًا في اللوبي، تقوم startRoundLoopAsync() بتمرير جميع اللاعبين الذين يتواجدون في التجربة إلى دالة spawnPlayersInMap في ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ spawnPlayersInMap لفرز وتوازن الجميع في فريق مع عدد تقريبًا متساوي من اللاعبين.
بالنسبة لأي لاعبين جدد ينضمون إلى التجربة بعد فرز مجموعة اللوبي إلى فريق، تستمع startRoundLoopAsync() لحدث Players.PlayerAdded:Connect، ثم تستدعي دالة spawnPlayersInMap مرة أخرى لإضافتهم إلى الفريق الذي لديه أقل عدد من اللاعبين. لمزيد من المعلومات حول هذه العملية، انظر تكوين مواقع الظهور من القسم السابق الظهور وإعادة الظهور من البرنامج التعليمي.
-- احتضان جميع اللاعبين في الخريطة
neutralSpawn.Neutral = false
spawnPlayersInMap(Players:GetPlayers())
-- احتضان لاعبين جدد في الخريطة عند انضمامهم
local playerAddedConnection = Players.PlayerAdded:Connect(function(player: Player)
spawnPlayersInMap({ player })
end)تحديد الهدف
الآن بعد أن أصبح كل لاعب في الساحة مع زملائه في الفريق، تحتاج التجربة إلى تقديم تعليمات حول ما يجب القيام به لتحقيق النجاح داخل الجولة. تتناول تجربة زجاجة الليزر النموذجية هذا المتطلب من خلال تقديم موجه هدف في أعلى شاشة كل لاعب مع إرشادات واضحة حول ما يحتاجه الفريق لتحقيق الفوز.

بينما يمكنك معرفة المزيد حول كيفية تكوين وعرض عنصر واجهة الهدف في منهج واجهة المستخدم، يركز هذا القسم على كيفية تنفيذ هدف الجولة مع بدء الجولة، بدءًا من كيفية تعيين كمية النقاط التي يحتاجها كل فريق لإكمال الجولة.
حتى لو كان موجه الهدف في وقت التشغيل يُخبر اللاعبين بأن عليهم تسجيل ثلاث نقاط للفوز، إذا قمت بفحص الموجه في StarterGui ⟩ HUDGui، يمكنك أن ترى أنه يحتوي في الواقع على سلسلة قابلة للتكوين "%d" لقيمة النقاط.

"%d" هي سلسلة عنصر نائب يمكنك زيادة أو تقليلها في أي وقت لتلبية متطلبات أسلوب اللعب الخاص بك من خلال تحديث المتغير TEAM_SCORE_LIMIT في ReplicatedStorage ⟩ TEAM_SCORE_LIMIT. على سبيل المثال، إذا كنت ترغب في تعيين هذا الرقم إلى 200 مرتفع جدًا، فسيتم تحديث الموجه وعدد نقاط الفريق وفقًا لذلك.
local TEAM_SCORE_LIMIT = 200 -- سطر محدث، تأكد من تغييره مرة أخرى
return TEAM_SCORE_LIMIT
تعمل هذه التحديثات البسيطة للمتغير في وقت التشغيل لأنه مع بدء الجولة، يتطلب ReplicatedStorage ⟩ HUDGuiSetup ⟩ SetObjective سكربت الوحدة TEAM_SCORE_LIMIT حتى يتمكن من تبديل سلسلة العنصر النائب في كائن TextLabel لواجهة هدف UI.
local TEAM_SCORE_LIMIT = require(ReplicatedStorage.TEAM_SCORE_LIMIT)
local function setObjective(gui: ScreenGui)
local bodyTextLabel = gui.Objective.ObjectiveDisplay.Body.BodyTextLabel
bodyTextLabel.Text = bodyTextLabel.Text:format(TEAM_SCORE_LIMIT)
endتتبع النقاط
الآن بعد أن لدى اللاعبين هدف للجولة، تحتاج التجربة إلى تتبع نقاط كل فريق حتى يحققوا هدفهم. بينما يقوم سلوك Teams الافتراضي تلقائيًا بتجميع كل لاعب تحت فريقه وإضافة مساهمات كل لاعب إلى نقاط فريقه، من المهم تخزين ومراقبة النقاط في مكان منفصل لألعاب قائمة على الجولات لأنه إذا سجل لاعب ما ثم غادر قبل نهاية الجولة، يتم خصم مساهمته من لوحة المتصدرين بمجرد أن ينفصل عن التجربة.
لضمان عدم حدوث ذلك والحفاظ على كل مساهمة نحو هدف الفريق، يخزن ReplicatedStorage ⟩ HUDGuiSetup ⟩ StartSyncingTeamPoints جميع النقاط بشكل منفصل تحت خاصية teamPoints في خدمة Teams. مع زيادة teamPoints، تستدعي هذه الوحدة سكربت دالة startSyncingTeamPoints للعثور على عداد الفريق Class.GuiObjects ضمن مكون واجهة الهدف.
عندما تجد TeamACounter وTeamBCounter، تحصل على خاصية teamColor الخاصة بها، والتي تتوافق مع مناطق ظهور الفرق: يعرض TeamACounter نقاط الفريق الأخضر، ويقوم TeamBCounter بتتبع نقاط الفريق الوردي.
local function startSyncingTeamPoints(gui: ScreenGui)
for _, teamPointCounter in gui.Objective.TeamPointCounter:GetChildren() do
if not teamPointCounter:IsA("GuiObject") then
continue
end
local iconTeamColor = teamPointCounter:GetAttribute(GuiAttribute.teamColor)ثم يستدعي سكربت الوحدة دالة getTeamFromTeamColor الخاصة به للتحقق من أن خاصية teamColor لـ TeamACounter mint وخصية teamColor لـ TeamBCounter carnation pink تتطابق مع الخصائص Team.Color المقابلة في خدمة Teams. إذا كان الأمر كذلك، تُعيد كلا الفريقين.
local function getTeamFromTeamColor(teamColor: Color3): Team?
for _, team in Teams:GetTeams() do
if team.TeamColor == teamColor then
return team
end
end
return nil
endعندما يحدث ذلك، تقوم startSyncingTeamPoints بتعيين كائنات TextLabel لكلا عدادي الفريق إلى قيم teamPoints المقابلة لها، وتستمر في تحديثها كلما سجل لاعب نقطة من خلال استبعاد لاعب آخر من الفريق المقابل.
teamPointCounter.TextLabel.Text = team:GetAttribute(GuiAttribute.teamPoints)
team:GetAttributeChangedSignal(GuiAttribute.teamPoints):Connect(function()
teamPointCounter.TextLabel.Text = team:GetAttribute(GuiAttribute.teamPoints)حتى الآن، ركز كل شيء في هذا القسم على كيفية تتبع النقاط على شاشة اللاعب، لكن من المهم مراجعة المنطق الذي يتعامل مع تتبع النقاط على الخادم حتى يعلم متى يحقق الفريق هدفه ويفوز بالجولة. إذا عدت إلى ServerScriptService ⟩ Gameplay ⟩ Scoring، يمكنك أن ترى أن سكربت الوحدة يبدأ بإنشاء حدث قابل للربط، والذي سيشتغل في كل مرة يسجل فيها لاعب نقطة.
local teamScoreChangedBindable = Instance.new("BindableEvent")
local Scoring = {
teamScoreChanged = teamScoreChangedBindable.Event,
}ثم تستدعي دالة incrementScore، التي تقوم بالإجراءات التالية:
- تحصل على نتيجة اللاعب الفردية على لوحة المتصدرين، وتضيف واحدًا.
- تطلق الحدث القابل للربط الذي تم ذكره سابقًا مع كل من فريق اللاعب ونتيجته.
تضمن هذه العملية الحفاظ على توافق كل من العميل والخادم بشأن كل من نتائج اللاعبين الفرديين ونقاط فريقهم.
function Scoring.incrementScore(player: Player, amount: number)
local team = player.Team
assert(team, `لاعب {player.Name} يجب أن يكون في فريق لتسجيل نقطة، لكنه ليس لديه فريق`)
local teamPoints = team:GetAttribute(GuiAttribute.teamPoints)
teamPoints += amount
team:SetAttribute(GuiAttribute.teamPoints, teamPoints)
local leaderstat = player.leaderstats.Points
leaderstat.Value += amount
teamScoreChangedBindable:Fire(team, teamPoints)
endعرض النتائج
مع تسجيل اللاعبين لبعض النقاط لصالح فريقهم، تقوم ServerScriptService ⟩ Gameplay ⟩ Rounds بالتحقق مما إذا كان الفريق الذي سجل قد حقق هدف الجولة. إذا كانت نتيجة فريقهم أقل من المتغير TEAM_SCORE_LIMIT في ReplicatedStorage ⟩ TEAM_SCORE_LIMIT، حيث يستمر الخادم في الانتظار حتى يسجل أحد الفرق مرة أخرى.
ومع ذلك، بمجرد أن تصل نتيجة فريق إلى المتغير TEAM_SCORE_LIMIT، يقوم السكربت بإطلاق حدث roundWinnerRemote مع اسم اللاعب وفريقه.
-- تحقق مما إذا كانت الجولة قد انتهت بعد كل تسجيل
local team: Team
local score: number = 0
while score < TEAM_SCORE_LIMIT do
team, score = Scoring.teamScoreChanged:Wait()
end
-- عرض الفريق الفائز
for _, player in Players:GetPlayers() do
-- إرسال ما هو الفريق الذي ينتمي إليه اللاعب في نهاية الجولة
-- لأن فريق اللاعب على وشك أن يتم إزالته، لذا فإن العميل
-- لن يكون قادرًا على التحقق من فريقه الخاص
roundWinnerRemote:FireClient(player, team, player.Team)
endيستمع سكربت ReplicatedStorage ⟩ RoundResultsGuiSetup على كل عميل لهذا الحدث roundWinnerRemote حتى يتمكن من:
- عرض واجهة المستخدم الفريدة StarterGui ⟩ RoundResultsGui التي تعلن نتائج الجولة وما إذا كان اللاعب في الفريق الفائز.
- تشغيل مقطع صوتي للنصر أو الهزيمة.
على سبيل المثال، إذا كان لاعب في الفريق الذي سجل النقطة الفائزة، فإنه يتلقى أشكالًا متعددة من الملاحظات حول نتائج الجولة في شكل واجهة مستخدم تعرض نص النصر، ومقطع صوتي يعزف صوتًا مبهجًا. من ناحية أخرى، إذا لم يكن لاعب في الفريق الذي سجل النقطة الفائزة، فإنه يتلقى واجهة مستخدم تعرض نص الهزيمة، ومقطع صوتي يعزف صوتًا مشؤومًا.
local function onRoundWinner(winner: Team, localTeam: Team?)
local victoryDefeatText = "انتهت الجولة!"
if localTeam then
-- إذا فاز فريقنا، سنعرض النصر! وإلا عرض الهزيمة...
local isVictory = winner == localTeam
if isVictory then
victorySound:Play()
victoryDefeatText = VICTORY_TEXT
else
defeatSound:Play()
victoryDefeatText = DEFEAT_TEXT
end
endإعادة تعيين الفرق
في نفس الوقت الذي يتحقق فيه ServerScriptService ⟩ Gameplay ⟩ Rounds من أن فريقًا ما حقق الهدف المطلوب ويفعل مختص الواجهة المناسب لكل لاعب، فإنه ينقل أيضًا جميع اللاعبين من الساحة إلى اللوبي من خلال فصلهم عن الجولة. يبدأ هذا العملية لإنهاء الجولة رسميًا وإعادة تعيين كلا الفريقين.
باستخدام نفس المنطق في Configure Spawn Locations، يقوم Rounds بعد ذلك بتعيين خاصية موقع الظهور Neutral Neutral إلى true حتى يتمكن اللاعبون من الظهور هناك بغض النظر عن حالتهم في الفريق. هذا يعني أن اللوبي يصبح المكان الوحيد الذي يمكن للاعبين الظهور فيه بعد أن يتم فصلهم عن الجولة.
-- أرسل الجميع إلى اللوبي
playerAddedConnection:Disconnect()
neutralSpawn.Neutral = true
spawnPlayersInLobby(Players:GetPlayers())بعد الانتظار لمدة عشر ثوانٍ في فترة الاستراحة، يبدأ سكربت Rounds بعد ذلك الحلقة مرة أخرى من خلال إعادة تعيين نتائج الجميع وفرزهم إلى فرق جديدة. تكرر العينة هذه العملية الدورية حتى لا يتواجد أي لاعبين داخل السيرفر.
الآن بعد أن يمكن للاعبين الظهور في الخريطة مع فريقهم الخاص واللعب في جولة كاملة، يعلّمك القسم التالي عن السكربتات وراء سلوك كل مسدس.