Roblox — популярная игра и платформа для создания игр, и она хорошо подходит для того, чтобы попробовать себя в роли программиста. Мой сын уже во всю с друганами делает какие‑то игры там, потом они в них играют сами. И на курсы он уже тоже ходил, где показывали основы скриптинга на Lua. Однако у него часто возникают вопросы и проблемы, когда он не знает, что делать, в основном с написанием кода. И приходится подключать папу‑программиста, то есть меня.

Сам я пишу код уже почти 20 лет, писал на разных языках (C++, C#, Java/Kotlin, Python) и даже в геймдеве на заре своей карьеры работал. Однако просто так сходу взять и разобраться в организации внутренностей игры в Roblox оказалось не так‑то просто. Уж слишком там все отличается от того, к чему привыкли энтерпрайзнутые‑Java‑синьоры типа меня.
И проблема не в том, что там используется незнакомый мне язык. Lua изначально создавалась чтобы быть максимально простым встраиваемым языком, который и ребенок поймет. В этом языке минимум концепций и конструкций, очень простой синтаксис, и начать писать код на нем совсем не сложно. Проблема именно в организации самой игры, которая имеет сложную внутреннюю структуру.
Roblox использует собственную бесплатную IDE под названием Roblox Studio. И когда впервые ее открываешь, глаза разбегаются от всяких кнопочек, деревьев объектов и непонятных штуковин. Тут нет просто папочек с кодом и конфигами, как я привык, зато есть тыща разных видов объектов, организованных в сложную иерархию и присыпанных сверху необходимостью делать сетевую синхронизацию между клиентом и сервером.

При этом все гайды по Roblox в интернете написаны для начинающих, в основном для детей, которые вообще не умеют программировать. И касаются они только совсем простых вещей, типа как сделать кубик, или как нанести игроку урон при касании этого кубика. При этом гайды часто не объясняют базу, структуру игры, а лишь дают набор действий типа: «нажмите тут, скопируйте этот скрипт вот сюда». А почему именно сюда, а не в другое место, не объясняют.
Возможно, такой подход обоснован для полных новичков, которым надо как можно скорее получить наглядный результат, иначе они заскучают и бросят. Но он не подходит для матерых бородатых бать‑программистов типа меня, к которым их дети обращаются за помощью, если что‑то не работает. И которым проще сперва уяснить теоретическую базу и основные принципы устройства игры, а уже потом, зная ее, легко написать любой скрипт.
Поэтому я решил написать этот краткий гайд по Roblox для опытных разработчиков. В нем не будет создания кубика (ну на самом деле немного будет, куда без этого), вместо этого я расскажу, как работает серверная синхронизация между игроками, как сделать нетривиальную игровую логику и что это за куча объектов в структуре игры. Погнали!
Общая структура игры
В Roblox игра это... дерево.
Каждый игровой элемент, будь то скрипт, модель, игровой объект, предмет в инвентаре — является узлом большого дерева. У любого элемента есть набор стандартных базовых операций для узла дерева: возможность получать своего родителя, братьев‑сестер, детей, возможность искать ребенка с конкретным именем или свойствами. Все это определено в базовом классе Instance
. Если вы работали с DOM или любыми другими древовидными структурами, тут для вас все будет просто и понятно.
Корнем дерева является объект DataModel
, доступный через глобальную переменную game
. Все манипуляции с деревом дальше осуществляются через него.
Комплексные игровые объекты сами являются деревьями. Например, типичный предмет типа «оружие», навроде меча или бластера, будет деревом с корневой вершиной типа Tool
, у которой будут дочерние вершины с моделью, анимациями, скриптами. В некоторых случаях структура дерева задается игрой, например у того же оружия обязательно должен быть ребенок с именем Handle
, который будет описывать прикрепление оружия к модели игрока. В остальном вы вольны группировать элементы как вам будет угодно.

В качестве примера в этой статье рассмотрим пример лампочки, которую игроки смогут включать и выключать, при этом состояние лампочки будет синхронизироваться между всеми игроками на сервере. Начнем с базы:
local lamp = Instance.new("Part") -- Part это класс для большинства объектов мира, обладающий координатами и 3д моделью
lamp.Name = "SmartLamp"
lamp.Anchored = true -- Координаты фиксированы, объект не будет обрабатываться физикой
lamp.Parent = workspace -- Кладем созданный объект в Workspace (о нем ниже)
local light = Instance.new("PointLight") -- создаем источник света
light.Name = "LampLight"
light.Enabled = false -- по умолчанию не горит
light.Parent = lamp -- кладем его в поддерево нашей лампы
local clickDetector = Instance.new("ClickDetector") -- создаем обработчик нажатий
clickDetector.Parent = lamp -- тоже кладем в поддерево нашей лампы
Этот код нам сгенерировал мини-дерево из объекта Part
(который по умолчанию будет просто кубиком), к которому привязаны источник света и возможность кликать на него. И добавил его на карту мира Workspace
.
То же самое можно сделать и через саму Roblox Studio, создавая и передвигая элементы в дереве:

Теперь можно перетаскивать корень нашего дерева — элемент SmartLamp, и все его потомки будут перетаскиваться вместе с ним.
Основные компоненты дерева
Положение объекта в дереве игры определяет его состояние и как Roblox будет его обрабатывать. Когда вы создаете новую игру, в вашем дереве уже есть набор стандартных компонент первого уровня, называемых Сервисами. Каждый Сервис это такой синглтон, отвечающий за работу одной из подсистем игры. Существует ровно один экземпляр каждого сервиса в течение сессии. Они автоматически создаются движком и доступны через методы GetService
. Например, game:GetService("ReplicatedStorage")
предоставляет доступ к хранилищу объектов, реплицируемых между сервером и клиентами.
Поместив один и тот же объект в поддеревья разных компонент, вы получите разное поведение. Например, поместив ваш предмет в качестве ребенка к родителю Workspace
(как я сделал в коде выше), вы добавите его в мир игры, где он будет лежать, игроки смогут его видеть и взаимодействовать с ним.
Поместив его в StarterPack
вы скажете игре, что этот предмет должен выдаваться в инвентарь каждому зашедшему на сервер игроку сразу при входе в игру. А положив его вServerStorage
вы вообще не увидите эффекта. Этот элемент — корень для хранилища объектов, которые будут клонироваться из скриптов, но автоматически в игровой мир добавляться не будут.

От обилия и разнообразия этих начальных элементов при первом запуске Roblox Studio немного разбегаются глаза. А их перечень выглядит каким‑то избыточно многословным, и сочетающим как абстрактные базовые концепции, нужные для любой сетевой игры (ивенты, скрипты, игроки), так и какие‑то нишевые вещи типа команд, на которые можно распределить игроков. Создается ощущение, что этот список наверняка формировался под какие‑то конкретные игры и является десятилетним легаси-нагромождением всего подряд.
Перечислю основные разделы дерева, которые понадобятся при создании практически любой игры, и куда вы можете складывать свои элементы игрового дерева:
ServerScriptService: Главное место для серверных скриптов (элементы дерева класса
Script
, про виды скриптов напишу ниже). Тут лежит основная игровая логика, работающая для всех игроков.ServerStorage: Тут лежат заранее заготовленные предметы и объекты, которые не должны быть доступны в мире игры напрямую, но мы будем в дальнейшем их создавать через код, копируя отсюда.
ReplicatedStorage: Тут лежит то, что должно пересылаться между клиентами. Например, события
RemoteEvent
, на которых мы будем строить синхронизацию между клиентами и сервером.ReplicatedFirst: То что надо отправить на клиент в первую очередь при загрузке. Я в своих простых играх никогда не использовал.
StarterPack: Все объекты, лежащие под этим корневым узлом, будут скопированы в инвентарь каждого игрока при подключении к игре. Можно сюда положить какое‑нибудь стартовое оружие. Чтобы оно корректно работало, корневым узлом для предметов в инвентаре должен быть
Tool
StarterPlayer: Локальные скрипты (
LocalScript
, про систему скриптов ниже), выполняющиеся при подключении игрока в первый раз. Например, сюда можно положить скрипты, которые создадут пользовательский интерфейс.
Workspace: Игровой мир. Объекты, помещенные сюда, будут отображаться в игре. Это земля, предметы, объекты уровня и тому подобное
ECS
В Roblox используется подход, похожий на Entity Component System. Этот паттерн проектирования хорошо знаком всем, кто работает в геймдеве, для них это база, а вот за пределами этой сферы встречается не так часто. Так что расскажу пару слов про него.
ECS (Entity‑Component‑System) — это архи��ектурный паттерн, который разделяет игровые объекты на три составляющие: Entity (сущность), Component (компонент) и System (система). Сущности представляют собой простые контейнеры с идентификатором, компоненты содержат только данные для какого‑то кусочка игровой логики, а системы обрабатывают логику, работая с группами компонентов.
Теперь если вам надо создать объект «зомби», вы создаете пустую сущность, а потом наполняете ее нужными компонентами. Один компонент хранит модель и анимации, другой компонент хранит данные для физического движка, третий хранит HP и прочие игровые параметры, четвертый хранит данные для ИИ. А во время игры отдельные системы движка пробегаются по списку всех сущностей, выбирают те, у кого есть нужные им компоненты, и применяют к ним свою логику. Например, подсистема нанесения урона ищет все сущности, у которых есть компонента HP, проверяет условия (пересечение с пулей или лавой) и вычитает из этого HP урон. А подсистема физики ищет сущности с компонентами, описывающими физические свойства (положение в мире, массу, скорость) и меняет их положение по своим физическим законам. При этом каждой системе максимально пофигу на то, что за сущность она сейчас обрабатывает: то ли персонажа игрока, то ли лежащий на земле камень. Важно лишь наличие у этой сущности компоненты нужного типа с нужными системе свойствами.

Не буду тут приводить аргументы за и против ECS подхода. Просто скажу, что во всех современных играх и игровых движках, о которых я слышал, используется именно он или что‑то похожее на него, и индустрия пришла к этому за 20 лет мучений с ООП.
В Roblox реализация ECS не является чистой, как в специализированных движках, но принципы прослеживаются. Entity в Roblox — это экземпляры (Instance
), такие как Part
, Model
или Player
. Каждый экземпляр содержит компоненты в виде свойств и дочерних объектов. Например, Part
имеет свойства Position
, Size
, Color
, которые выступают в роли данных‑компонентов, которые уже положены в него и всегда у него есть. При этом новая логика добавляется уже в стиле ECS: вы добавляете дополнительные компоненты (например, свет, или звук) в поддерево объекта, и соответствующая система игры их автоматически подхватывает.
Системы в Roblox — это встроенные движковые механизмы, которые автоматически обрабатывают определенные типы компонентов. Физическая система отслеживает все частицы с физическими свойствами и вычисляет их перемещения. Система рендеринга обрабатывает визуальные компоненты и отрисовывает сцену. Система анимации работает с костями и анимациями персонажей.
У меня создалось впечатление, что Roblox (который, на минуточку, существует с 2006 года!) сперва создавался как ООП проект, а потом уже по мере изменения подходов к геймдеву, частично был переведен на ECS. В итоге его внутренняя организация сочетает черты обоих подходов: базовый нерасширяемый набор классов, функционал которых можно дополнять, цепляя к ним в поддерево узлы‑компоненты со своим функционалом.
В наш пример с лампочкой мы добавили компонент PointLight
. Теперь наш объект будет автоматически обрабатываться системой освещения. Все что нам надо сделать, чтобы он начал светиться - выставить в соответствующей компоненте нужное свойство:
-- Изменяем только данные - системы делают остальное
local light = lamp:FindFirstChildOfClass("PointLight")
light.Enabled = not light.Enabled
Там же рядом лежат необходимые свойства для настройки самого освещения: радиус, свет, яркость и т.п.
Преимущество такого подхода становится очевидным при добавлении новых функций. Если мы добавим звуковой компонент, система аудио автоматически начнет его обрабатывать. Аналогично, добавление физических свойств автоматически включает обработку физической системой.
Скрипты серверные и локальные
Roblox это изначально сетевая платформа, заточенная под мультиплеерные игры. Так что ваша игра запускается одновременно на сервере и на клиентах. Под капотом платформа делает очень много работы, чтобы у вас из коробки уже работали по сети все базовые вещи, такие как перемещение игроков и синхронизация игрового мира. Все объекты, лежащие в Workspace, будут правильно синхронизированы, и разные игроки увидят один и тот же кубик в одних и тех же координатах. Это очень круто, так как позволяет сразу начать писать игровую логику, не разбираясь в дебрях сетевой синхронизации. И любой, кто пытался когда‑либо сделать мультиплеер полностью самостоятельно с нуля понимает, какой объем сложности и работы спрятан от игрока и от создателя игры.
Но рано или поздно вам придется расширить базовый функционал. И вот тут начинаются дебри, во всяком случае для меня, привыкшего к четкому разделению веб‑приложений на бэк и фронт. Которые взаимодействуют через API, а по коду вообще не связаны друг с другом, лежат в разных репозиториях и написаны на разных языках и разными командами.
В Roblox части клиентской и серверной логики лежат бок о бок в вашем дереве игры. При этом часть логики синхронизируется сама игровым движком (все что лежит в Workspace: объекты, их положения и состояние), а часть вам надо синхронизировать самостоятельно.
И для этого у вас есть два вида скриптов, которые вы можете добавить в ваше дерево‑игру (ну точнее три, но третий — ModuleScript
— это просто библиотека для переиспользования кода):
Script
, он же серверный скрипт. Выполняется на сервере игры.LocalScript
, он же локальный скрипт. Выполняется на клиенте игры.
В дереве они обозначаются разными значками и визуально различаются.
Локальный скрипт имеет дело только с клиентом. У него есть доступ к начинке самого клиента. Например у него есть переменная LocalPlayer
— игрок, на чьем клиенте он запущен. У него есть доступ к пользовательскому интерфейсу, возможность создавать его и что‑то отображать на экране. Есть доступ к пользовательскому инпуту.
В локальных скриптах вы можете делать вещи, которые влияют только на игрока и которые на надо синхронизировать с другими клиентами. Например, какие‑нибудь визуальные эффекты, партиклы. В принципе, если у вас игра чисто однопользовательская, вы можете ее целиком написать на локальных скриптах. Но всегда стоит помнить, что нельзя на 100% доверять тому, что происходит на клиенте. Так как читами или хаками игроки могут вмешиваться в их работу и состояние.
Серверный скрипт запускается на сервере, вне досягаемости пользовательских читов. По‑хорошему, всю значимую логику надо располагать именно тут. У серверного скрипта уже нет доступа к локально‑клиентским штукам типа инпута или UI. Зато он умеет рассылать события всем подключенным клиентам.
Это разделение на два вида скриптов иногда бесит, когда вы по ошибке выбираете не тот, который надо, а потом обнаруживаете что не можете в этом коде использовать какие‑то переменные или методы.
Еще раз подчеркну: Script
и LocalScript
выполняются в разных контекстах и разном окружении. По сути в разных виртуальных машинах. Если проводить аналогию с веб‑программированием, то Script
это код вашего бэкенда, а LocalScript
это JS который выполняется в браузере пользователя. Даже если они лежат в соседних папочках в исходном коде, во время выполнения у них нет прямой связи друг с другом, так как они работают на разных машинах. И у них разный контекст выполнения, разный набор доступных глобальных переменных и методов API движка.
События и синхронизация
Окей, у нас есть локальные скрипты, есть серверные. Как в итоге их использовать чтобы сделать какую-то свою логику?
Для этого нам пригодится RemoteEvent
. Это сетевое событие, которое можно пересылать между клиентами и сервером. Объект этого типа можно или создать вручную в интерфейсе студии, или через код. В любом случае он должен лежать в поддереве системы ReplicatedStorage
.
У RemoteEvent
есть имя, по которому мы можем его искать в дереве, есть произвольный набор параметров, который мы можем в нем пересылать. И есть методы для его отправки. Клиент в своем LocalScript
может отправить ивент на сервер. Сервер в своем Script
может отправить ивент или всем игрокам, или кому-то одному конкретному.
В итоге архитектура любого игрового объекта, который требует синхронизации между клиентами, выглядит как:
Корень поддерева имеет нужный для игровой логики тип, например
Part
если это объект игрового мира, илиTool
если это предмет, который игрок может взять в инвентарьВ
StarterPlayerScripts
лежитLocalScript
, который обрабатывает состояние предмета на стороне игрока, может создавать какой-то интерфейс или слушать пользовательский инпут. При необходимости изменения состояния скрипт отправляетRemoteEvent
на сервер. Этот же скрипт подписывается на обратные события от сервера, чтобы получать уведомление об изменении состояния другим игроком.Где-то еще (в
ServerScripts
или просто в поддереве предмета) естьScript
, который выполняется уже на сервере. Он подписывается на нашRemoteEvent
, ждет пока кто-то из игроков его пришлет, обрабатывает, затем рассылает всем игрокам другойRemoteEvent
, чтобы они обновили у себя состояние предмета
Итого наш пример выглядит как-то так. Локальный скрипт, управляющий внешним видом лампы на клиенте:
-- Локальный скрип в StarterPlayerScripts
-- Находим лампу в дереве уровня, используем WaitForChild так как загрузка уровня происходит
-- позже, чем запускается этот скрипт
local lamp = workspace:WaitForChild("SmartLamp")
local light = lamp:FindFirstChildOfClass("PointLight")
local toggleEvent = game.ReplicatedStorage:FindFirstChild("LampToggleEvent")
local stateSyncEvent = game.ReplicatedStorage:FindFirstChild("LampStateSyncEvent")
local clickDetector = lamp:FindFirstChildOfClass("ClickDetector")
-- Подписываемся на ивент клика мышкой по нашему парту и шлем ивент на сервер
clickDetector.MouseClick:Connect(function()
toggleEvent:FireServer(not light.Enabled)
end)
-- Подписываемся на ивенты с сервера об обновлении статуса лампы
stateSyncEvent.OnClientEvent:Connect(function(state)
light.Enabled = state.enabled
end)
И серверный скрипт, хранящий серверное состояние лампы, получающий ивенты от клиентов с кликами на нее и рассылающий всем новое состояние:
-- Серверный скрипт, я его положил в саму лампу, поэтому она ищется как его родитель в дереве
local lamp = script.Parent
local light = lamp:FindFirstChildOfClass("PointLight")
local toggleEvent = game.ReplicatedStorage:FindFirstChild("LampToggleEvent")
local stateSyncEvent = game.ReplicatedStorage:FindFirstChild("LampStateSyncEvent")
local lampState = {enabled = false}
local function applyState()
light.Enabled = lampState.enabled
end
toggleEvent.OnServerEvent:Connect(function(player, requestedState)
if typeof(requestedState) == "boolean" then
lampState.enabled = requestedState
applyState()
stateSyncEvent:FireAllClients(lampState)
end
end)
game.Players.PlayerAdded:Connect(function(player)
stateSyncEvent:FireClient(player, lampState)
end)
applyState()
Сами ивенты LampToggleEvent
и LampStateSyncEvent
я создал через интерфейс студии. Но можно все сделать и чисто через код, через манипуляцию деревом, доступную через методы.

Добавлю что этот пример с лампочкой и ивентами все-таки немного синтетический. Свойства базовых компонент Roblox синхронизирует между клиентами сам. Так что можно было бы обойтись и без клиентских скриптов и ивентов, достаточно было бы написать серверный скрипт, выставляющий объекту PointLight свойство Enabled, а рассылкой его по клиентам движок игры занялся бы сам.
Toolbox
Пара слов про Toolbox, он же магазин всяких готовых кусков игр.
Идея магазина очень легко ложится на древовидную структуру игры. В магазине хранятся фактически готовые деревья, состоящие из модели, скриптов, ивентов, ресурсов и всего-всего. Скрипты, их обрабатывающие, используют методы для обхода своего поддерева и в целом не интересуются что там снаружи. Получается этакая инкапсуляция. Вставив из магазина готовый предмет "меч", вы получаете сразу вагон связанной с ним логики, и у вас сразу все работает (ну или не работает, половина объектов в Toolbox глючные и нерабочие).

Все это позволяет делать игру вообще без программирования. Сделать простенький шутер, в котором игроки сообща будут бегать по городу и стрелять по зомби, можно исключительно перетаскивая готовые решения из магазина мышкой и не прикасаясь к клавиатуре. И в объекте "зомби" уже будут скрипты поиска и нападения на игрока, а в объекте "автомат" будут скрипты наносящие урон. И поскольку работает это все через одни и те же стандартные классы и компоненты, которые движок обеспечивает из коробки, оно все внезапно неплохо друг с другом взаимодействует. Автомат, сделанный одним автором, успешно убивает зомби, сделанных другим, так как они оба полагаются на стандартную компоненту Humanoid
, содержащую здоровье и разные статусы для человекообразных игровых персонажей.
Заключение
Программирование в Roblox оказалось для меня неплохой разминкой мозгов. Немного отдохнуть от того, чем я занимаюсь на работе, и поковыряться в чем-то совершенно новом. Как на уровне языка, так и на уровне концепций.

Причем делать игры в нем реально прикольно и очень просто. Если у вас еще с детства свербит желание сделать свою стрелялку и прыгалку, то вкатиться в Roblox очень просто. Достаточно понять пару основных концепций, которые я описал в этой статье (дерево игры, сущности, клиент-серверную синхронизацию), и можно лепить свой очередной шедевр.
Конечно, в чем-то движок вас ограничивает. Не думаю, что на этой платформе легко сделать что-то кроме бегалки-прыгалки-стрелялки, игру без явного человечка-персонажа. Но даже так вариантов множество. В Roblox делают и аркадные авиасимуляторы, и Tycoon'ы, и градостроительные игры. Простор для фантазии огромный, а простые (хоть иногда и очевидно хардоженно-легасево-костыльные) инструменты позволяют делать это легко.

Надеюсь, кому-нибудь статья поможет помочь своим детям, которые будут пытаться что-то в Roblox создать, или подтолкнет к своему собственному творчеству.