Первый скрипт ребёнка в Roblox Studio почти всегда делает что-то один раз: ставит цвет, двигает деталь, печатает строку. Игра от этого ещё не появляется. Игра появляется тогда, когда скрипт перестаёт действовать сам и начинает ждать — прикосновения, клика, входа игрока. Этот механизм ожидания и называется событием.

Ниже — события, которых хватает на любой первый проект, с примерами и теми ловушками, из-за которых они работают «не так». Справочник рассчитан на то, что базовый синтаксис уже знаком: что такое функция, переменная и где в Studio лежит скрипт.

Как выглядит подключение

Запись всегда одинаковая: объект, точка, имя события, двоеточие, Connect и функция, которую надо вызвать.

local dver = script.Parent

dver.Touched:Connect(function(hit)
    print('кто-то коснулся:', hit.Name)
end)

Два места здесь бывают непонятными. Точка перед именем события означает «возьми у этого объекта», а двоеточие перед Connect — «вызови метод у самого события». Перепутать их легко, и Studio подчеркнёт строку, но сообщение об ошибке мало что объясняет. Второе — слово hit в скобках: это не что-то, что надо объявить, а подарок от самого события. Оно само передаёт туда то, что именно коснулось, и назвать этот параметр можно как угодно.

События, которые нужны чаще всего

СобытиеКогда срабатываетЧто передаёт
Touchedдетали что-то коснулоськоснувшуюся деталь
TouchEndedкасание закончилосьту же деталь
MouseClickклик по ClickDetectorигрока, который кликнул
MouseButton1Clickклик по кнопке интерфейсаничего
PlayerAddedигрок зашёл в игруобъект игрока
PlayerRemovingигрок выходитобъект игрока
CharacterAddedперсонаж появился на картемодель персонажа
Diedперсонаж погибничего
Changedсвойство изменилосьимя свойства
Heartbeatкаждый кадр на серверевремя с прошлого кадра

Touched: самое частое и самое капризное

Касание кажется простым, пока ребёнок не поставит на него выдачу монет. Тогда выясняется, что за один проход персонажа игрок получает не одну монету, а двадцать: событие срабатывает на каждую часть тела и повторяется, пока контакт продолжается. Для игры это выглядит поломкой экономики, а на самом деле событие работает ровно так, как должно.

Лечится флажком, который у разработчиков зовётся дебаунс. Смысл в том, чтобы скрипт сам помнил, что уже выдал награду, и до конца паузы игнорировал повторные касания.

local moneta = script.Parent
local zaniato = false

moneta.Touched:Connect(function(hit)
    if zaniato then return end
    local humanoid = hit.Parent:FindFirstChild('Humanoid')
    if not humanoid then return end
    zaniato = true
    print('монета собрана')
    task.wait(1)
    zaniato = false
end)

Вторая важная строка здесь — поиск Humanoid. Без него монету «соберёт» что угодно: упавшая деталь, брошенный предмет, другая монета. Проверка на наличие гуманоида отсекает всё, что не является персонажем, и это та самая проверка, которой почти никогда нет в первых скриптах из видеоуроков.

Клики: два разных механизма

Клик по объекту в мире и клик по кнопке интерфейса — это разные события, и их часто путают. Для предмета на карте нужно сначала вложить в него ClickDetector, иначе кликать будет нечем: сама деталь событий мышки не имеет. После этого работает MouseClick, и она удобно передаёт в функцию того игрока, который кликнул, — не надо догадываться, кто это был.

Кнопка интерфейса устроена иначе: она уже кнопка, детектор ей не нужен, и событие у неё своё — MouseButton1Click. Ничего она не передаёт, потому что нажать кнопку на своём экране может только её владелец. Именно поэтому скрипт для кнопки должен быть клиентским и лежать в StarterPlayerScripts или рядом с самой кнопкой, а не в ServerScriptService.

Игрок зашёл, персонаж родился

Здесь ловушка, которая съедает больше всего времени. PlayerAdded срабатывает, когда человек подключился, но персонажа на карте в этот момент ещё нет — модель появится позже. Скрипт, который сразу после входа пытается найти Humanoid, ничего не находит и тихо ничего не делает.

local Players = game:GetService('Players')

Players.PlayerAdded:Connect(function(igrok)
    igrok.CharacterAdded:Connect(function(person)
        print(igrok.Name, 'готов к игре')
    end)
end)

