RemoteEvent в Roblox простыми словами - это одностороннее сообщение между клиентом и сервером. LocalScript вызывает FireServer(), сервер принимает запрос через OnServerEvent, проверяет его и меняет игру. В обратную сторону сервер использует FireClient() или FireAllClients().
Клиент отвечает за ввод, камеру и локальный интерфейс. Сервер хранит общие правила, награды и состояние, которое должны видеть все игроки. Эти стороны не вызывают функции друг друга напрямую. RemoteEvent служит мостом, но не переносит доверие. Сообщение клиента означает «я прошу купить предмет», а не «у меня уже достаточно монет и предмет нужно выдать». Решение принимает сервер.
Клиент отправляет запрос серверу.
Сервер получает Player первым аргументом и проверяет запрос.
Сервер сообщает результат конкретному клиенту.

RemoteEvent Roblox простыми словами на примере магазина
Создайте RemoteEvent BuyItem в ReplicatedStorage. LocalScript кнопки отправляет только идентификатор предмета. Сервер держит собственный каталог цен, проверяет тип аргумента и текущий баланс. Он не принимает цену от клиента. Ниже показана серверная часть, которая защищает ключевые правила.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local buyItem = ReplicatedStorage:WaitForChild("BuyItem")
local prices = {
SpeedCoil = 100,
JumpCoil = 140,
}
buyItem.OnServerEvent:Connect(function(player, itemName)
if typeof(itemName) ~= "string" then
return
end
local price = prices[itemName]
local coins = player:GetAttribute("Coins") or 0
if not price or coins < price then
buyItem:FireClient(player, false, "Purchase rejected")
return
end
-- Баланс меняет только сервер после всех проверок
player:SetAttribute("Coins", coins - price)
buyItem:FireClient(player, true, itemName)
end)- Корректный запрос: баланс уменьшается, клиент получает true
- Неизвестный предмет или мало монет: сервер возвращает false
LocalScript может вызвать buyItem:FireServer("SpeedCoil") и показать состояние ожидания. После OnClientEvent интерфейс отображает успех или отказ. Настоящую выдачу Tool тоже делает сервер. Для часто повторяющихся действий добавьте rate limit по Player. Проверяйте не только типы, но и контекст: расстояние до магазина, состояние раунда, наличие предмета и допустимую частоту.
Какое средство связи выбрать
| Элемент | Что означает | Что делать |
|---|---|---|
| RemoteEvent | Одностороннее сообщение | Запрос действия, уведомление UI |
| RemoteFunction | Запрос с ожидаемым ответом | Редкие синхронные чтения |
| UnreliableRemoteEvent | Частые некритичные обновления | Позиция эффекта, потоковые данные |
| BindableEvent | Связь на одной стороне | Модули сервера между собой |
| Attributes | Репликация состояния | Небольшие наблюдаемые значения |
RemoteFunction удобна, но ожидающая сторона приостанавливается до ответа. Не используйте её для каждого кадра. RemoteEvent не ждёт результат, поэтому интерфейс должен уметь показать загрузку и получить отдельное подтверждение. Для визуального эффекта, где потеря одного обновления не критична, подходит UnreliableRemoteEvent.

