Сообщение об ошибке в Roblox Studio почти всегда говорит о типах: ожидалось одно, пришло другое. Ребёнок читает «attempt to perform arithmetic on a Vector3» и видит набор латиницы, хотя на самом деле игра объяснила проблему довольно точно — просто на языке, которого ещё не знают.
Типов в Roblox немного, и половина из них есть в любом языке. Остальные — собственные, придуманные именно для трёхмерного мира, и как раз они сначала путают. Ниже справочник по всем, с которыми ребёнок встретится в первый год, и разбор четырёх мест, где они подводят.
Обычные типы
| Тип | Что это | Пример |
|---|---|---|
| number | число, целое или дробное | 12, 0.5 |
| string | текст в кавычках | 'Olia' |
| boolean | да или нет | true, false |
| nil | ничего, пустое место | nil |
| table | набор значений | {1, 2, 3} |
Первая неожиданность — числа здесь одни на все случаи. Деление не требует отдельного типа, и 10 / 4 честно даёт 2.5. Для количества жизней или очков это иногда вредит: после нескольких операций в переменной оказывается что-то вроде 2.9999, и на экране появляется смешное число. Лечится округлением перед показом.
Типы, которых нет в других языках
| Тип | Для чего | Как создать |
|---|---|---|
| Vector3 | точка или размер в пространстве | Vector3.new(4, 1, 4) |
| CFrame | место вместе с поворотом | CFrame.new(0, 10, 0) |
| Color3 | цвет | Color3.fromRGB(255, 0, 0) |
| UDim2 | размер элемента интерфейса | UDim2.new(0.5, 0, 0.2, 0) |
| Enum | значение из списка | Enum.Material.Neon |
| Instance | сам объект сцены | Instance.new('Part') |
Эти шесть строк объясняют большинство ошибок в первых скриптах. Размер детали — не число, а Vector3 из трёх чисел, поэтому chast.Size = 4 не работает. Материал — не строка, поэтому chast.Material = 'Neon' тоже падает, нужно значение из Enum.
Разница между Vector3 и CFrame стоит отдельного слова. Первый описывает только место или размер, второй — место вместе с поворотом. Пока объект не крутится, хватает Vector3; как только нужно развернуть, берут CFrame, и тогда перемещение с поворотом происходит одним присваиванием, без рывка.
Enum: список, из которого выбирают
Там, где значений немного и они заранее известны, Roblox не принимает ни числа, ни текста — только значение из своего перечня. Материалы, клавиши, состояния игрока, типы освещения устроены именно так.
chast.Material = Enum.Material.Wood
if klavisha == Enum.KeyCode.E then
print('нажата E')
end
Выглядит длинно, зато ошибиться почти невозможно: Studio подсказывает продолжение после каждой точки, а несуществующее имя сразу подсвечивается. Текстовая строка такой проверки не дала бы, и ошибку в слове нашли бы уже во время игры.
Искать нужное значение удобнее не в справочниках, а прямо в панели свойств: там эти же списки показаны выпадающим меню, и название в коде совпадает с подписью в панели.
Цвет: два способа и главная путаница
Цвет создаётся двумя разными функциями, и они принимают разные числа. Это самая частая мелкая ошибка в проектах с интерфейсом.
Color3.fromRGB(255, 0, 0) -- красный, числа от 0 до 255
Color3.new(1, 0, 0) -- тот же красный, числа от 0 до 1
Если перепутать и написать Color3.new(255, 0, 0), ошибки не будет: игра примет число и обрежет его до максимума. Результат будет выглядеть просто слишком ярким, а причина при этом нигде не указана. Совет простой: брать fromRGB всегда, потому что именно в этих числах цвета записаны во всех палитрах и редакторах.
UDim2: почему интерфейс едет на чужом экране
Тип для интерфейса устроен сложнее остальных, и причина в том, что экраны у игроков разные. Каждое из двух измерений задаётся парой чисел: доля от экрана и точное количество пикселей сверху.
knopka.Size = UDim2.new(0.5, 0, 0.1, 0) -- половина ширины экрана
knopka.Size = UDim2.new(0, 200, 0, 50) -- ровно 200 на 50 пикселей
Первый вариант подстраивается: на телефоне кнопка будет уже в пикселях, но так же займёт половину ширины. Второй всегда одинаковый, и на маленьком экране может занять его целиком.
Отсюда простой совет: размеры и положение крупных элементов задавать долями, а мелкие отступы — пикселями. Проект, сделанный целиком в пикселях, на компьютере автора выглядит безупречно, а на телефоне рассыпается, и понять причину без этого объяснения почти невозможно.
Арифметика с собственными типами
Vector3 можно складывать и вычитать, и это самый удобный способ сдвинуть объект относительно текущего места:
chast.Position = chast.Position + Vector3.new(0, 5, 0)
А вот прибавить к Vector3 обычное число нельзя — именно об этом говорит ошибка из начала статьи. Умножать на число, наоборот, разрешено: так вектор удлиняют или укорачивают, сохраняя направление.
Полезная мелочь, о которой редко рассказывают: у вектора есть Magnitude — его длина. Разность двух позиций, взятая по длине, даёт расстояние между объектами, и это кратчайший способ проверить, близко ли игрок:
local rasstoyanie = (igrok.Position - dver.Position).Magnitude
if rasstoyanie < 10 then
print('игрок рядом')
end
Таблицы нумеруются с единицы
Вот место, где Lua отличается почти от всех языков, которые ребёнок увидит потом. Первый элемент имеет номер один, а не ноль.
cveta = {'red', 'green', 'blue'}
print(cveta[1]) -- red
print(#cveta) -- 3
Значок решётки перед именем даёт длину. Привыкнуть к нумерации с единицы легко, сложнее будет потом: в Python и большинстве других языков счёт снова с нуля, и на переходе дети путаются второй раз. Мы на занятиях эту разницу проговариваем заранее, чтобы она не выглядела чьей-то ошибкой.
Таблица умеет и вторую роль — хранить пары «имя и значение», как словарь. Тогда обращаются не по номеру, а по ключу: igrok['ochki'] или короче igrok.ochki. Обе роли живут в одном типе, и это одна из причин, почему таблица в Lua встречается буквально везде.
nil и самая частая ошибка во всём Studio
Значение nil означает «здесь ничего нет». Его возвращает поиск, который ничего не нашёл, и оно же лежит в переменной, которой ничего не присвоили.
Сообщение attempt to index nil value — самое распространённое в детских проектах. Читается оно так: «ты попросил что-то у пустоты». Обычно причина одна из трёх: имя объекта написано с ошибкой, объект лежит не там, где его ищут, или скрипт обратился к нему раньше, чем тот появился.
local dver = workspace:FindFirstChild('Dver')
if dver then
dver.Transparency = 0.5
end
Проверка перед использованием снимает эту ошибку полностью. Привычка писать её сразу стоит одной строки, а экономит часы поиска — и дальше точно так же работает в любом языке.
Преобразование между текстом и числом
Текст и число в Lua не смешиваются сами, и попытка сложить их даёт ошибку. Для перевода есть две короткие функции.
local ochki = 120
print('Баллов: ' .. tostring(ochki))
local vvedeno = '42'
print(tonumber(vvedeno) + 8) -- 50
Две точки подряд — это склеивание текста, аналог плюса в других языках. Запоминается не сразу, зато потом узнаётся мгновенно: если в строке видны две точки, значит собирается сообщение.
Функция tonumber на тексте, который числом не является, возвращает nil, а не падает. Это удобно для проверки введённого: если результат пустой, пользователь написал не то. Такую проверку стоит ставить везде, где игрок может что-то набрать сам.
Как узнать, что именно пришло
Когда непонятно, с чем скрипт имеет дело, есть функция typeof. Она знает и обычные типы, и собственные типы Roblox:
print(typeof(5)) -- number
print(typeof(Vector3.new(1, 2, 3))) -- Vector3
print(typeof(workspace.Part)) -- Instance
Рядом живёт похожая функция type без окончания, но она проще и на Vector3 ответит просто userdata, что ничего не объясняет. Для Roblox всегда берут typeof.
Для объектов сцены полезнее проверка IsA: запись obj:IsA('BasePart') отвечает, является ли объект деталью, и именно её ставят в циклах по содержимому модели, чтобы не пытаться закрепить звук.
Типы лежат в основе всего остального: свойств, событий, интерфейса. Синтаксис, в котором они живут, разобран в справочнике Lua, список свойств детали — здесь, а как заставить деталь реагировать, показано в справочнике событий.