Вложенная схема — подписка на событие внутри другого события — сначала выглядит слишком сложной, но это стандартный способ, и дальше он встретится десятки раз. CharacterAdded к тому же срабатывает после каждого возрождения, а не только раз на вход, и это именно то, что нужно для награды или начального инвентаря.

RemoteEvent: разговор клиента с сервером

Клиентский скрипт не может менять то, что видят другие игроки, а серверный не знает, на что нажал конкретный игрок. Мост между ними — RemoteEvent, объект, который кладут в ReplicatedStorage.

НаправлениеКто отправляетКто ловит
Клиент серверуFireServer()OnServerEvent
Сервер одномуFireClient(igrok)OnClientEvent
Сервер всемFireAllClients()OnClientEvent

Одна деталь ломает половину первых попыток: в OnServerEvent первым параметром сервер всегда подставляет игрока сам, автоматически. То есть если клиент отправил одно число, функция на сервере получит два аргумента — сначала игрока, потом число. Ребёнок, который этого не знает, читает вместо числа непонятный объект и не понимает, откуда он взялся.

И правило, без которого игра станет взломанной через час после публикации: никогда не верить тому, что пришло с клиента. Если клиент отправляет «добавь мне сто монет», сервер должен сам решить, заслужил ли игрок эти монеты. Иначе любой с редактором скриптов отправит себе миллион.

Когда изменилось свойство

Событие Changed удобно для здоровья, очков, уровня — всего, что надо отслеживать. Но у него есть расхождение, на котором спотыкаются все. У обычного объекта оно передаёт имя изменившегося свойства, а у объектов-значений вроде IntValue — само новое значение. Один и тот же код на двух объектах ведёт себя по-разному, и выглядит это мистикой.

Надёжнее просить конкретное свойство отдельно, тогда гадать не придётся:

humanoid:GetPropertyChangedSignal('Health'):Connect(function()
    print('уровень жизни теперь', humanoid.Health)
end)

Каждый кадр: когда событие заменяет цикл

Отдельная категория — события, срабатывающие постоянно. Heartbeat из службы RunService вызывается каждый кадр и передаёт, сколько времени прошло с предыдущего. Выглядит это как цикл, но принципиально отличается: игра сама решает, когда вызвать функцию, и не блокирует всё остальное.

Разница важна именно потому, что новичок вместо этого события пишет while true do без паузы. Такой цикл не отдаёт управление игре, и Studio либо повисает, либо выдаёт ошибку о перегрузке. Если что-то должно происходить непрерывно — вращение платформы, отсчёт таймера, проверка расстояния — берите событие кадра, а не бесконечный цикл.

Попутно стоит знать границу: вычисления внутри такой функции выполняются шестьдесят раз в секунду, поэтому тяжёлая работа там очень быстро просаживает игру. Всё, что можно посчитать один раз заранее, считают до подписки, а не в ней.

Почему событие не сработало: порядок проверки

Когда скрипт молчит, проверять стоит по порядку, а не наугад. Первое — выполняется ли вообще скрипт: поставьте print в первую строку, до всех подписок. Если в Output ничего нет, проблема не в событии, а в том, что скрипт лежит не там, где его запускают.

Второе — правильный ли объект. Запись script.Parent возвращает то, во что вложен скрипт, и если ребёнок перетащил скрипт на уровень выше, Parent стал другим. Третье — серверный скрипт или клиентский: событие кнопки интерфейса на сервере не сработает никогда, и ошибки при этом не будет, потому что сама подписка совершенно законна.

Отключение: о чём забывают

Вызов Connect возвращает соединение, и его можно разорвать:

local soedinenie = chast.Touched:Connect(function() end)
soedinenie:Disconnect()

Пока объект жив, соединение тоже живёт. Если скрипт подписывается на событие в цикле и не отключает старые подписки, их становятся сотни, каждая делает свою работу, и игра постепенно тормозит без видимой причины. Для действия, которое нужно один раз, удобнее сразу отключать подписку внутри самой функции.

События — тот момент, после которого проект начинает ощущаться игрой, а не набором деталей. Если синтаксис ещё не привычен, начните с базового синтаксиса Lua, сохранение прогресса разобрано в материале про DataStore, а то, что ломает первый проект чаще всего, собрано в ошибках новичков.