إضافة الجولات يتيح لك هيكلة طريقة اللعب إلى مراحل مع نقطة بداية ونهاية واضحة حتى يتمكن اللاعبون من قياس تقدمهم والحصول على فرصة دورية لميدان لعب متساوي. هذا مهم بشكل خاص للعب القائم على الفرق لأنه يوفر للاعبين فرصة لتغيير أسلوب لعبهم اعتمادًا على من هو في فريقهم خلال تلك الجولة.
باستخدام لعبة الليزر التاجية النموذجية كمرجع، يعلمك هذا القسم من البرنامج التعليمي كيفية استخدام وتخصيص ميزات 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 لموقع الظهور أن يظهروا هناك. نظرًا لأن خاصية TeamColor لموقع الظهور تم تعيينها إلى الأبيض بدلاً من فرق النعناع أو الوردي، فإن هذا التكوين يمنع جميع اللاعبين من الظهور أو إعادة الظهور هناك بينما تكون الجولة نشطة.
بالنسبة للاعبين الذين هم حاليًا في اللوبي، تقوم startRoundLoopAsync() بتمرير جميع اللاعبين الموجودين حاليًا في اللعبة إلى دالة spawnPlayersInMap في ServerScriptService ⟩ Gameplay ⟩ Rounds ⟩ spawnPlayersInMap لفرز وتوازن الجميع في فريق مع عدد تقريبي متساوي من اللاعبين.
بالنسبة لأي لاعبين جدد ينضمون إلى اللعبة بعد أن تم فرز مجموعة اللوبي إلى فريق، تستمع startRoundLoopAsync() لحدث Players.PlayerAdded:Connect، ثم تستدعي دالة spawnPlayersInMap مرة أخرى لإضافتهم إلى الفريق الذي لديه أقل عدد من اللاعبين. لمزيد من المعلومات حول هذه العملية، راجع تكوين مواقع الظهور من القسم السابق الظهور وإعادة الظهور من البرنامج التعليمي.
-- Spawn all players in the map
neutralSpawn.Neutral = false
spawnPlayersInMap(Players:GetPlayers())
-- Spawn new players in the map when they join
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 -- updated line, be sure to change back
return TEAM_SCORE_LIMIT
تعمل هذه التحديثات البسيطة للمتغير في وقت التشغيل لأنه عند بدء الجولة، تتطلب ReplicatedStorage ⟩ HUDGuiSetup ⟩ SetObjective وحدة البرنامج النصي TEAM_SCORE_LIMIT حتى تتمكن من تبديل سلسلة العنصر النائب في كائن TextLabel لواجهة المستخدم للهدف.
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 النعناع وخاصية teamColor الخاصة بـ TeamBCounter الوردي تتطابق مع خصائص 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 {player.Name} must be on a team to score a point, but has no team`)
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 مع اسم اللاعب وفريقه.
-- Check if the round has finished after each score
local team: Team
local score: number = 0
while score < TEAM_SCORE_LIMIT do
team, score = Scoring.teamScoreChanged:Wait()
end
-- Display winning team
for _, player in Players:GetPlayers() do
-- Sending what team the player is on at the end of the round
-- because the player's team is about to be removed, so the client
-- won't be able to check its own team
roundWinnerRemote:FireClient(player, team, player.Team)
endتستمع وحدة ReplicatedStorage ⟩ RoundResultsGuiSetup على كل عميل لهذا الحدث roundWinnerRemote حتى تتمكن من:
- عرض شاشة واجهة المستخدم الفريدة StarterGui ⟩ RoundResultsGui التي تعلن عن نتائج الجولة وما إذا كان اللاعب في الفريق الفائز.
- تشغيل مقطع صوتي للنصر أو الهزيمة.
على سبيل المثال، إذا كان اللاعب في الفريق الذي سجل النقطة الفائزة، فإنهم يتلقون أشكالًا متعددة من التعليقات حول نتائج الجولة في شكل شاشة واجهة المستخدم التي تعرض نص النصر، ومقطع صوتي يعزف صوتًا مبهجًا. على العكس من ذلك، إذا لم يكن اللاعب في الفريق الذي سجل النقطة الفائزة، فإنهم يتلقون شاشة واجهة المستخدم التي تعرض نص الهزيمة، ومقطع صوتي يعزف صوتًا ominous.
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 من أن فريقًا ما قد حقق الهدف في الجولة ويقوم بتفعيل عرض واجهة المستخدم المناسب لكل لاعب، فإنه أيضًا ينقل جميع اللاعبين من الساحة إلى اللوبي عن طريق فصلهم عن الجولة. يبدأ هذا عملية إنهاء الجولة رسميًا وإعادة تعيين كلا الفريقين.
باستخدام نفس المنطق في تكوين مواقع الظهور، تقوم Rounds بعد ذلك بتعيين خاصية موقع الظهور Neutral Neutral إلى true حتى يتمكن اللاعبون من الظهور هناك بغض النظر عن حالة فريقهم. هذا يعني أن اللوبي يصبح الموقع الوحيد الذي يمكن للاعبين الظهور فيه بعد فصلهم عن الجولة.
-- Send everyone to the lobby
playerAddedConnection:Disconnect()
neutralSpawn.Neutral = true
spawnPlayersInLobby(Players:GetPlayers())بعد الانتظار لمدة عشر ثوانٍ لفترة استراحة، يبدأ البرنامج النصي للخادم Rounds الحلقة مرة أخرى عن طريق إعادة تعيين درجات الجميع وفرزهم إلى فرق جديدة. تكرر العينة هذه العملية الدورية للجولة مرة أخرى حتى لا يكون هناك أي لاعبين داخل الخادم.
الآن بعد أن يمكن للاعبين الظهور في الخريطة مع فريقهم الخاص واللعب في جولة كاملة، يعلمك القسم التالي عن البرامج النصية وراء سلوك كل بازوكا.