Roblox · Захват флага · урок 8

Общий: вся игра целиком

Итог курса на одной странице: полная структура захвата флага и финальные версии всех скриптов. Удобно сверить свою игру или собрать её заново.

Структура

Синим — что добавляем на этом уроке.

Teams — Синие, Красные (AutoAssignable выкл.)
Workspace
├─ Lobby (Model) — комната на высоте 100, Neutral-спавн
├─ LobbySpawn, GameSpawn (Folder) — SpawnLocation «Синие» и «Красные»
├─ Field (Model) — земля, ограда, укрытия
└─ Bases (Folder)
   └─ Синие, Красные (Model) — Stand, GivePart, ArmorGiver (ArmorScript), Flag → Hitbox, Pole, Cloth
ReplicatedStorage
├─ TeamEvent, WinEvent (RemoteEvent)
├─ Lobby (BoolValue)
└─ TimeLeft (IntValue)
ServerScriptService
└─ TeamsServer, FlagServer, TimerServer (Script)
StarterGui — все ResetOnSpawn выкл.
├─ TeamGui (ScreenGui) — Frame → Синие, Красные; TeamsLocal
├─ TimerGui (ScreenGui) — TimerLabel, TimerLocal
└─ WinnerGui (ScreenGui) — WinLabel, WinLocal

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Имена связывают всё: команда = база в Bases = SpawnLocation в LobbySpawn и GameSpawn = кнопка в TeamGui.
  3. Флаг: Hitbox Anchored вкл., Pole и Cloth приварены, у всех CanCollide выкл. и Massless вкл.
  4. Все ScreenGui — ResetOnSpawn выкл.
  5. Проверяй вдвоём: Test → Clients and Servers → 2 Players. Для быстрой проверки уменьши LOBBY_TIME и GAME_TIME.

Скрипты

TeamsServer

Где: ServerScriptService · Script
Зачем: Выбор команды.

-- команды
local Teams = game:GetService("Teams")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- событие выбора
local teamEvent = RS:WaitForChild("TeamEvent")
-- true — идёт лобби, false — игра
local lobby = RS:WaitForChild("Lobby")

-- игрок нажал кнопку
teamEvent.OnServerEvent:Connect(function(player, teamName)
    -- от клиента пришло не имя — игнорируем
    if typeof(teamName) ~= "string" then return end
    -- команда с таким именем
    local team = Teams:FindFirstChild(teamName)
    -- нет такой или игрок уже в ней
    if not team or not team:IsA("Team") or player.Team == team then return end
    -- записываем в команду
    player.Team = team
    -- лобби или поле — смотря что идёт
    local folder = lobby.Value and workspace.LobbySpawn or workspace.GameSpawn
    -- точка появления команды
    player.RespawnLocation = folder:FindFirstChild(team.Name)
    -- возрождаем там
    player:LoadCharacter()
-- конец функции
end)

FlagServer

Где: ServerScriptService · Script
Зачем: Флаги и очки.

-- игроки
local Players = game:GetService("Players")
-- команды
local Teams = game:GetService("Teams")
-- базы; имя базы = имя команды
local bases = workspace:WaitForChild("Bases")
-- имя базы → функция «вернуть её флаг»
local returns = {}

-- игрок зашёл
Players.PlayerAdded:Connect(function(player)
    -- таблица по Tab
    local leaderstats = Instance.new("Folder")
    -- имя строго такое
    leaderstats.Name = "leaderstats"
    -- в игрока
    leaderstats.Parent = player
    -- принесённые флаги
    local flags = Instance.new("IntValue")
    -- столбец
    flags.Name = "Flags"
    -- в таблицу
    flags.Parent = leaderstats
-- конец функции
end)

-- живой игрок, чья деталь коснулась
local function GetPlayer(hit)
    -- модель персонажа
    local character = hit.Parent
    -- Humanoid
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    -- не персонаж или погиб
    if not humanoid or humanoid.Health <= 0 then return nil end
    -- игрок
    return Players:GetPlayerFromCharacter(character)
-- конец функции
end

