Luau - типизированный диалект Lua от Roblox
Luau
Что такое Luau?
Luau — это диалект языка программирования Lua, разработанный компанией Roblox Corporation как усовершенствованная, промышленно-ориентированная версия Lua 5.1, адаптированная под нужды платформы Roblox. Хотя синтаксически и семантически Luau остаётся совместимым с Lua 5.1 на уровне базовой лексики и исполнения, он вносит существенные изменения в модель типизации, инструментарий времени разработки и производительность. Эти изменения отвечают на вызовы, с которыми сталкиваются разработчики крупных интерактивных миров — необходимость в статической проверке ошибок, поддержка командной работы, масштабируемость кодовой базы и гарантии безопасности при разделении логики между клиентом и сервером.
Luau — это архитектурное расширение, затрагивающее всю цепочку разработки: от редактирования кода в среде Roblox Studio до исполнения в виртуальной машине на клиенте и сервере. В отличие от многих других форков Lua (например, MoonScript, Fennel или Typed Lua, последний из которых оказал влияние на ранние версии Luau), Luau развивается не как независимый проект с открытым исходным кодом, а как внутренняя технология, глубоко интегрированная в экосистему Roblox, и одновременно — как open-source реализация: с 2020 года Roblox Corporation публикует исходный код компилятора и статического анализатора Luau под лицензией MIT на GitHub. Это позволяет изучать язык как инструмент для создания игр и как интересный кейс эволюции динамически типизированного языка в сторону гибридной типизации без ущерба для производительности и доступности.
Ниже мы подробно рассмотрим как лингвистические, так и системные особенности Luau, а также их проекцию на архитектуру приложений в среде Roblox.
Происхождение и философия Luau
Lua 5.1, выпущенный в 2006 году, остаётся одним из самых компактных и гибких скриптовых языков, идеально подходящим для встраивания в приложения. Его минимализм, наличие корутин, таблиц как универсальной структуры данных и гибкой метапрограммирования через метатаблицы сделали его популярным в игровой индустрии: от World of Warcraft до Factorio. Однако по мере роста сложности проектов, разрабатываемых на платформе Roblox (которая к середине 2010-х перешла от конструктора мини-игр к полноценной платформе для создания persistent-вселенных), выяснилось, что чисто динамическая природа Lua создаёт серьёзные проблемы:
- отсутствие возможности выявлять опечатки в именах полей или вызовах методов на этапе написания кода;
- невозможность построения точных контрактов интерфейсов между модулями;
- сложность поддержки кода в командах из нескольких разработчиков;
- отсутствие инструментов автодополнения и рефакторинга, сравнимых с теми, что доступны для TypeScript или C#.
Первоначально Roblox использовал Lua 5.1 с некоторыми внутренними расширениями, включая собственную библиотеку для работы с объектной моделью (Roblox Instance API). Однако с 2019 года началась постепенная замена интерпретатора Lua на собственную виртуальную машину, а в 2020 — появление первого прототипа статической типизации. В 2021 году был представлен Luau Type Checker и Luau Language Server, а в 2022 — официальное объявление о переходе всей платформы на Luau.
Ключевой философский принцип Luau — ненавязчивая статика (gradual typing). Это означает:
- код без аннотаций типов остаётся валидным и исполняется точно так же, как в Lua 5.1;
- аннотации не влияют на семантику во время выполнения — они обрабатываются только на этапе статического анализа;
- разработчик может постепенно добавлять типы, начиная с критически важных модулей, не переписывая всю кодовую базу.
Такой подход позволяет сохранить низкий порог входа для новичков (в том числе детей и подростков, составляющих значительную долю аудитории Roblox), одновременно предоставляя профессионалам инструменты для построения надёжных систем.
Статическая типизация
Синтаксис аннотаций типов
Основной способ указания типа — аннотация после двоеточия, как в TypeScript:
local playerName: string = "Guest"
local health: number = 100
local isAlive: boolean = true
Разбор:
- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Аннотации могут быть указаны:
- для локальных переменных;
- для параметров функций;
- для возвращаемых значений (в том числе множественных);
- для полей таблиц (включая классы, построенные на основе метатаблиц).
Пример функции с полной типизацией:
type Vector3 = { x: number, y: number, z: number }
function calculateDistance(a: Vector3, b: Vector3): number
local dx = a.x - b.x
local dy = a.y - b.y
local dz = a.z - b.z
return math.sqrt(dx*dx + dy*dy + dz*dz)
end
Разбор:
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект.returnзавершает функцию и может вернуть несколько значений через запятую.- Вызовы из стандартных библиотек
math— готовые функции языка, не нужно писать их с нуля. - Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
type Имя = { … }в Luau объявляет именованный тип для статической проверки.- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
math.sqrt(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Обратите внимание: type — это директива для анализатора. Она не создаёт объектов, не генерирует проверок в рантайме. Это чисто статическая конструкция.
Система типов Luau
Система типов Luau включает:
- Примитивные типы —
nil,boolean,number,string,thread,userdata(представлен какInstanceи его подтипы — см. ниже). - Составные типы:
table<K, V>— параметризованный тип таблицы;{ field1 — T1, field2: T2, ... }— структурные типы ("анонимные интерфейсы");(T1, T2) -> T3— тип функции;
- Специальные типы:
any— "выключатель" типизации, эквивалентunknownв TypeScript по строгости (но на практике ближе кany, так как разрешает любые операции);never— невыполнимый тип (например, для функции, всегда вызывающейerror);?T(илиT?) — сокращение дляT | nil, nilable-тип;
- Union-типы:
string | number,Part | Model | nil; - Generics (универсальные типы), введённые в 2023 году:
type LinkedList<T> = { value: T, next: LinkedList<T>? }
function map<T, U>(list: LinkedList<T>, f: (T) -> U): LinkedList<U>
if list == nil then return nil end
return { value = f(list.value), next = map(list.next, f) }
end
Разбор:
if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.returnзавершает функцию и может вернуть несколько значений через запятую.type Имя = { … }в Luau объявляет именованный тип для статической проверки.- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Генерики поддерживаются как на уровне type, так и на уровне функций. Важно — инференс (выведение типов) в Luau мощный — если параметры функции типизированы, а возвращаемое значение не указано явно, анализатор попытается его вывести. Однако для рекурсивных функций и сложных полиморфных случаев явная аннотация возвращаемого типа обязательна.
Интеграция с Roblox Instance API
Одна из самых значимых особенностей Luau — глубокая интеграция со структурой объектов Roblox. В платформе все сущности (персонажи, части мира, скрипты) представлены как экземпляры класса Instance, иерархия которого задаётся статически (например, Part — BasePart — Instance, Script : LuaSourceContainer : Instance, Player : Instance). Luau знает эту иерархию и позволяет использовать имена классов как типы:
local part: Part = Instance.new("Part")
local player: Player = game.Players:GetPlayers()[1]
Разбор:
- Код выполняется построчно: каждая инструкция использует значения, созданные строками выше.
- При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Более того, при доступе к свойствам и методам через точку или двоеточие IDE (через Language Server) предоставляет точную подсказку:
part.Size = Vector3.new(5, 5, 5) -- ok
part.Health = 100 -- ошибка: Part не имеет свойства Health
Разбор:
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Это достигается за счёт встроенного type definition для всей Roblox API, поддерживаемого командой Roblox и обновляемого с каждой версией движка. Таким образом, типизация охватывает взаимодействие с платформой.
Совместимость с Lua 5.1 и отклонения
Luau сохраняет почти полную обратную совместимость с Lua 5.1 на уровне синтаксиса и семантики выполнения. Можно скопировать любой валидный Lua 5.1-скрипт — он будет работать в Roblox без изменений (если не использует ограниченные API, например, os.execute или io.*).
Однако есть важные оговорки:
- Нет поддержки
gotoи меток. Это сознательное решение, вызванное сложностью интеграции с типовой системой и JIT-компиляцией. - Ограниченная поддержка замыканий в
for-циклах. В Lua 5.1 переменная цикла захватывается по ссылке, что приводит к известной ошибке:
local funcs = {}
for i = 1, 3 do
funcs[i] = function() print(i) end
end
funcs[1]() -- печатает 4
Разбор:
-
Цикл
forповторяет тело: в числовой форме перебирает диапазон, в generic — элементы через итератор. -
Ключевые вызовы в фрагменте:
print(). -
Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. -
При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.В Luau это поведение изменено: переменная цикла захватывается по значению, как в Lua 5.2+. Это не нарушает совместимость на уровне валидности кода, но изменяет семантику.
- Отсутствие
debug.*API, кроме ограниченных функций вродеdebug.traceback. Безопасность платформы требует изоляции скриптов. - Другие запрещённые глобальные —
loadstring,dofile,package.*,os.*,io.*. Доступны толькоprint,warn,assert,error,math.*,string.*,table.*,utf8.*и некоторые другие. - Синтаксические расширения (типы,
type,export type) не являются валидным Lua 5.1. Это означает, что код с аннотациями не будет работать в стандартной Lua-среде без транспиляции. Однако транспилятор Luau (в составе компилятора) автоматически удаляет аннотации перед генерацией байткода — в рантайме их нет.
Производительность
Любая скриптовая платформа, ориентированная на реальное время (real-time interactivity), сталкивается с фундаментальным противоречием: гибкость динамического языка и предсказуемая производительность. Ранние версии Roblox использовали интерпретатор LuaJIT 2.0, но его интеграция оказалась небезопасной и несовместимой с архитектурой sandbox’а. Поэтому Roblox разработал собственный виртуальный процессор (Luau VM) с последующим внедрением JIT-компилятора, оптимизированного под специфику платформы.
Архитектура Luau VM
Luau VM — это стек-машина с 32-битным байткодом, построенная по образцу Lua 5.1 VM, но с рядом изменений:
- расширенный набор инструкций — более 80 против 38 в Lua 5.1, включая специализированные для работы с таблицами и вызовами методов (
GETIMPORT,FASTCALLn); - поддержка "быстрых путей" для частых операций — например, прямой доступ к полям через смещения, если структура таблицы стабильна;
- inline-кэширование вызовов функций и метаметодов.
Байткод генерируется статическим компилятором Luau, который включает в себя лексический, синтаксический анализ и анализ потока типов (type flow analysis). Это позволяет, например, устранять избыточные проверки nil или объединять последовательные операции на этапе компиляции.
JIT-компиляция
JIT в Luau активируется автоматически для "горячих" функций — тех, которые вызываются часто (порог настраивается, по умолчанию ~100 вызовов). Он транслирует байткод в нативный x64-код (на Windows/macOS) или ARM64 (на iOS/Android) с учётом профиля выполнения:
- Инлайнинг: маленькие функции встраиваются непосредственно в вызывающий код.
- Деоптимизация по demand — если во время выполнения встречается значение другого типа (например, ожидался
number, пришёлstring), JIT-код деоптимизируется и возвращается к интерпретируемому режиму для этой ветви — без краха приложения. Это критически важно для gradual typing. - Оптимизация замыканий — переменные, захваченные в замыкании, могут быть размещены в регистрах, а не в куче, если анализ показывает, что их жизненный цикл ограничен.
- Устранение границ проверок (
bounds check elimination) для индексов таблиц при стабильных шаблонах доступа.
Важно: JIT работает только на стороне клиента. Серверные скрипты (в облаке Roblox Cloud) исполняются в режиме интерпретации с агрессивной оптимизацией байткода, но без генерации нативного кода — из соображений детерминизма и воспроизводимости (все серверы должны выдавать одинаковый результат при одинаковом входе).
Это разделение — сознательный дизайн: клиент может жертвовать детерминизмом ради плавности анимации и отклика, тогда как сервер обязан обеспечивать согласованность состояния мира для всех участников.
Архитектура выполнения в Roblox
В отличие от классических приложений, где клиент и сервер — разные процессы с чётким разделением ответственности, в Roblox граница проходит через один и тот же скрипт. Разработчик управляет контекстом выполнения, задаваемым типом скрипта:
| Тип скрипта | Контекст | Описание |
|---|---|---|
Script | Сервер | Выполняется только на сервере. Имеет полный доступ к экземплярам, может изменять состояние мира для всех. |
LocalScript | Клиент | Выполняется только на клиенте конкретного игрока. Имеет доступ к PlayerGui, может читать (но не писать) большинство свойств серверных объектов. |
ModuleScript | Любой | Контейнер для кода, импортируемого (require()) другими скриптами. Может содержать и клиентскую, и серверную логику — в зависимости от того, кто его вызывает. |
Модель данных
Вся виртуальная вселенная Roblox представлена в виде древовидной структуры объектов, наследующих от Instance. Корень — game, псевдоним для DataModel. Наиболее важные ветви:
game.Workspace— физическое пространство мира — части, модели, персонажи. Доступен и клиенту, и серверу.game.Players— коллекция всех игроков. Сервер может изменять состав (например, банить), клиент видит только себя и других (только чтение).game.ReplicatedStorage— область, синхронизируемая от сервера к клиентам. Используется для хранения общих ресурсов — модулей, ассетов, шаблонов персонажей. Изменения здесь реплицируются автоматически.game.ReplicatedFirst— аналогично, но репликация происходит до загрузки игрока (для критически важных ресурсов).game.ServerScriptService,game.ServerStorage,game.StarterPlayer— серверные/клиентские контейнеры, недоступные на противоположной стороне.
Репликация — ключевой механизм синхронизации. Не все изменения транслируются: только свойства, помеченные как Replicated (внутренний флаг API). Например, изменение Part.Position реплицируется, а изменение локальной переменной в скрипте — нет.
Событийная модель
Прямой вызов кода на другой стороне невозможен. Взаимодействие осуществляется через события:
RemoteEvent: односторонняя отправка данных (fire-and-forget).
-- Server
local event = Instance.new("RemoteEvent")
event.Parent = ReplicatedStorage
event.OnServerEvent:Connect(function(player, payload)
print(player.Name .. " sent: " .. payload)
end)
-- Client
local event = ReplicatedStorage:WaitForChild("RemoteEvent")
event:FireServer("Hello from client!")
Разбор:
-
Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). -
События Roblox связывают клиент и сервер: прямого вызова чужого скрипта нет, только обмен сообщениями.
-
Ключевые вызовы в фрагменте:
print(). -
Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. -
При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик. -
RemoteFunction: двухсторонний вызов с возвратом значения (синхронный на клиенте, асинхронный на сервере).
-- Server
local func = Instance.new("RemoteFunction")
func.Parent = ReplicatedStorage
func.OnServerInvoke = function(player, x, y)
return x * y
end
-- Client
local result = func:InvokeServer(6, 7) -- result = 42
Разбор:
returnзавершает функцию и может вернуть несколько значений через запятую.- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Важно: RemoteFunction:InvokeServer() блокирует клиентский поток до ответа. Злоупотребление приводит к "подвисанию" клиента. Рекомендуется использовать RemoteEvent + коллбэки для асинхронных сценариев.
Пример — визуальный эффект при прыжке игрока
Рассмотрим реализацию эффекта частиц, появляющегося под ногами игрока в момент прыжка. Это требует:
- обнаружения события прыжка (клиент);
- проверки валидности (сервер);
- создания частиц (клиент, но по команде сервера или с его разрешения).
Клиентская часть (LocalScript в StarterPlayerScripts)
Код ITЗагрузка примера кода…
Разбор:
if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. - События Roblox связывают клиент и сервер: прямого вызова чужого скрипта нет, только обмен сообщениями.
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Серверная часть (Script в ServerScriptService)
Код ITЗагрузка примера кода…
Разбор:
- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. returnзавершает функцию и может вернуть несколько значений через запятую.- События Roblox связывают клиент и сервер: прямого вызова чужого скрипта нет, только обмен сообщениями.
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Архитектурные замечания
- Безопасность: клиент не создаёт эффект напрямую — только по разрешению сервера. Это предотвращает читерство (например, спам частицами для маскировки).
- Эффективность: шаблон
JumpEffect—ParticleEmitter, загруженный один раз вReplicatedStorage. Его клонирование дешевле, чем создание черезInstance.new("ParticleEmitter"). - Изоляция — каждый клиент создаёт эффект локально — другие игроки не получают данные о частицах (если не требуется синхронизация, например, для взрывов), что экономит трафик.
Инструментарий разработки
Отладка в среде
Roblox Studio предоставляет:
- Output — лог
print(),warn(),error(). - Developer Console (Ctrl+F9) — во время игры позволяет:
- просматривать стек вызовов;
- устанавливать точки останова (breakpoints) в скриптах;
- инспектировать переменные в текущем фрейме;
- выполнять произвольный код в контексте (REPL-подобный режим).
Однако print() остаётся основным инструментом из-за простоты. Для структурированного логгирования рекомендуется обёртка:
local Logger = {}
Logger.Level = "INFO" -- DEBUG, INFO, WARN, ERROR
function Logger.debug(msg: string)
if Logger.Level == "DEBUG" then print("[DEBUG] " .. msg) end
end
Разбор:
- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Ключевые вызовы в фрагменте:
print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Luau LSP и статический анализ
В 2022 году Roblox выпустил Luau Language Server — реализацию Language Server Protocol (LSP), интегрируемую в VS Code, Sublime Text, Neovim и др. Он предоставляет:
- автодополнение с учётом типов и Roblox API;
- переход к определению (
Go to Definition);
Go to Definition
Разбор:
-
Команда или конфигурация окружения: выполняется вне интерпретатора Lua.
-
В фрагменте 1 строк(и); читайте сверху вниз как последовательность шагов.
-
поиск всех ссылок (
Find All References);
Find All References
Разбор:
-
Команда или конфигурация окружения: выполняется вне интерпретатора Lua.
-
В фрагменте 1 строк(и); читайте сверху вниз как последовательность шагов.
-
переименование (
Rename Symbol); -
inline-проверку типов в реальном времени;
-
предупреждения о неиспользуемых переменных, неявных
any, потенциальныхnil-разыменованиях.
Анализатор также поддерживает strict mode (включается через --!strict в начале файла), который:
- запрещает необъявленные глобальные переменные;
- требует аннотации для
function,local, параметров; - интерпретирует
nilкак отдельный тип (без автоматического объединения).
Это приближает Luau к "промышленному" уровню TypeScript.
Практика на Roblox — учебный маршрут
Полноценный проект с DataStore, этапами и магазином — в практикуме "обби". Studio и первый Place — Roblox Studio — первая игра и настройки Place. В примерах ниже используйте task.spawn / task.wait вместо устаревших spawn / wait.
| Устаревшее | Замена в Luau |
|---|---|
spawn(function() … end) | task.spawn(function() … end) |
wait(n) | task.wait(n) |
delay(n, fn) | task.delay(n, fn) |
Практика — создание первой игры — "Сборщик монет"
Краткий мини-проект для закрепления RemoteEvent и GUI. Для полного учебного цикла (чекпоинты, DataStore, магазин) откройте практикум "обби".
Для закрепления теории рассмотрим пошаговое создание мини-игры, демонстрирующей ключевые концепции:
- Цель — игрок управляет персонажем, собирает монеты (
PartсTouchInterest), счёт отображается на экране. - Архитектура:
- сервер — спавн монет, хранение счёта, валидация сбора;
- клиент: отображение GUI, анимация сбора;
- модуль: общая логика (например,
CoinSpawner).
Создание монеты
- В
ReplicatedStorageсоздаёмPart→CoinTemplate:BrickColor = BrickColor.new("Bright yellow")Shape = Enum.PartType.Ball,Size = Vector3.new(1,1,1)- Добавляем
ClickDetectorили используемTouched(осторожно:Touchedсрабатывает при любом контакте, включая стены).
Серверный скрипт спавна
Код ITЗагрузка примера кода…
Разбор:
- Одинарные и двойные кавычки эквивалентны: выбирайте тот вид, где меньше экранирования внутри текста.
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект.- Цикл
forповторяет тело: в числовой форме перебирает диапазон, в generic — элементы через итератор. if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. returnзавершает функцию и может вернуть несколько значений через запятую.- Ключевые вызовы в фрагменте:
os.clock(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Клиентский GUI
- В
StarterGuiсоздаёмScreenGui→TextLabel(CoinCounter). LocalScriptвStarterPlayerScripts:
-- LocalScript/CoinUI.luau
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local coinCounter = player:WaitForChild("PlayerGui"):WaitForChild("CoinCounter")
game.ReplicatedStorage.CoinCollected.OnClientEvent:Connect(function(count: number)
coinCounter.Text = "Монет: " .. tostring(count)
end)
Разбор:
- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). - Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Ключевые вызовы в фрагменте:
tostring(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Обучение через ограничения
Эта простая игра уже включает:
- работу с иерархией
Instance; - событийную модель (
Touched); - клиент-серверное взаимодействие;
- валидацию входных данных;
- репликацию состояния через
RemoteEvent; - работу с GUI.
Дальнейшее развитие — добавить таймер, уровни, сохранение прогресса через DataStoreService, звуки, анимации.
Продвинутая типизация
Механизм inference и его границы
Luau использует flow-sensitive type inference (чувствительный к потоку управления вывод типов), что означает: тип переменной может меняться в зависимости от ветвления.
local x = math.random() > 0.5 and "hello" or 123
-- x: string | number
if typeof(x) == "string" then
print(x:upper()) -- ok: в этой ветке x: string
else
print(x + 1) -- ok: x: number
end
Разбор:
if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. - Вызовы из стандартных библиотек
math— готовые функции языка, не нужно писать их с нуля. - Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Ключевые вызовы в фрагменте:
math.random(),print(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Однако inference ограничен:
- Нет полиморфного рекурсивного вывода. Для рекурсивных функций аннотация возвращаемого типа обязательна:
-- ОШИБКА: Cannot infer type for recursive function
function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1)
end
-- Правильно:
function factorial(n: number): number
if n <= 1 then return 1 end
return n * factorial(n - 1)
end
Разбор:
-
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект. -
Цикл
forповторяет тело: в числовой форме перебирает диапазон, в generic — элементы через итератор. -
if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse. -
returnзавершает функцию и может вернуть несколько значений через запятую. -
Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
-
type Имя = { … }в Luau объявляет именованный тип для статической проверки. -
Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. -
При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик. -
Циклы не сужают типы. В отличие от TypeScript, Luau не отслеживает изменения в циклах:
local items: {string | number} = {"a", 1, "b", 2}
for _, v in items do
if typeof(v) == "string" then
-- v здесь всё ещё string | number
-- приходится использовать утверждение:
local s = v :: string
print(s:upper())
end
end
Разбор:
-
Цикл
forповторяет тело: в числовой форме перебирает диапазон, в generic — элементы через итератор. -
if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse. -
Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
-
Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. -
Ключевые вызовы в фрагменте:
print(). -
Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. -
При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик. -
Утверждения типов (
::) — последнее средство. Они отключают проверку, но не генерируют проверок в рантайме:
local x: any = "text"
local _ = (x :: number) + 1 -- анализатор может не поймать ошибку после `::`
-- Предпочитайте `typeof(x) == "string"` вместо слепого cast
Разбор:
- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Generics
Luau поддерживает generics с 2023 года, но с оговорками. Синтаксис:
type Box<T> = { value: T }
function createBox<T>(value: T): Box<T>
return { value = value }
end
local strBox = createBox("hello") -- Box<string>
local numBox = createBox(42) -- Box<number>
Разбор:
returnзавершает функцию и может вернуть несколько значений через запятую.type Имя = { … }в Luau объявляет именованный тип для статической проверки.- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Ограничения
- Нет bounded generics (ограничений типов). Нельзя написать:
-- НЕ РАБОТАЕТ
function add<T: number | string>(a: T, b: T): T
Разбор:
-
Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
-
При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.Вместо этого используют union-типы в параметрах:
function add(a: number | string, b: number | string): number | string
if typeof(a) == "number" and typeof(b) == "number" then
return a + b
elseif typeof(a) == "string" and typeof(b) == "string" then
return a .. b
else
error("Invalid types")
end
end
Разбор:
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект.- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. returnзавершает функцию и может вернуть несколько значений через запятую.- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Ключевые вызовы в фрагменте:
error(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
-
Нет generic-модулей верхнего уровня.
typeс generics должны быть определены внутри scope (функция, таблица). Глобальные generic-типы ограничены по сложности. -
Инференс generics в вызовах — частичный. Если хотя бы один параметр аннотирован, тип выводится:
function map<T, U>(list: {T}, f: (T) -> U): {U} ... end
local strings = map({1,2,3}, function(x) return tostring(x) end) -- {string}
Разбор:
- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). returnзавершает функцию и может вернуть несколько значений через запятую.- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
tostring(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
- Нет variance-модификации (covariance/contravariance). Luau рассматривает
Box<Part>иBox<Instance>как несовместимые типы, даже при наследованииPart : Instance. Это безопасно, но требует ручного кастинга:
local partBox: Box<Part> = createBox(Instance.new("Part"))
local instBox: Box<Instance> = partBox :: any -- прямое присваивание невозможно
Разбор:
- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Эти ограничения — компромисс между выразительностью и производительностью анализа. В крупных проектах это компенсируется использованием фабрик и интерфейсов.
Паттерны проектирования в экосистеме Luau
Roblox-разработка породила собственные идиомы, адаптированные под ограничения платформы — отсутствие true-множественного наследования, необходимость изоляции клиент/сервер, и высокая стоимость создания Instance.
Signal
Встроенная система событий Roblox (BindableEvent, RemoteEvent) неудобна для внутренней логики — они — Instance, их создание дорого, и они не поддерживают типизацию параметров.
Поэтому повсеместно используется паттерн Signal — pure-Lua реализация события:
Код ITЗагрузка примера кода…
Разбор:
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект.- Синтаксис
obj:method()передаётobjпервым аргументом (self) — удобный стиль для методов таблиц. - Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). - Цикл
forповторяет тело: в числовой форме перебирает диапазон, в generic — элементы через итератор. if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.returnзавершает функцию и может вернуть несколько значений через запятую.setmetatableсвязывает таблицу с метатаблицей; поля__…задают перехват операций (индекс, арифметика, вызов).- Вызовы из стандартных библиотек
table— готовые функции языка, не нужно писать их с нуля. type Имя = { … }в Luau объявляет именованный тип для статической проверки.
Преимущества:
- 100% Lua, нет
Instance; - полная типизация параметров и возвращаемых значений;
- поддержка variadic generics;
- совместимость с Luau LSP (автодополнение в
Connectработает корректно).
Использование:
local Signal = require(script.Parent.Signal)
local playerDied = Signal.new<Player, number>() -- Player, время смерти (сек)
playerDied:Connect(function(player, time)
print(player.Name .. " died at " .. time)
end)
playerDied:Fire(somePlayer, os.clock())
Разбор:
- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). require("имя")загружает модуль один раз и возвращает его публичную таблицу из кэшаpackage.loaded.- Ключевые вызовы в фрагменте:
os.clock(),print(),require(). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Service Locator и модульная архитектура
Крупные проекты (>100 скриптов) структурируются по принципу сервисов — одиночных (Singleton) модулей, управляемых глобальным локатором.
Структура:
src/
├── Services/
│ ├── PlayerService.luau
│ ├── EconomyService.luau
│ └── ...
├── Shared/
│ └── Types.luau
└── init.luau -- точка входа
Разбор:
- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). - Комментарии после
--поясняют ожидаемый результат; при запуске интерпретатор их игнорирует. - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
init.luau (в ServerScriptService и StarterPlayerScripts):
Код ITЗагрузка примера кода…
Разбор:
if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. returnзавершает функцию и может вернуть несколько значений через запятую.require("имя")загружает модуль один раз и возвращает его публичную таблицу из кэшаpackage.loaded.- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Ключевые вызовы в фрагменте:
rawget(),require(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
PlayerService.luau:
Код ITЗагрузка примера кода…
Разбор:
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект.- Синтаксис
obj:method()передаётobjпервым аргументом (self) — удобный стиль для методов таблиц. if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. returnзавершает функцию и может вернуть несколько значений через запятую.setmetatableсвязывает таблицу с метатаблицей; поля__…задают перехват операций (индекс, арифметика, вызов).- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
type Имя = { … }в Luau объявляет именованный тип для статической проверки.- События Roblox связывают клиент и сервер: прямого вызова чужого скрипта нет, только обмен сообщениями.
Преимущества:
- Чёткое разделение ответственности;
- Легко mock’ать в тестах;
- Возможность lazy-загрузки.
Promise-подобные конструкции (без async/await)
Luau не имеет async/await, но поддерживает корутины через task.spawn, task.delay, task.wait. Для композиции асинхронных операций используется паттерн Promise, реализованный в стиле Lua:
Код ITЗагрузка примера кода…
Разбор:
functionобъявляет функцию;endзакрывает тело. Имя послеfunctionстановится ссылкой на вызываемый объект.- Оператор
..склеивает строки (и автоматически приводит числа к строке при конкатенации). - Цикл
forповторяет тело: в числовой форме перебирает диапазон, в generic — элементы через итератор. if … then … endвыбирает ветку по truthiness: ложными считаются толькоnilиfalse.- Логика записывается словами
and/or/not, а не символами&&/||/!как в C-подобных языках. returnзавершает функцию и может вернуть несколько значений через запятую.setmetatableсвязывает таблицу с метатаблицей; поля__…задают перехват операций (индекс, арифметика, вызов).- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
type Имя = { … }в Luau объявляет именованный тип для статической проверки.
Важно: такие реализации не эквивалентны JS-Promise по семантике (не имеют микрозадач), но решают задачу управления асинхронным потоком.
Интеграция с внешними инструментами
Roblox Studio — удобная среда, но непригодна для командной работы, CI/CD и тестирования. Поэтому развиты инструменты для внешней разработки.
Rojo
Rojo — open-source утилита, которая:
- монтирует иерархию
InstanceRoblox как файловую систему; - позволяет писать скрипты в
.luau-файлах, а не в окне Studio; - поддерживает сборку проектов (
default.project.json); - интегрируется с Git, CI, редакторами.
Пример default.project.json:
Код ITЗагрузка примера кода…
Разбор:
- Конфигурационный файл: описывает структуру проекта, а не исполняемый Lua-код.
- В фрагменте 18 строк(и); читайте сверху вниз как последовательность шагов.
Rojo запускается в watch-режиме: любое изменение в src/ мгновенно отражается в запущенном Roblox Studio.
Selene
Selene — официальный линтер от Roblox, заменяющий устаревший StyLua.
Конфигурация (selene.toml):
std = "roblox+testez"
[config]
unused_variable = "allow" # в учебных проектах — allow, в продакшене — "deny"
global_usage = "allow" # для _G, game и т.д.
[rules]
roblox_unsafe_call = "error" # запрет os.time(), warn()
unused_variable = "warn"
Разбор:
- Ключевые вызовы в фрагменте:
os.time(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Selene понимает:
- Roblox API;
- аннотации Luau;
- особенности
taskиcoroutine; - common pitfalls (например, захват переменной цикла).
TestEZ и запуск тестов
- TestEZ — BDD-фреймворк (аналог Jasmine) для unit-тестов Luau в Roblox.
- run-in-roblox / lemur / CI-плагины — запуск тестов вне Studio (зависит от настройки проекта).
- Tarmac — отдельный инструмент для ассетов (текстуры, звуки), не для unit-тестов; не путать с TestEZ.
Пример теста:
Код ITЗагрузка примера кода…
Разбор:
require("имя")загружает модуль один раз и возвращает его публичную таблицу из кэшаpackage.loaded.- Двоеточие после имени — аннотация типа Luau; на выполнение в рантайме не влияет, помогает анализатору и IDE.
- Фигурные скобки
{}создают таблицу: можно задать поляkey = valueи/или элементы-массив с индексами с 1. - Ключевые вызовы в фрагменте:
require(). - При переносе в свой проект сохраните порядок шагов и проверьте результат через
printили отладчик.
Запуск через CLI (типичная связка Rojo + TestEZ; точная команда зависит от runner в проекте):
rojo build default.project.json -o build.rbxlx
# далее — test runner вашей команды (run-in-roblox, foreman и т.д.)
Разбор:
- Команда или конфигурация окружения: выполняется вне интерпретатора Lua.
- В фрагменте 2 строк(и); читайте сверху вниз как последовательность шагов.
CI/CD
Типичный pipeline в GitHub Actions:
Код ITЗагрузка примера кода…
Разбор:
- Команда или конфигурация окружения: выполняется вне интерпретатора Lua.
- В фрагменте 15 строк(и); читайте сверху вниз как последовательность шагов.
Это позволяет:
- гарантировать отсутствие regression’ов;
- форсировать стандарты кода;
- автоматически деплоить в Roblox после успешного merge в
main.