Перший скрипт дитини в 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(gravec)
    gravec.CharacterAdded:Connect(function(person)
        print(gravec.Name, 'готовий до гри')
    end)
end)

Вкладена схема — підписка на подію всередині іншої події — спочатку виглядає надто складною, але це стандартний спосіб, і далі він зустрінеться десятки разів. CharacterAdded до того ж спрацьовує після кожного відродження, а не лише раз на вхід, і це саме те, що потрібно для нагород чи початкового інвентарю.

RemoteEvent: розмова клієнта з сервером

Клієнтський скрипт не може змінювати те, що бачать інші гравці, а серверний не знає, на що натиснув конкретний гравець. Міст між ними — RemoteEvent, об'єкт, який кладуть у ReplicatedStorage.

НапрямокХто надсилаєХто ловить
Клієнт серверуFireServer()OnServerEvent
Сервер одномуFireClient(gravec)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 zednannia = chastyna.Touched:Connect(function() end)
zednannia:Disconnect()

Поки об'єкт живий, з'єднання теж живе. Якщо скрипт підписується на подію в циклі й не відключає старі підписки, їх стає сотні, кожна робить свою роботу, і гра поступово гальмує без видимої причини. Для дії, яка потрібна один раз, зручніше одразу відключати підписку всередині самої функції.

Події — той момент, після якого проєкт починає відчуватися грою, а не набором деталей. Якщо синтаксис ще не звичний, почніть із базового синтаксису Lua, збереження прогресу розібране в матеріалі про DataStore, а те, що ламає перший проєкт найчастіше, зібране в помилках початківців.