Studio

Почему твоя игра лагает: разбор по причинам

Что на самом деле съедает FPS в Roblox: детали, свет, частицы, физика и скрипты. Как найти виновника инструментами Studio и что делать с каждым из них.

Сложно 24 минуты проверен 1 час назад

Что понадобится

Свой проект в Studio и понимание, где лежат скрипты. Гайд «Клиент и сервер» поможет, но не обязателен.

Текст гайда «Почему твоя игра лагает: разбор по причинам»

Содержание

«Игра лагает» — это не диагноз, а симптом. Причин ровно пять, и лечатся они по-разному. Прежде чем что-то удалять, надо понять, что именно тормозит: картинка, физика, скрипты, сеть или память. Иначе можно вырезать пол-карты и не получить ни одного лишнего кадра.

Сначала измерить, потом чинить#

В Studio на вкладке View есть три вещи, которые отвечают на вопрос «куда уходит время».

Чем смотреть
ИнструментЧто показывает
MicroProfilerПодробную раскладку кадра: сколько времени ушло на отрисовку, физику и скрипты
Консоль разработчика (F9)Память, сеть, ошибки скриптов — уже в запущенной игре
Explorer с фильтромСколько в сцене деталей, частиц и источников света

Смотри на телефоне, а не на своём компьютере

У тебя игра идёт плавно почти всегда. Реальный игрок сидит на телефоне трёхлетней давности, и первым у него ложится именно то, чего ты не замечаешь: свет с тенями, прозрачность и частицы. Проверяй в эмуляторе устройства и по возможности на настоящем телефоне.

Причина первая: картинка#

Отрисовка — самый частый виновник. Дорого стоит не количество деталей само по себе, а количество того, что видно одновременно, и сложность каждого объекта.

  • Прозрачность дороже непрозрачности: стекло поверх стекла поверх дыма — верный способ уронить FPS
  • Тени от каждой лампы в режиме Future красивы и дороги; на мобильной игре чаще выбирают ShadowMap
  • Частицы (ParticleEmitter) с большим Rate и крупным размером бьют по слабым устройствам сильнее всего
  • Мелкие детали, которых не видно с расстояния, всё равно рисуются — если они не нужны, их не должно быть
  • Много разных текстур и материалов — больше памяти; повторяющийся набор лучше уникального на каждый дом

Главный рычаг для больших карт — StreamingEnabled у Workspace. С ним клиент загружает только ближайшую часть мира, а дальнюю подтягивает по мере движения. Для карты размером с город это разница между «играет» и «не запускается на телефоне».

Скрипт, который ждёт подгруженную часть мира · Luau
-- LocalScript. При включённом StreamingEnabled деталь может
-- ещё не приехать к клиенту, и обращение к ней уронит скрипт.

local Workspace = game:GetService("Workspace")

local function whenReady(name, callback)
    local part = Workspace:FindFirstChild(name)

    if part then
        callback(part)

        return
    end

    -- Ждём появления, но не вечно: пять секунд и честная запись в Output.
    task.spawn(function()
        local found = Workspace:WaitForChild(name, 5)

        if found then
            callback(found)
        else
            warn("Не дождались объекта: " .. name)
        end
    end)
end

whenReady("Fountain", function(fountain)
    print("Фонтан на месте:", fountain:GetFullName())
end)

Причина вторая: физика#

Каждая незакреплённая деталь считается физическим движком каждый кадр. Сотня коробок, которые «просто лежат», — это сотня объектов, за которыми сервер следит постоянно.

  • Ставь Anchored = true всему, что не должно двигаться. Это самое дешёвое ускорение из существующих
  • У декораций, сквозь которые всё равно ходят, выключай CanCollide и CanTouch
  • Не оставляй кучи мусора после боёв: осколки и гильзы надо убирать через Debris или таймер
  • Сложная форма считается дороже простой: там, где хватит невидимого куба, не нужен коллайдер по мешу

Причина третья: скрипты#