-- настраиваем одну базу
local function Setup(base)
    -- команда этой базы
    local team = Teams:WaitForChild(base.Name)
    -- модель флага
    local flag = base:WaitForChild("Flag")
    -- главная деталь флага
    local hitbox = flag:WaitForChild("Hitbox")
    -- куда приносить чужой флаг
    local givePart = base:WaitForChild("GivePart")
    -- место флага на подставке
    local home = hitbox.CFrame
    -- кто несёт флаг
    local carrier = nil
    -- подписки на смерть носителя
    local connections = {}

    -- флаг — обратно на подставку
    local function ReturnFlag()
        -- отписываемся
        for _, connection in ipairs(connections) do connection:Disconnect() end
        -- список пуст
        table.clear(connections)
        -- у игрока больше нет флага
        if carrier then carrier:SetAttribute("HasFlag", nil) end
        -- никто не несёт
        carrier = nil
        -- сварка со спиной
        local weld = hitbox:FindFirstChild("CarryWeld")
        -- убираем
        if weld then weld:Destroy() end
        -- закрепляем
        hitbox.Anchored = true
        -- на подставку
        hitbox.CFrame = home
    -- конец функции
    end
    -- чтобы другая база могла вернуть этот флаг
    returns[base.Name] = ReturnFlag

    -- кто-то коснулся флага
    hitbox.Touched:Connect(function(hit)
        -- флаг уже несут
        if carrier then return end
        -- игрок
        local player = GetPlayer(hit)
        -- свой флаг не берут
        if not player or not player.Team or player.Team == team then return end
        -- два флага сразу нельзя
        if player:GetAttribute("HasFlag") then return end
        -- Humanoid носителя
        local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
        -- к нему крепим
        local root = player.Character:FindFirstChild("HumanoidRootPart")
        -- персонаж не собран
        if not root then return end
        -- запоминаем носителя
        carrier = player
        -- чей флаг у игрока
        player:SetAttribute("HasFlag", base.Name)
        -- сварка со смещением
        local weld = Instance.new("Weld")
        -- имя — чтобы найти и убрать
        weld.Name = "CarryWeld"
        -- спина игрока
        weld.Part0 = root
        -- флаг
        weld.Part1 = hitbox
        -- чуть выше и за спиной
        weld.C0 = CFrame.new(0, 1.5, 1.2)
        -- в флаг
        weld.Parent = hitbox
        -- флаг едет за игроком
        hitbox.Anchored = false
        -- погиб — флаг домой
        table.insert(connections, humanoid.Died:Connect(ReturnFlag))
        -- вышел или возродился — тоже
        table.insert(connections, player.CharacterRemoving:Connect(ReturnFlag))
    -- конец функции
    end)

    -- кто-то пришёл на свою базу
    givePart.Touched:Connect(function(hit)
        -- игрок
        local player = GetPlayer(hit)
        -- только игроки этой команды
        if not player or player.Team ~= team then return end
        -- чей флаг принёс
        local enemyName = player:GetAttribute("HasFlag")
        -- ничего не несёт
        if not enemyName or not returns[enemyName] then return end
        -- чужой флаг возвращается на свою подставку
        returns[enemyName]()
        -- таблица игрока
        local stats = player:FindFirstChild("leaderstats")
        -- +1 флаг
        if stats then stats.Flags.Value += 1 end
    -- конец функции
    end)
-- конец функции
end

-- все базы
for _, base in ipairs(bases:GetChildren()) do
    -- настраиваем
    task.spawn(Setup, base)
-- конец перебора
end

TimerServer

Где: ServerScriptService · Script
Зачем: Раунды и победитель.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- идёт лобби?
local lobby = RS:WaitForChild("Lobby")
-- сколько секунд осталось
local timeLeft = RS:WaitForChild("TimeLeft")
-- команды
local Teams = game:GetService("Teams")
-- объявить победителя
local winEvent = RS:WaitForChild("WinEvent")
-- секунд в лобби
local LOBBY_TIME = 30
-- секунд игры (5 минут)
local GAME_TIME = 300

-- всех игроков с командой — на точки своей команды
local function SendAll(folder)
    -- каждый игрок
    for _, player in ipairs(Players:GetPlayers()) do
        -- команда выбрана
        if player.Team then
            -- точка команды в этой папке
            player.RespawnLocation = folder:FindFirstChild(player.Team.Name)
            -- возрождаем там (флаг сам вернётся)
            player:LoadCharacter()
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- обратный отсчёт
local function Countdown(seconds)
    -- от seconds до 0
    for t = seconds, 0, -1 do
        -- игроки видят время
        timeLeft.Value = t
        -- секунда
        task.wait(1)
    -- конец цикла
    end
-- конец функции
end

-- какая команда принесла больше флагов
local function Winner()
    -- лучшая, её счёт, ничья ли
    local bestTeam, bestScore, draw = nil, -1, false
    -- каждая команда
    for _, team in ipairs(Teams:GetTeams()) do
        -- сумма флагов
        local score = 0
        -- игроки команды
        for _, player in ipairs(team:GetPlayers()) do
            -- таблица
            local stats = player:FindFirstChild("leaderstats")
            -- прибавляем
            if stats then score += stats.Flags.Value end
        -- конец перебора
        end
        -- больше всех
        if score > bestScore then
            -- новый лидер
            bestTeam, bestScore, draw = team, score, false
        -- столько же
        elseif score == bestScore then
            -- ничья
            draw = true
        -- конец проверки
        end
    -- конец перебора
    end
    -- белым
    if not bestTeam or draw then return "Ничья!", Color3.new(1, 1, 1) end
    -- цветом команды
    return "Победили " .. bestTeam.Name .. "! Флагов: " .. bestScore, bestTeam.TeamColor.Color
