25 KiB
План: Полная поддержка Lua-скриптов в Рублоксе
Цель: Пользователь Рублокс-студии создаёт скрипт → выбирает язык JS или Lua → пишет код → в плеере оба языка работают параллельно. Lua-скрипты совместимы с Roblox API настолько, чтобы код из Roblox-игр работал без модификаций.
Зачем: В Roblox экосистеме сотни тысяч разработчиков, привыкших к Lua + Roblox API (Vector3, CFrame, Instance, RemoteEvent, etc.). Сейчас они не могут перенести свои игры. С этой фичей — могут.
Срок: ~6 недель полного времени. Можно делать поэтапно (после каждого этапа есть полезный результат).
Юр.риск: "юр риски беру на себя" (МИН, 2026-06-08).
Архитектурное решение
Текущая ситуация
- Скрипты хранятся как
{id, code, target, name}в БД GameRuntimeзапускает каждый JS-скрипт в Web WorkerScriptSandboxWorker.js- API игре доступен через
game.*объект (события, scene, player, gui, save, etc.) - Скриптов в игре могут быть сотни, каждый — отдельный Worker
Целевая
- Скрипты получают поле
language: 'js' | 'lua'(default'js') - В редакторе — переключатель в
ScriptEditor.jsxв шапке: JS / Lua - Monaco-editor подсвечивает Lua (есть встроенный язык в
monaco-editor) - В runtime: JS-скрипты идут через старый sandbox, Lua — через новый
LuaSandbox.js - Lua-runtime построен поверх wasmoon (Lua 5.4 в WebAssembly)
- Lua-скрипты делятся на один shared VM на игру (а не Web Worker на скрипт) — иначе OOM при 100+ скриптах
- Lua-API проксирует Roblox API → нашу game.* плюс полную DataModel иерархию
Ключевая идея
Lua-скрипты не вызывают game.move(...). Они вызывают script.Parent.Position = Vector3.new(1,2,3) — как в Roblox. Lua-shim под капотом переводит это в partSet команды для движка Рублокса. Юзер пишет идиоматичный Roblox-Lua, движок Рублокса исполняет.
Этап 1: UI и хранение языка (3 дня)
1.1 Миграция БД
- Добавить поле
language VARCHAR(8) DEFAULT 'js' NOT NULLв таблицуscripts(есть на storys API) - API endpoints (
POST/PATCH /scripts/...) принимают и сохраняютlanguage - Старые скрипты без поля →
'js'по умолчанию
1.2 ScriptEditor.jsx
- Переключатель в шапке редактора:
[ JS | Lua ](segmented control) - При смене языка — confirm("Сменить язык? Текущий код будет очищен"), затем код сбрасывается на шаблон-заглушку нового языка
- Monaco language switch:
javascript↔lua - Подсветка/автодополнение Lua встроены в Monaco (
monaco-editor/esm/vs/basic-languages/lua) - Линтер ошибок Lua через luaparse (npm пакет, ~50KB, parse-only) — показываем красные подчёркивания
- Шаблон Lua для нового скрипта на target=Part:
-- Скрипт на части. script.Parent = эта часть. local part = script.Parent part.Touched:Connect(function(hit) print("Касание:", hit.Name) end) - Шаблон Lua для глобального скрипта (target=nil):
local Players = game:GetService("Players") Players.PlayerAdded:Connect(function(player) print("Игрок зашёл:", player.Name) end)
1.3 Иконка языка в HierarchyPanel
- Рядом с именем скрипта — маленький бейдж
JSилиLua(синий / голубой) - Помогает не путаться при сотне скриптов
Что готово в конце Этапа 1
- Юзер может создать Lua-скрипт, написать код, сохранить
- Код не исполняется — пока только хранится и редактируется
- В плеере Lua-скрипты молча игнорируются
Этап 2: Базовый Lua-runtime (5 дней)
2.1 LuaSharedSandbox.js — новый sandbox-класс
Полная архитектура шаринг-VM (один wasmoon-state на все Lua-скрипты игры):
GameRuntime
├── ScriptSandbox (JS-скрипт, Web Worker, как сейчас)
├── ScriptSandbox (JS-скрипт)
├── LuaSharedSandbox ← НОВЫЙ
│ ├── LuaSharedWorker.js
│ │ └── wasmoon VM (один на всю игру)
│ │ ├── Roblox API shim
│ │ ├── DataModel tree (game.Workspace, Players, ...)
│ │ └── все Lua-скрипты как сопрограммы (coroutines)
│ └── проксирует partSet/sceneCreate/event обратно в main thread
Файлы:
src/editor/engine/LuaSharedSandbox.js(main thread): API совместимый с ScriptSandbox (sendSceneSnapshot,sendGlobalEvent, etc.)src/editor/engine/LuaSharedWorker.js(Web Worker): держит wasmoon, исполняет скрипты, шлёт командные сообщенияsrc/editor/engine/RobloxLuaShim.js(worker side): объявление всех Roblox-классов и сервисов
2.2 Минимальный Roblox shim в первой итерации
Vector3.new(x,y,z),+,-,*,:Magnitude(),:Dot(),:Cross(),:Lerp(),:Normalize()Color3.new(r,g,b),Color3.fromRGB(r,g,b),:Lerp()CFrame.new(x,y,z),CFrame.lookAt(),CFrame.fromEulerAnglesXYZ(), операторы*и:Inverse(),:ToWorldSpace()UDim2.new(sx,ox,sy,oy),UDim.new(s,o)Enum.KeyCode.W,Enum.UserInputType.MouseButton1, etc. (через generated table)print()→ console + onLog event в студииwait(secs)/task.wait(secs)— через coroutine + scheduler в main looptick(),os.time(),os.clock(),math.*,string.*,table.*(стандартные Lua)pcall,xpcall,error,assert(стандартные)
2.3 GameRuntime интеграция
- При старте игры — пробежать по
scripts[], разделить наjsScriptsиluaScripts - Для JS — старый путь (по сэндбоксу на скрипт)
- Для Lua — один
LuaSharedSandbox, в негоaddScript(luaSource)каждым - LuaSharedSandbox шлёт назад те же команды что JS sandbox:
partSet,sceneCreate,chatSay,guiSet, etc.
Что готово в конце Этапа 2
- Lua-скрипты исполняются
- Можно использовать
Vector3,Color3,print(),wait(),math/string/table - Скрипт ещё не видит Workspace, Player, GUI
Этап 3: DataModel — game.Workspace и иерархия (5 дней)
3.1 Что такое DataModel
В Roblox любая игра — дерево объектов. Корень = game. У него детки = сервисы: Workspace, Players, ReplicatedStorage, Lighting, StarterGui, RunService, etc. У каждого деток — свои детки.
У нас сейчас сцена плоская: primitives, blocks, models. Нужно виртуальное дерево DataModel поверх плоской сцены.
3.2 Виртуальное дерево
Файл: src/editor/engine/datamodel/DataModelTree.js
При старте Lua-runtime, для текущей сцены строится виртуальное дерево:
game (RbxGame)
├── Workspace (RbxWorkspace)
│ ├── Part_0 ← обёртка над primitive id=0
│ ├── Part_1 ← обёртка над primitive id=1
│ ├── Model_5 ← обёртка над model id=5 (с Children)
│ │ └── Part_inner
│ ├── Camera
│ └── Terrain
├── Players (RbxPlayers)
│ └── LocalPlayer (RbxPlayer)
│ ├── Character (RbxCharacter)
│ │ ├── Humanoid (RbxHumanoid)
│ │ └── HumanoidRootPart (RbxPart)
│ └── PlayerGui (RbxScreenGui-контейнер)
│ └── (Lua-скрипты могут спавнить GUI через Instance.new)
├── ReplicatedStorage (RbxFolder)
├── ServerStorage (RbxFolder)
├── Lighting (RbxLighting)
├── StarterGui (RbxFolder)
├── StarterPlayer (RbxFolder)
│ └── StarterCharacter (RbxFolder)
├── RunService (RbxRunService с :Heartbeat, :Stepped, :RenderStepped)
├── UserInputService (события InputBegan/Ended/Changed)
├── TweenService (:Create, :GetService для tweens)
├── HttpService (заглушка либо проксируем через нашего бэка)
├── DataStoreService (проксируем через game.save)
└── MarketplaceService (заглушка)
3.3 Instance-классы
src/editor/engine/datamodel/Instance.js:
class RbxInstance {
Name = "Instance";
ClassName = "Instance";
Parent = null;
Children = [];
// Свойства которые юзер может ставить через __newindex (metatable)
// отслеживаются — при изменении посылается команда в main thread
// для синхронизации с Babylon-сценой
GetChildren() { return [...this.Children]; }
FindFirstChild(name, recursive) { ... }
WaitForChild(name, timeout) { ... } // через coroutine + yield
FindFirstAncestor(name) { ... }
FindFirstChildOfClass(class) { ... }
Destroy() { ... }
Clone() { ... }
IsA(class) { ... }
GetFullName() { ... }
GetAttribute(name) / SetAttribute(name, value)
GetPropertyChangedSignal(name) → RbxSignal
}
class RbxPart extends RbxInstance {
Position // setter: партиклс в Babylon (primitiveManager.setPosition)
Size // setter
Color // setter
Material // setter (mapping Roblox materials → наши)
Anchored // setter
CanCollide // setter
Touched // RbxSignal — Fire когда BabylonScene детектит overlap
TouchEnded // RbxSignal
CFrame // computed property — Position + rotation
}
class RbxModel extends RbxInstance {
PrimaryPart
GetPrimaryPartCFrame() / SetPrimaryPartCFrame()
PivotTo(cframe) // MoveTo + Rotate
GetBoundingBox()
}
class RbxHumanoid extends RbxInstance {
Health = 100
MaxHealth = 100
WalkSpeed = 16
JumpPower = 50
Died, HealthChanged, Touched, StateChanged — signals
TakeDamage(amount)
MoveTo(pos) // simulates Roblox NPC pathing
LoadAnimation(anim) → RbxAnimationTrack
}
class RbxScript extends RbxInstance {
Source // источник Lua (read-only обычно)
Disabled // bool
RunContext // Server / Client / Legacy
}
class RbxRemoteEvent extends RbxInstance {
OnServerEvent : RbxSignal
OnClientEvent : RbxSignal
FireServer(...args)
FireClient(player, ...args)
FireAllClients(...args)
}
3.4 Метатаблицы Lua
Каждая JS-обёртка RbxPart экспортируется в Lua как table с metatable:
__index— чтение свойства, либо метод__newindex— запись свойства, триггерит side-effects (синхронизация сцены)__tostring— дляprint(part)показывает"Part_0"
Это критично для совместимости с Roblox-скриптами.
3.5 script.Parent для каждого Lua-скрипта
- Если скрипт привязан к
target=42(primitive id 42) —script.Parent = workspace:FindFirstChild по primId(42) - Если глобальный —
script.Parent = nil - В скриптовом контексте:
scriptэто таблица{Name=..., Parent=..., ClassName="Script"}
Что готово в конце Этапа 3
- Lua-скрипт может пройтись по
game.Workspace:GetChildren() script.Parent.Touched:Connect(...)работает (KillBrick = реально)local player = game.Players.LocalPlayer; player.Character.Humanoid.Health = 0убивает игрокаInstance.new("Part", workspace)создаёт примитив
Этап 4: Полный Roblox API (10 дней)
Закрываем "длинный хвост" API. Каждый день — 1-2 сервиса.
4.1 Services
- RunService:
.Heartbeat,.Stepped,.RenderStepped— RbxSignals, фаер в main loop tick - UserInputService:
.InputBegan,.InputChanged,.InputEnded— события KeyCode/MouseButton/Touch - TweenService:
:Create(instance, TweenInfo, propertyTable)→ возвращает RbxTween;Tween:Play()/Pause()/Cancel(); интерполируется в main loop - DataStoreService: проксируем через наш
game.save(sync version):GetDataStore(name)→ объект с:GetAsync(key),:SetAsync(key, value),:UpdateAsync(key, fn)- Async-методы через coroutine.yield + наш save API
- MarketplaceService: заглушки
PromptPurchase,GetProductInfo(бизнес-логика через наш интерфейс) - HttpService:
:JSONEncode,:JSONDecode,:GenerateGUID— простая встроенная реализация;:GetAsync/PostAsyncпроксируем через ограниченный список разрешённых доменов - Players:
LocalPlayer,:GetPlayers(),PlayerAdded/PlayerRemovingсигналы - Lighting: read-only сейчас (через
Lighting.Ambient,Lighting.OutdoorAmbientставить значения нашему envManager) - Workspace:
:Raycast(origin, direction, params)→ реальный raycast через PhysicsAABB;:GetServerTimeNow();CurrentCamera
4.2 GUI (важно!)
Instance.new("ScreenGui")→ еслиParent = playerGui, регистрируется в нашем GuiManagerTextLabel,TextButton,ImageLabel,ImageButton,Frame— все мапятся на наш GuiOverlayMouseButton1Click,MouseEnter,MouseLeave,Activated— сигналыUDim2,Vector2для позиций/размеров- При установке
gui.Position = UDim2.new(0.5, 0, 0.5, 0)— пересылается в GuiManager и обновляется DOM
4.3 Sound
Instance.new("Sound", part)сSoundId = "rbxassetid://12345"или с нашим URL:Play(),:Stop(),:Pause(),Volume,Pitch,Looped- Под капотом — наш SoundManager
4.4 Animation
Instance.new("Animation")сAnimationId(наши собственные ID анимаций R15)humanoid:LoadAnimation(anim) → AnimationTrack:Play(),:Stop(),:AdjustSpeed(),:GetMarkerReachedSignal()- Связь с нашим R15Animator
4.5 Tools / Backpack
ToolInstance:Activated,Equipped,Unequippedсигналыplayer.Backpack:GetChildren()— Lua видит инвентарь- Реализация через наш HotbarManager + InventoryService
4.6 ProximityPrompt, ClickDetector
- ProximityPrompt:
Triggeredсигнал,:Show/:Hide, ActionText, ObjectText - ClickDetector:
MouseClick,MouseHoverEnter/Leaveсигналы
4.7 Networking-эмуляция (single-player)
- RemoteEvent / RemoteFunction работают локально (поскольку у нас singleplayer на client-only)
FireServer/InvokeServerзапускают handlers в том же VM, но в other-side контексте- Это позволяет копировать многопользовательские скрипты Roblox без изменений (хоть мультиплеера и нет)
- Когда у Рублокса появится мультиплеер —
RemoteEventуже будет работать «по-настоящему» без изменений в скриптах юзера
Что готово в конце Этапа 4
- ~90% типовых Roblox-скриптов работают без модификаций
- DataStore сохраняет прогресс
- TweenService плавно двигает объекты
- GUI создаётся скриптом
- Анимации играются
Этап 5: Импорт .rbxl + конвертер юзер-кода (5 дней)
5.1 Изменение импортера
Сейчас импортер сохраняет Lua-исходник в JS-комментарии и пытается завернуть в JS-обёртку. Это устаревает.
Новый путь:
- Импортер сохраняет Lua-source как есть (без обёрток)
- В записи скрипта
language = 'lua' - target = primitiveId или null
- В GameRuntime Lua-скрипт идёт сразу в LuaSharedSandbox
5.2 Конвертация ассетов
- Roblox MeshId/TextureId через наш ImageProxy → ассеты сохраняются в minio + на CDN
rbxassetid://12345→ resolve в наш asset_id- Сохранение в БД ссылок на ассеты
5.3 Поведение при импорте
- При импорте .rbxl карты — все Lua-скрипты сохраняются как Lua-скрипты Рублокса (не пытаемся конвертить в JS)
- Юзер открывает игру → редактирует → видит в Hierarchy
Script (Lua)рядом сScript (JS)— может писать на любом
Что готово в конце Этапа 5
- Импорт Roblox-карты работает бесшовно
- Юзер может править Lua-код в редакторе и видеть результат
Этап 6: Производительность и стабильность (5 дней)
6.1 Profiling
- Замерить FPS на картах с 100/500/1000 Lua-скриптов
- Если падает — переезд на fengari (pure-JS Lua interp, в 5-10× медленнее wasmoon но без WASM overhead на старте) либо на собственный Lua-bytecode runtime для горячих скриптов
6.2 Memory
- Каждый wasmoon VM ~10-15MB. Один на игру = ОК. Если придётся разделять на части (server/client), нужно бенчить.
6.3 Песочница (security)
- Lua не должен дёргать
io.*,os.execute,loadstring(внешний код), etc. - Whitelist стандартной библиотеки. Запрещаем всё что может выйти из браузера.
6.4 Ошибки
- Lua-ошибки в скрипте — показываются в Output-панели студии (как JS-ошибки)
- Stack trace с правильными номерами строк (не из обёртки)
- Если скрипт зациклился — kill через
debug.sethookпосле N инструкций без yield
6.5 Тесты
- 50 unit-тестов на Roblox API (Vector3 операции, CFrame, Instance.new, Touched, RunService, TweenService)
- 10 интеграционных: импортировать тест-rbxl, запустить, проверить что нужное случилось
- CI: тесты прогоняются в Gitea Actions при PR
Что готово в конце Этапа 6
- Lua-runtime production-ready
- Можно объявить публично «теперь в Рублоксе пишут на Lua с Roblox-совместимостью»
Этап 7: Документация (3 дня)
7.1 Раздел вики
wiki/lua-intro— введение в Lua для Рублокса (для пользователей, которые приходят с Roblox — короткое)wiki/lua-vs-js— таблица: «то же самое на JS и на Lua» для типичных задачwiki/roblox-api-supported— список того что работает / не работает / отличаетсяwiki/lua-examples— 20 готовых сниппетов (KillBrick, TeleportPad, Checkpoint, Coin, NPCFollower, etc.)
7.2 Migration guide
- «Как перенести свою Roblox-игру в Рублокс» — пошаговое
- Список известных несовместимостей и их обходов
7.3 PR-материал
- Пост в /developer на team.rublox.pro: «Lua-поддержка теперь GA»
- Если есть бюджет — короткий ролик на YouTube/TikTok
Этапы целиком
| Этап | Длительность | Содержание |
|---|---|---|
| 1 | 3 дня | UI и хранение языка |
| 2 | 5 дней | Базовый Lua-runtime + минимальный shim |
| 3 | 5 дней | DataModel (game.Workspace и иерархия) |
| 4 | 10 дней | Полный Roblox API (services, GUI, Sound, Animation) |
| 5 | 5 дней | Импорт .rbxl и асетов |
| 6 | 5 дней | Производительность, безопасность, тесты |
| 7 | 3 дня | Документация и публикация |
| Итого | ~36 рабочих дней | ~6 недель полного времени |
Что-то можно делать раньше, чтобы получить пользу
- MVP (Этапы 1+2): через 8 дней — юзер может писать Lua-скрипты с минимальным API (Vector3, Color3, print, wait). Уже видит что фича есть.
- Beta (Этапы 1-3): через 13 дней — KillBrick'и работают, можно делать простые игры на Lua.
- GA (все этапы): через 6 недель — продакшен.
После каждого этапа можно делать релиз и собирать фидбек.
Решения которые нужны от тебя перед стартом
- wasmoon vs fengari — wasmoon быстрее но WASM-heavy, fengari проще но медленнее. Предлагаю wasmoon (уже используем для импорта).
- Один shared VM на игру — согласен или разделять server/client? Предлагаю один в singleplayer-фазе, разделение — позже когда будет мультиплеер.
- Бэкенд изменения — нужна миграция БД (поле
language). У нас сейчас S2 + S1 + auto-backup, ничего страшного, но согласовать момент апдейта. - Roblox API trademark/copyright — мы делаем API-compatible runtime. Названия классов
Workspace,Humanoid, etc. это API names. Юр.риск есть. Ты сказал берёшь — фиксируем. - Приоритет — этот план делать вместо других задач (тогда параллельные фичи стопаются) или после текущего бэклога?
Связанные документы
RUBLOX_PROJECT.md— общий план РублоксаRUBLOX_EDITOR_ROADMAP.md— куда движется редакторINFO_PROCESS.md— лог реализации (будет апдейтиться по ходу)
Создано: 2026-06-08, Claude (Opus 4.7) совместно с МИНом. Статус: план готов, ждём решения по 5 вопросам перед стартом.