Studio

Интерфейс игры в Roblox Studio: кнопки, которые не разъезжаются

Собираем экранный интерфейс: счётчик монет, кнопка магазина, окно с подтверждением. И главное — делаем так, чтобы на телефоне всё это не съехало и не закрыло половину экрана.

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

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

Roblox Studio и понимание, чем Script отличается от LocalScript. Основы языка закрывает гайд «Luau с нуля».

Текст гайда «Интерфейс игры в Roblox Studio: кнопки, которые не разъезжаются»

Содержание

Интерфейс в Roblox живёт в StarterGui: всё, что там лежит, копируется каждому игроку при появлении на сервере. Внутри — ScreenGui, контейнер поверх экрана, а уже в нём кнопки, надписи и картинки. Это клиентская история целиком: интерфейс рисует и обрабатывает клиент, а решения по игре всё равно принимает сервер.

Из чего собирается экран#

Основные объекты
ОбъектЧто делает
ScreenGuiСлой поверх экрана. Всё остальное лежит внутри него
FrameПрямоугольник-контейнер: группирует элементы и двигает их вместе
TextLabelНадпись. Нажатия не принимает
TextButtonКнопка с текстом
ImageLabel и ImageButtonТо же самое, но с картинкой
UICornerСкругление углов у родителя
UIListLayoutРаскладывает детей в столбик или строку
UIAspectRatioConstraintДержит пропорции при любом размере экрана

ResetOnSpawn: почему интерфейс исчезает

У ScreenGui есть свойство ResetOnSpawn, и по умолчанию оно включено: после смерти персонажа интерфейс пересоздаётся заново, а всё, что скрипт в нём успел изменить, сбрасывается. Для постоянного HUD его выключают, для временных окон — оставляют.

Scale вместо Offset — главное правило#

Размер и положение задаются типом UDim2, у которого на каждую ось по два числа: Scale — доля экрана, Offset — пиксели. Интерфейс, собранный в пикселях, отлично выглядит на мониторе разработчика и разъезжается на телефоне. Поэтому размеры задают в долях, а пиксели оставляют для отступов и рамок.

Кнопка, которая держится в углу на любом экране · Luau
local player = game.Players.LocalPlayer
local gui = Instance.new("ScreenGui")
gui.Name = "Hud"
gui.ResetOnSpawn = false
gui.Parent = player:WaitForChild("PlayerGui")

local button = Instance.new("TextButton")
-- UDim2.new(ScaleX, OffsetX, ScaleY, OffsetY)
button.Size = UDim2.new(0.18, 0, 0.08, 0)      -- доля экрана, а не пиксели
button.Position = UDim2.new(0.98, 0, 0.04, 0)  -- правый верхний угол
button.AnchorPoint = Vector2.new(1, 0)         -- точка привязки — тоже правый верх
button.BackgroundColor3 = Color3.fromRGB(91, 92, 246)
button.TextColor3 = Color3.new(1, 1, 1)
button.Font = Enum.Font.GothamBold
button.TextScaled = true
button.Text = "Магазин"
button.Parent = gui

local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = button

AnchorPoint — вторая половина рецепта. Он говорит, какая точка самого элемента встаёт в заданную позицию: (0, 0) — левый верхний угол, (0.5, 0.5) — центр, (1, 1) — правый нижний. Без него элемент цепляется левым верхним углом и вылезает за экран, когда его двигают вправо.

Проверяй в эмуляторе устройства

В Studio на вкладке «Test» есть «Device»: экран переключается на конкретный телефон или планшет. Полминуты проверки там экономят вечер жалоб «на мобилке всё съехало». Заодно посмотри альбомную и портретную ориентацию.

Счётчик, который обновляется сам#

Числа на экране почти всегда живут в leaderstats — папке с числовыми значениями внутри игрока. Клиенту достаточно подписаться на изменение, и надпись обновится сама, без опроса в цикле.

LocalScript: подписка на изменение значения · Luau
local player = game.Players.LocalPlayer
local stats = player:WaitForChild("leaderstats")
local coins = stats:WaitForChild("Coins")

local label = script.Parent -- TextLabel внутри ScreenGui

local function redraw()
    label.Text = ("Монет: %d"):format(coins.Value)
end

coins.Changed:Connect(redraw)
redraw() -- первый раз рисуем сразу, не дожидаясь изменения

WaitForChild здесь обязателен: клиент запускается раньше, чем сервер успевает создать leaderstats, и обращение к ещё не существующему объекту уронит скрипт. Как это выглядит в Output, разобрано в гайде про ошибки Luau.

Окно, которое открывается и закрывается#

Показ и скрытие панели · Luau
local shop = gui:WaitForChild("ShopFrame")
local openButton = gui:WaitForChild("OpenShop")
local closeButton = shop:WaitForChild("Close")

shop.Visible = false

openButton.Activated:Connect(function()
    shop.Visible = true
end)

closeButton.Activated:Connect(function()
    shop.Visible = false
end)

Событие Activated лучше, чем MouseButton1Click: оно одинаково срабатывает от мыши, от пальца и от геймпада. Игру, где кнопки нажимаются только мышью, половина аудитории просто не сможет пройти.

Кнопка «Купить» ничего не покупает

Нажатие на клиенте — это только намерение. Списывать монеты и выдавать предмет должен сервер, получив событие. Иначе покупку можно совершить, не имея денег. Как выстроить эту границу, разобрано в гайде «Клиент и сервер: RemoteEvent без дыр».

Телефон: что ломается чаще всего#

  • Кнопка меньше примерно 44 пикселей по стороне — пальцем в неё не попасть
  • Нижний левый угол занят джойстиком движения, нижний правый — кнопкой прыжка: свои кнопки туда не ставят
  • Верхний левый угол — системная кнопка Roblox, её тоже нельзя перекрывать
  • Текст с фиксированным TextSize на маленьком экране обрезается: включай TextScaled
  • Полноэкранное окно поверх всего лучше панели сбоку: на 390 пикселях ширины боковая панель съедает пол-экрана
  • Проверь альбомную ориентацию — большинство играет именно в ней, а собирают интерфейс обычно в портретной

Полезный приём — разный интерфейс для разных устройств. Определить платформу можно через UserInputService: TouchEnabled без KeyboardEnabled означает телефон или планшет. На таком экране разумно убрать подсказки про клавиши и увеличить кнопки.

Порядок в дереве объектов#

  • Один ScreenGui на смысловую группу: HUD, магазин, настройки — а не всё в одном
  • Имена без пробелов и по делу: ShopFrame, а не Frame7
  • ZIndex решает, что поверх чего: у окна поверх HUD число должно быть больше
  • IgnoreGuiInset нужен, если элемент должен заходить под верхнюю системную полосу
  • Повторяющиеся элементы списка держи шаблоном в ReplicatedStorage и клонируй, а не создавай из кода по свойству за раз

Когда интерфейс собран, остаётся сохранить то, что он показывает: «DataStore: как сохранить прогресс игрока». Весь курс по Luau и справочник ошибок — в разделе Studio.

Обсуждение

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

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

Войти

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