-- конец функции
end

-- раунды по кругу
while true do
    -- лобби
    lobby.Value = true
    -- все в лобби
    SendAll(workspace.LobbySpawn)
    -- ждём
    Countdown(LOBBY_TIME)
    -- игра
    lobby.Value = false
    -- обнуляем счёт
    for _, player in ipairs(Players:GetPlayers()) do
        -- таблица
        local stats = player:FindFirstChild("leaderstats")
        -- с нуля
        if stats then stats.Flags.Value = 0 end
    -- конец перебора
    end
    -- все на поле
    SendAll(workspace.GameSpawn)
    -- играем
    Countdown(GAME_TIME)
    -- показываем победителя всем
    winEvent:FireAllClients(Winner())
    -- пока читают
    task.wait(5)
-- конец цикла
end

TeamsLocal

Где: StarterGui → TeamGui · LocalScript
Зачем: Кнопки команд.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- событие выбора команды
local teamEvent = RS:WaitForChild("TeamEvent")
-- TeamGui
local gui = script.Parent
-- окно с кнопками
local frame = gui:WaitForChild("Frame")

-- все объекты окна
for _, button in ipairs(frame:GetChildren()) do
    -- только кнопки
    if button:IsA("TextButton") then
        -- нажатие
        button.MouseButton1Click:Connect(function()
            -- имя кнопки = имя команды
            teamEvent:FireServer(button.Name)
            -- прячем окно
            gui.Enabled = false
        -- конец функции
        end)
    -- конец проверки
    end
-- конец перебора
end

TimerLocal

Где: StarterGui → TimerGui · LocalScript
Зачем: Время на экране.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- идёт лобби?
local lobby = RS:WaitForChild("Lobby")
-- секунды
local timeLeft = RS:WaitForChild("TimeLeft")
-- надпись
local label = script.Parent:WaitForChild("TimerLabel")

-- обновить надпись
local function Update()
    -- секунды
    local t = timeLeft.Value
    -- что идёт
    local prefix = lobby.Value and "До начала: " or "До конца: "
    -- минуты и секунды с нулём: 01:05
    label.Text = prefix .. string.format("%02d:%02d", t // 60, t % 60)
-- конец функции
end

-- каждую секунду
timeLeft.Changed:Connect(Update)
-- лобби ↔ игра
lobby.Changed:Connect(Update)
-- сразу при входе
Update()

WinLocal

Где: StarterGui → WinnerGui · LocalScript
Зачем: Победитель.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- событие победы
local winEvent = RS:WaitForChild("WinEvent")
-- WinnerGui
local gui = script.Parent
-- надпись
local label = gui:WaitForChild("WinLabel")
-- сколько секунд показывать
local SHOW_TIME = 5

-- сервер объявил победителя
winEvent.OnClientEvent:Connect(function(text, color)
    -- текст
    label.Text = text
    -- цвет команды
    label.TextColor3 = color
    -- показываем
    gui.Enabled = true
    -- ждём
    task.wait(SHOW_TIME)
    -- прячем
    gui.Enabled = false
-- конец функции
end)

ArmorScript

Где: Bases → команда → ArmorGiver · Script
Зачем: Броня.

-- деталь, выдающая броню
local giver = script.Parent
-- сколько здоровья добавляет
local BONUS = 100

-- касание
giver.Touched:Connect(function(hit)
    -- персонаж
    local character = hit.Parent
    -- Humanoid
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    -- не игрок, погиб или броня уже есть
    if not humanoid or humanoid.Health <= 0 or character:GetAttribute("Armor") then return end
    -- туловище R15 или R6
    local torso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso")
    -- нет туловища
    if not torso then return end
    -- у этого персонажа броня есть (после смерти — снова нет)
    character:SetAttribute("Armor", true)
    -- больше максимум
    humanoid.MaxHealth += BONUS
    -- полное здоровье
    humanoid.Health = humanoid.MaxHealth
    -- нагрудник
    local plate = Instance.new("Part")
    -- имя
    plate.Name = "ArmorPlate"
    -- чуть больше туловища
    plate.Size = torso.Size + Vector3.new(0.3, 0.1, 0.3)
    -- цвет как у выдачи
    plate.Color = giver.Color
    -- металл
    plate.Material = Enum.Material.Metal
    -- не мешает ходить
    plate.CanCollide = false
    -- лучи сквозь
    plate.CanQuery = false
    -- без веса
    plate.Massless = true
    -- на туловище
    plate.CFrame = torso.CFrame
    -- сварка
    local weld = Instance.new("WeldConstraint")
    -- туловище
    weld.Part0 = torso
    -- нагрудник
    weld.Part1 = plate
    -- в нагрудник
    weld.Parent = plate
    -- в персонажа
    plate.Parent = character
-- конец функции
end)

Проверка

Запусти Play (или Run) и убедись:

Собрать всю игру одной командой