Практика: RemoteEvent BuyItem и покупка SpeedCoil за 100 монет
Соберите в Roblox Studio покупку одного предмета через RemoteEvent с именем BuyItem. Клиент отправляет только строку с названием предмета, а каталог цен и баланс в атрибуте Coins остаются на сервере. Успех выглядит так: покупка SpeedCoil за 100 монет уменьшает Coins на сервере, а клиент получает true и имя предмета через OnClientEvent.
- BuyItem в ReplicatedStorage. Создайте RemoteEvent, назовите его BuyItem и положите в ReplicatedStorage. В серверном скрипте получите его строкой ReplicatedStorage:WaitForChild("BuyItem") и убедитесь, что Output чист, а скрипт не завис на ожидании.
- Каталог цен и Coins. В серверном скрипте заведите таблицу prices с SpeedCoil = 100 и JumpCoil = 140 и выдайте игроку стартовый баланс через player:SetAttribute("Coins", 150). Значение видно в свойствах игрока на вкладке Attributes прямо во время игры.
- Обработчик OnServerEvent. Подпишитесь на buyItem.OnServerEvent и опишите функцию с аргументами player и itemName, проверьте typeof(itemName) ~= "string" и наличие цены в prices. Отклонённый запрос завершайте вызовом FireClient с false и текстом Purchase rejected.
- FireServer из LocalScript. В LocalScript кнопки вызовите buyItem:FireServer("SpeedCoil") и покажите состояние ожидания на кнопке. Нажмите Play: атрибут Coins должен упасть со 150 до 50, а изменение выполняется только серверным кодом.
- Ответ через OnClientEvent. Подпишитесь в LocalScript на buyItem.OnClientEvent и выводите успех или отказ в интерфейс. Попробуйте купить второй SpeedCoil: монет не хватит и на экране появится Purchase rejected.
Сломайте контракт намеренно: отправьте buyItem:FireServer(999) вместо строки. Проверка typeof отсечёт запрос, функция завершится через return, баланс Coins останется прежним, и это правильное поведение. Потом перенесите BuyItem в ServerStorage и запустите игру снова: LocalScript повиснет на WaitForChild, потому что клиент не видит содержимое ServerStorage. Верните объект в ReplicatedStorage и повторите удачную покупку.
Дальше добавьте rate limit по Player: храните время последнего запроса игрока и отбрасывайте повторы чаще одного раза в секунду. Затем расширьте проверки контекстом: расстояние до магазина, состояние раунда и наличие предмета в инвентаре. Когда покупка станет надёжной, пусть сервер выдаёт настоящий Tool, а подтверждённый баланс уходит в DataStore. Для частых некритичных обновлений вроде позиции эффекта возьмите UnreliableRemoteEvent, а редкие синхронные чтения оставьте RemoteFunction.
Частые ошибки и почему они появляются
Уязвимый RemoteEvent обычно слишком мощный. Например, клиент отправляет произвольный Instance, а сервер удаляет его, или передаёт сумму награды без проверки. Другой риск связан со спамом: корректный запрос становится проблемой, если его можно послать сотни раз за секунду. Сервер должен быть узким шлюзом для одного понятного действия.
Передавайте itemName, а цену находите в серверном каталоге.
Клиент не увидит объект. Для общей доступности обычно нужен ReplicatedStorage.
Если результат нужен одному игроку, используйте FireClient и не создавайте лишний трафик.
Самопроверка
Кто приходит первым аргументом в OnServerEvent?
Первым аргументом в OnServerEvent приходит Player, который вызвал FireServer. Roblox подставляет его автоматически, клиент этот аргумент не отправляет. Поэтому обработчик выглядит как function(player, itemName), где itemName это уже первый пользовательский аргумент.
Можно ли доверять данным, которые клиент прислал через FireServer?
Данным клиента доверять нельзя: через FireServer можно отправить любое значение любого типа. Сервер сам проверяет тип аргумента, ищет цену в собственном каталоге и читает баланс из своего состояния. Клиент только просит выполнить действие, решение принимает сервер.
Чем RemoteEvent отличается от RemoteFunction?
RemoteEvent передаёт одностороннее сообщение и не ждёт ответа, а RemoteFunction ждёт возврата и приостанавливает вызывающую сторону. Из-за ожидания RemoteFunction плохо подходит для действий, которые повторяются каждый кадр. При RemoteEvent интерфейс показывает загрузку и получает подтверждение отдельным сообщением через OnClientEvent.
Когда нужен FireAllClients вместо FireClient?
FireAllClients нужен, когда подтверждённое сервером событие должны увидеть все подключённые игроки, например начало раунда или общее объявление. Если результат касается одного игрока, берите FireClient и адресуйте сообщение ему. Рассылка всем ради личного ответа создаёт лишний трафик без пользы.
Где хранить RemoteEvent, в ReplicatedStorage или ServerStorage?
RemoteEvent обычно кладут в ReplicatedStorage, потому что объект должен быть доступен и клиенту, и серверу. Из ServerStorage клиент его не увидит, и WaitForChild в LocalScript не дождётся события. Серверные скрипты получают тот же объект из ReplicatedStorage через WaitForChild.
Как защитить RemoteEvent от спама запросами?
От спама защищает rate limit по Player на стороне сервера: сервер помнит время последнего запроса игрока и отбрасывает слишком частые. Корректный запрос превращается в проблему, когда его можно послать сотни раз за секунду. Помогает и узкая роль события: одно RemoteEvent на одно понятное действие проверять и ограничивать проще.
Что делать дальше
Сделайте один RemoteEvent для конкретной механики и запишите контракт: аргументы, проверки, ответ и частота. Основы функций и таблиц закрепите на курсе Lua. Затем свяжите событие с кнопкой ScreenGui, выдайте предмет по схеме из статьи про Tool и сохраняйте подтверждённый сервером баланс через DataStore.