Скрипт тормозит игру не размером, а частотой. Код, который выполняется каждый кадр для каждого игрока, обязан быть коротким.

Было: поиск по всему миру каждый кадр · Luau
local RunService = game:GetService("RunService")

RunService.Heartbeat:Connect(function()
    -- Обход всей сцены шестьдесят раз в секунду.
    for _, item in ipairs(workspace:GetDescendants()) do
        if item:IsA("BasePart") and item.Name == "Coin" then
            item.CFrame = item.CFrame * CFrame.Angles(0, 0.05, 0)
        end
    end
end)
Стало: список собран один раз · Luau
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

-- Монетам заранее выдан тег «Coin» — в Studio это делает
-- окно Tag Editor, из кода AddTag.
local coins = CollectionService:GetTagged("Coin")

CollectionService:GetInstanceAddedSignal("Coin"):Connect(function(coin)
    table.insert(coins, coin)
end)

RunService.Heartbeat:Connect(function(delta)
    for index = #coins, 1, -1 do
        local coin = coins[index]

        if coin.Parent == nil then
            table.remove(coins, index) -- монету забрали, следить не за чем
        else
            coin.CFrame = coin.CFrame * CFrame.Angles(0, delta * 3, 0)
        end
    end
end)

Вращение — работа клиента

Если монета крутится только для красоты, вращай её в LocalScript. Тогда сервер вообще не участвует, а по сети не идёт ни одного лишнего пакета. Сервер должен знать только факт: монету забрали.

Причина четвёртая: сеть#

Сетевые лаги выглядят иначе, чем просадка FPS: картинка плавная, но персонаж дёргается, а награда приходит с задержкой. Причина почти всегда одна — слишком много сообщений между клиентом и сервером.

  • Не шли FireServer каждый кадр: собери изменения и отправь пачкой раз в несколько кадров
  • FireAllClients с большой таблицей на полном сервере умножается на число игроков
  • Передавай минимум: идентификатор предмета вместо всей его карточки
  • Часто меняющиеся значения (полоска здоровья) обновляй по факту изменения, а не по таймеру

Причина пятая: память и утечки#

Игра, которая идёт нормально первые пять минут и умирает к двадцатой, почти всегда течёт. Классика — подписки на события, которые никто не отключает, и таблицы, куда только добавляют.

Подписка, которая убирает за собой · Luau
local Players = game:GetService("Players")

local connections = {}

Players.PlayerAdded:Connect(function(player)
    local connection = player.CharacterAdded:Connect(function(character)
        print(character.Name .. " появился")
    end)

    connections[player] = connection
end)

Players.PlayerRemoving:Connect(function(player)
    local connection = connections[player]

    if connection then
        connection:Disconnect() -- иначе она живёт до конца сервера
        connections[player] = nil
    end
end)

Проверить утечку просто: открой консоль разработчика на F9, посмотри память в начале и через двадцать минут игры. Если число растёт и не падает, значит что-то накапливается.

Порядок действий, если уже лагает#

  1. Определи вид лага: низкий FPS, рывки раз в несколько секунд или задержка действий
  2. Низкий FPS — смотри картинку: свет, прозрачность, частицы, количество видимого
  3. Рывки — ищи скрипт, который делает тяжёлую работу редко, но всю сразу; раздели её по кадрам
  4. Задержки — считай сообщения по сети и убирай лишние
  5. Растущая память — ищи подписки и таблицы, которые никто не чистит
  6. После каждой правки измеряй заново: без замера легко «оптимизировать» то, что и так работало

Если тормозит не твоя игра, а сам Roblox, лечение другое — оно собрано в гайде «Roblox лагает: что делать». Ошибки из Output разобраны здесь, а проверить правки на разных устройствах поможет гайд про тестирование.

Обсуждение

Пиши по делу и без личных данных. Ссылки на сторонние сайты, обмен и «бесплатные робуксы» мы скрываем — почему.

Комментарии пишут те, кто вошёл — так спокойнее всем.

Войти

Пока никто ничего не написал. Будь первым.