Интерфейс игры в 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 — пиксели. Интерфейс, собранный в пикселях, отлично выглядит на мониторе разработчика и разъезжается на телефоне. Поэтому размеры задают в долях, а пиксели оставляют для отступов и рамок.
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 = buttonAnchorPoint — вторая половина рецепта. Он говорит, какая точка самого элемента встаёт в заданную позицию: (0, 0) — левый верхний угол, (0.5, 0.5) — центр, (1, 1) — правый нижний. Без него элемент цепляется левым верхним углом и вылезает за экран, когда его двигают вправо.
Проверяй в эмуляторе устройства
В Studio на вкладке «Test» есть «Device»: экран переключается на конкретный телефон или планшет. Полминуты проверки там экономят вечер жалоб «на мобилке всё съехало». Заодно посмотри альбомную и портретную ориентацию.
Счётчик, который обновляется сам#
Числа на экране почти всегда живут в leaderstats — папке с числовыми значениями внутри игрока. Клиенту достаточно подписаться на изменение, и надпись обновится сама, без опроса в цикле.
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.
Окно, которое открывается и закрывается#
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.
Обсуждение
Пиши по делу и без личных данных. Ссылки на сторонние сайты, обмен и «бесплатные робуксы» мы скрываем — почему.
Комментарии пишут те, кто вошёл — так спокойнее всем.
ВойтиПока никто ничего не написал. Будь первым.