Сообщение об ошибке в 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, список свойств детали — здесь, а как заставить деталь реагировать, показано в справочнике событий.