Roblox · Королевская битва · урок 6
Самолёт пролетает над островом, камера смотрит на него, пробел — прыжок. Дальше бой: зона сужается, «Живых: N», последний выживший побеждает и получает +1 к победам.
Синим — что добавляем на этом уроке.
Workspace ├─ PlaneStart, PlaneEnd (Part) — невидимые, высоко над краями острова ├─ SpawnRange (Part) — невидимая плита во весь остров — над ней можно прыгать └─ MapSpawn (Part) — больше не нужна — удали ServerStorage ├─ Plane (Model) — PrimaryPart — Body; все детали Anchored, CanCollide выкл. └─ RoundStart (BindableEvent) — лобби → раунд ReplicatedStorage └─ PlaneEvent (RemoteEvent) ServerScriptService └─ RoundServer (Script) — вместо ZoneServer — удали его Workspace → Lobby → Gate └─ GateScript (Script) — обновлён: отправляет игроков в раунд StarterPlayer → StarterCharacterScripts └─ PlaneLocal (LocalScript) StarterGui → RoundGui ├─ AliveText, CenterText (TextLabel) — «Живых: N» и подсказка/победитель └─ RoundLocal (LocalScript) — обновлён
Где: ServerScriptService · Script
Зачем: Самолёт, бой, победа.
-- игроки
local Players = game:GetService("Players")
-- атрибуты раунда и события
local RS = game:GetService("ReplicatedStorage")
-- образец самолёта
local SS = game:GetService("ServerStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- сохранение побед
local DSS = game:GetService("DataStoreService")
-- модуль зоны
local Zone = require(script.Parent:WaitForChild("ZoneModule"))
-- хранилище
local store = DSS:GetDataStore("BattleRoyale")
-- лобби отправило игроков
local roundStart = SS:WaitForChild("RoundStart")
-- самолёт (PrimaryPart — Body)
local planeSample = SS:WaitForChild("Plane")
-- начало маршрута
local planeStart = workspace:WaitForChild("PlaneStart")
-- конец маршрута
local planeEnd = workspace:WaitForChild("PlaneEnd")
-- над этой областью можно прыгать
local spawnRange = workspace:WaitForChild("SpawnRange")
-- куда вернуть победителя
local lobbySpawn = workspace:WaitForChild("Lobby"):WaitForChild("SpawnLocation")
-- прыжок из самолёта
local planeEvent = RS:WaitForChild("PlaneEvent")
-- секунд полёта от края до края
local FLIGHT_TIME = 30
-- игрок → его персонаж: кто ещё жив в раунде
local alive = {}
-- игрок → true: кто ещё в самолёте
local onBoard = {}
-- самолёт сейчас
local plane = nil
-- Lobby, Plane или Fight
RS:SetAttribute("Phase", "Lobby")
-- имени победителя пока нет
RS:SetAttribute("Winner", "")
-- игрок зашёл: таблица лидеров
local function Setup(player)
-- папка
local stats = Instance.new("Folder")
-- так Roblox показывает её в таблице справа
stats.Name = "leaderstats"
-- в игрока
stats.Parent = player
-- победы
local wins = Instance.new("IntValue")
-- имя
wins.Name = "Wins"
-- в таблицу
wins.Parent = stats
-- загружаем (pcall — сервер может не ответить)
local ok, value = pcall(function() return store:GetAsync(player.UserId .. "Wins") end)
-- были победы — ставим
if ok and value then wins.Value = value end
-- конец функции
end
-- сохранить победы
local function Save(player)
-- таблица
local stats = player:FindFirstChild("leaderstats")
-- сохраняем
if stats then pcall(function() store:SetAsync(player.UserId .. "Wins", stats.Wins.Value) end) end
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end
-- вышел
Players.PlayerRemoving:Connect(function(player)
-- сохраняем
Save(player)
-- больше не в раунде
alive[player] = nil
-- и не в самолёте
onBoard[player] = nil
-- конец функции
end)
-- сервер выключается — сохраняем всех
game:BindToClose(function() for _, player in ipairs(Players:GetPlayers()) do Save(player) end end)
-- самолёт над островом?
local function OverRange(position)
-- координаты относительно SpawnRange
local p = spawnRange.CFrame:PointToObjectSpace(position)
-- внутри по ширине и длине
return math.abs(p.X) <= spawnRange.Size.X / 2 and math.abs(p.Z) <= spawnRange.Size.Z / 2
-- конец функции
end
-- выпустить игрока из самолёта
local function Drop(player)
-- уже прыгнул
if not onBoard[player] or not plane then return end
-- больше не в самолёте
onBoard[player] = nil
-- интерфейс: подсказка пропадает
player:SetAttribute("OnPlane", false)
-- камера снова за персонажем
planeEvent:FireClient(player, false)
-- центр персонажа
local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
-- персонажа нет
if not root then return end
-- под самолётом
root.CFrame = plane:GetPivot() * CFrame.new(0, -15, 0)
-- падает
root.Anchored = false
-- конец функции
end
-- игрок нажал пробел
planeEvent.OnServerEvent:Connect(function(player)
-- только над островом
if plane and OverRange(plane:GetPivot().Position) then Drop(player) end
-- конец функции
end)
-- посадить игроков в самолёт
local function Board(list)
-- новый раунд
table.clear(alive)
-- самолёт пуст
table.clear(onBoard)
-- каждый из лобби
for _, player in ipairs(list) do
-- персонаж
local character = player.Character
-- Humanoid
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
-- центр
local root = character and character:FindFirstChild("HumanoidRootPart")
-- живой и в игре
if player.Parent and humanoid and root and humanoid.Health > 0 then
-- в раунде
alive[player] = character
-- в самолёте
onBoard[player] = true
-- летит вместе с самолётом, а не падает
root.Anchored = true
-- интерфейс: «Пробел — прыгнуть»
player:SetAttribute("OnPlane", true)
-- камера смотрит на самолёт
planeEvent:FireClient(player, true)
-- конец проверки
end
-- конец перебора
end
-- конец функции
end
-- полёт самолёта от PlaneStart к PlaneEnd
local function Fly()
-- в начале смотрит на конец
local startCF = CFrame.lookAt(planeStart.Position, planeEnd.Position)
-- в конце — тот же поворот
local endCF = startCF.Rotation + planeEnd.Position
-- новый самолёт
plane = planeSample:Clone()
-- в начало
plane:PivotTo(startCF)
-- в мир
plane.Parent = workspace
-- сколько летим
local t = 0
-- пока не долетели
while t < FLIGHT_TIME do
-- прошёл кадр
t += RunService.Heartbeat:Wait()
-- точка на маршруте
local cf = startCF:Lerp(endCF, math.min(t / FLIGHT_TIME, 1))
-- самолёт летит
plane:PivotTo(cf)
-- кто внутри
for player in pairs(onBoard) do
-- центр
local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
-- внутри самолёта
if root then root.CFrame = cf end
-- конец перебора
end
-- конец полёта
end
-- кто не прыгнул — сбрасываем в конце маршрута
for player in pairs(onBoard) do Drop(player) end
-- самолёт улетел
plane:Destroy()
-- самолёта нет
plane = nil
-- конец функции
end
-- убрать погибших и вышедших; вернуть число живых
local function CheckAlive()
-- счётчик и последний живой
local count, last = 0, nil
-- каждый в раунде
for player, character in pairs(alive) do
-- Humanoid
local humanoid = character:FindFirstChildOfClass("Humanoid")
-- вышел, возродился или погиб
if not player.Parent or player.Character ~= character or not humanoid or humanoid.Health <= 0 then
-- выбыл
alive[player] = nil
-- жив
else
-- считаем
count += 1
-- запоминаем
last = player
-- конец проверки
end
-- конец перебора
end
-- интерфейс: «Живых: N»
RS:SetAttribute("Alive", count)
-- число и последний
return count, last
-- конец функции
end
-- весь раунд
local function RunRound(list)
-- полёт
RS:SetAttribute("Phase", "Plane")
-- в самолёт
Board(list)
-- один игрок — раунд до его смерти (для проверки)
local solo = CheckAlive() <= 1
-- летим 30 секунд
Fly()
-- бой
RS:SetAttribute("Phase", "Fight")
-- раунд идёт
local running = true
-- волны зоны — отдельно
task.spawn(Zone.Run, function() return running end)
-- победитель
local winner = nil
-- раз в секунду
while true do
-- секунда
task.wait(1)
-- кто жив
local count, last = CheckAlive()
-- список живых
local players = {}
-- собираем
for player in pairs(alive) do table.insert(players, player) end
-- урон тем, кто за зоной
Zone.Hurt(players)
-- остался один (или никого)
if count == 0 or (count == 1 and not solo) then
-- он победил
winner = last
-- раунд окончен
break
-- конец проверки
end
-- конец цикла
end
-- зона останавливается
running = false
-- есть победитель
if winner then
-- +1 победа
winner.leaderstats.Wins.Value += 1
-- все видят имя
RS:SetAttribute("Winner", winner.DisplayName)
-- конец проверки
end
-- празднуем
task.wait(6)
-- надпись пропадает
RS:SetAttribute("Winner", "")
-- предупреждения гаснут
for _, player in ipairs(Players:GetPlayers()) do player:SetAttribute("OutsideZone", false) end
-- победитель — в лобби
if winner and winner.Character then winner.Character:PivotTo(lobbySpawn.CFrame + Vector3.new(0, 4, 0)) end
-- раунда больше нет
table.clear(alive)
-- зона снова большая
Zone.Reset()
-- лобби снова набирает игроков
RS:SetAttribute("Phase", "Lobby")
-- конец функции
end
-- лобби отправило игроков
roundStart.Event:Connect(function(list)
-- раунд не идёт — начинаем
if RS:GetAttribute("Phase") == "Lobby" and #list > 0 then RunRound(list) end
-- конец функции
end)Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Камера и прыжок.
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- прыжок
local UIS = game:GetService("UserInputService")
-- игроки
local Players = game:GetService("Players")
-- сервер: «ты в самолёте» / «ты прыгнул»
local planeEvent = RS:WaitForChild("PlaneEvent")
-- камера
local camera = workspace.CurrentCamera
-- уже в самолёте?
local onPlane = Players.LocalPlayer:GetAttribute("OnPlane") == true
-- сервер сообщил
planeEvent.OnClientEvent:Connect(function(value)
-- запоминаем
onPlane = value
-- прыгнул — камера за персонажем
if not value then camera.CameraType = Enum.CameraType.Custom end
-- конец функции
end)
-- каждый кадр
RunService.RenderStepped:Connect(function()
-- самолёт
local plane = workspace:FindFirstChild("Plane")
-- не летим
if not onPlane or not plane then return end
-- камерой управляет скрипт
camera.CameraType = Enum.CameraType.Scriptable
-- где самолёт
local cf = plane:GetPivot()
-- сзади-сверху смотрим на самолёт
camera.CFrame = CFrame.lookAt(cf.Position - cf.LookVector * 120 + Vector3.new(0, 50, 0), cf.Position)
-- конец функции
end)
-- пробел (или кнопка прыжка на телефоне)
UIS.JumpRequest:Connect(function()
-- просим выпрыгнуть — сервер проверит, что над островом
if onPlane then planeEvent:FireServer() end
-- конец функции
end)Где: Workspace → Lobby → Gate · Script
Зачем: Отправка в раунд.
-- игроки
local Players = game:GetService("Players")
-- события
local RS = game:GetService("ReplicatedStorage")
-- сигнал начала раунда
local SS = game:GetService("ServerStorage")
-- модель Gate
local gate = script.Parent
-- модель Lobby
local lobby = gate.Parent
-- светящаяся рамка — войти в неё
local gatePart = gate:WaitForChild("Gate")
-- табло над воротами
local board = gate:WaitForChild("GuiPart"):WaitForChild("Board"):WaitForChild("Frame")
-- площадка готовых
local readyPart = lobby:WaitForChild("ReadyZone"):WaitForChild("TeleportPart")
-- куда вернуть, если передумал
local leavePart = lobby:WaitForChild("LeaveRoomPart")
-- кнопка «Выйти»
local leaveEvent = RS:WaitForChild("LeaveEvent")
-- затемнение экрана
local teleportingEvent = RS:WaitForChild("TeleportingEvent")
-- BindableEvent: RoundServer начинает раунд
local roundStart = SS:WaitForChild("RoundStart")
-- мест в раунде
local MAX_PLAYERS = 30
-- для проверки одному; в настоящей игре поставь 2 и больше
local MIN_PLAYERS = 1
-- секунд отсчёта
local WAIT_TIME = 30
-- игрок → true: кто вошёл в ворота
local ready = {}
-- идёт отправка — ворота закрыты
local sending = false
-- сколько готовых
local function Count()
-- счётчик
local n = 0
-- каждый готовый
for player in pairs(ready) do
-- вышел из игры — убираем
if player.Parent then n += 1 else ready[player] = nil end
-- конец перебора
end
-- число
return n
-- конец функции
end
-- обновить табло
local function UpdateBoard(status, seconds)
-- готовые
board.Players.Text = "Игроков: " .. Count() .. "/" .. MAX_PLAYERS
-- состояние
board.Status.Text = status
-- отсчёт
board.Time.Text = seconds and (seconds .. " с") or ""
-- конец функции
end
-- кто-то вошёл в ворота
gatePart.Touched:Connect(function(hit)
-- модель персонажа
local character = hit.Parent
-- игрок (а не NPC или предмет)
local player = Players:GetPlayerFromCharacter(character)
-- Humanoid
local humanoid = character:FindFirstChildOfClass("Humanoid")
-- уже готов, отправка или погиб
if not player or ready[player] or sending or not humanoid or humanoid.Health <= 0 then return end
-- раунд уже идёт
if RS:GetAttribute("Phase") ~= "Lobby" then return end
-- мест нет
if Count() >= MAX_PLAYERS then return end
-- готов
ready[player] = true
-- на площадку готовых
character:PivotTo(readyPart.CFrame + Vector3.new(math.random(-6, 6), 3, math.random(-6, 6)))
-- показать кнопку «Выйти»
leaveEvent:FireClient(player, true)
-- конец функции
end)
-- нажал «Выйти»
leaveEvent.OnServerEvent:Connect(function(player)
-- не в списке или уже отправляем
if not ready[player] or sending then return end
-- не готов
ready[player] = nil
-- обратно в лобби
if player.Character then player.Character:PivotTo(leavePart.CFrame + Vector3.new(0, 3, 0)) end
-- спрятать кнопку
leaveEvent:FireClient(player, false)
-- конец функции
end)
-- игрок зашёл
local function Setup(player)
-- персонаж пропал — не готов
player.CharacterRemoving:Connect(function() ready[player] = nil end)
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end
-- бесконечно
while true do
-- идёт раунд
while RS:GetAttribute("Phase") ~= "Lobby" do
-- табло
UpdateBoard("ИДЁТ РАУНД")
-- ждём
task.wait(1)
-- конец ожидания
end
-- отсчёт с начала
local left = WAIT_TIME
-- пока не 0
while left > 0 do
-- мало игроков
if Count() < MIN_PLAYERS then
-- отсчёт заново
left = WAIT_TIME
-- табло
UpdateBoard("ЖДЁМ ИГРОКОВ")
-- хватает
else
-- табло с секундами
UpdateBoard("ГОТОВЬСЯ", left)
-- минус секунда
left -= 1
-- конец проверки
end
-- секунда
task.wait(1)
-- конец отсчёта
end
-- ворота закрыты
sending = true
-- кого отправляем
local list = {}
-- готовые
for player in pairs(ready) do
-- ещё в игре
if player.Parent then
-- в список
table.insert(list, player)
-- кнопка «Выйти» больше не нужна
leaveEvent:FireClient(player, false)
-- экран темнеет
teleportingEvent:FireClient(player)
-- конец проверки
end
-- конец перебора
end
-- список пуст
table.clear(ready)
-- табло
UpdateBoard("ОТПРАВКА")
-- экран успел потемнеть
task.wait(2)
-- RoundServer сажает их в самолёт
roundStart:Fire(list)
-- раунд успел начаться
task.wait(1)
-- ворота снова открыты
sending = false
-- конец цикла
endГде: StarterGui → RoundGui · LocalScript
Зачем: Живые и победитель.
-- игроки
local Players = game:GetService("Players")
-- атрибуты раунда
local RS = game:GetService("ReplicatedStorage")
-- этот игрок
local player = Players.LocalPlayer
-- RoundGui
local gui = script.Parent
-- таймер зоны
local timerFrame = gui:WaitForChild("TimerFrame")
-- что происходит
local info = timerFrame:WaitForChild("Info")
-- мм:сс
local timeLabel = timerFrame:WaitForChild("Time")
-- «Ты за зоной!»
local warnText = gui:WaitForChild("WarnText")
-- сколько живых
local aliveText = gui:WaitForChild("AliveText")
-- подсказка и победитель
local centerText = gui:WaitForChild("CenterText")
-- обновить таймер
local function UpdateTimer()
-- текст от модуля зоны
local text = RS:GetAttribute("ZoneText") or ""
-- секунды
local left = RS:GetAttribute("TimeLeft") or 0
-- нет раунда — таймер скрыт
timerFrame.Visible = text ~= ""
-- текст
info.Text = text
-- минуты и секунды двумя цифрами
timeLabel.Text = string.format("%02d:%02d", left // 60, left % 60)
-- конец функции
end
-- текст сменился
RS:GetAttributeChangedSignal("ZoneText"):Connect(UpdateTimer)
-- секунда прошла
RS:GetAttributeChangedSignal("TimeLeft"):Connect(UpdateTimer)
-- сразу
UpdateTimer()
-- предупреждение
local function UpdateWarn()
-- за зоной — видно
warnText.Visible = player:GetAttribute("OutsideZone") == true
-- конец функции
end
-- вышел или вернулся
player:GetAttributeChangedSignal("OutsideZone"):Connect(UpdateWarn)
-- сразу
UpdateWarn()
-- живые, подсказка, победитель
local function UpdateRound()
-- имя победителя
local winner = RS:GetAttribute("Winner") or ""
-- счётчик — только во время боя
aliveText.Visible = RS:GetAttribute("Phase") == "Fight"
-- сколько
aliveText.Text = "Живых: " .. (RS:GetAttribute("Alive") or 0)
-- раунд закончен
if winner ~= "" then
-- поздравляем
centerText.Text = "Победитель: " .. winner
-- в самолёте
elseif player:GetAttribute("OnPlane") then
-- подсказка
centerText.Text = "Пробел — прыгнуть"
-- иначе
else
-- пусто
centerText.Text = ""
-- конец проверки
end
-- конец функции
end
-- атрибуты раунда
for _, name in ipairs({"Phase", "Alive", "Winner"}) do
-- изменился — обновить
RS:GetAttributeChangedSignal(name):Connect(UpdateRound)
-- конец перебора
end
-- сел в самолёт или прыгнул
player:GetAttributeChangedSignal("OnPlane"):Connect(UpdateRound)
-- сразу
UpdateRound()Запусти Play (или Run) и убедись: