Studio

Античит без античита: как сделать игру, в которой читеру нечего ломать

Клиент в Roblox принадлежит игроку, и остановить его подмену нельзя. Зато можно построить сервер так, что подмена ничего не даёт. Разбираем, что проверять, что считать и почему кик по подозрению — плохая идея.

Сложно 25 минут проверен 35 дней назад

Что понадобится

Понимание клиента и сервера, RemoteEvent и RemoteFunction. Стоит сначала прочитать гайды про магазин и админ-команды — здесь та же мысль, доведённая до конца.

Текст гайда «Античит без античита: как сделать игру, в которой читеру нечего ломать»

Содержание

Первое, с чем нужно смириться: всё, что работает на устройстве игрока, игроку и принадлежит. Локальные скрипты можно прочитать, изменить, остановить. Любое сообщение на сервер можно отправить руками, с любыми аргументами, сколько угодно раз. Никакой «защиты клиента» не существует, и время, потраченное на неё, потрачено зря.

Единственная работающая формула

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

Шаг 1. Инвентаризация: что решает клиент#

Открой каждый RemoteEvent и RemoteFunction в игре и ответь на один вопрос: что случится, если это сообщение придёт с выдуманными аргументами тысячу раз подряд? Список ниже — типичные ответы и что с ними делать.

Чему сервер верить не должен
Клиент присылаетПочему нельзя веритьЧто делать вместо
Сумму уронаПришлют миллионСервер знает оружие игрока и считает урон сам
Кого ударилПришлют игрока на другом конце картыСервер проверяет расстояние и направление
Цену покупкиПришлют нольЦена лежит на сервере, клиент шлёт только название
«Я прошёл уровень»Пришлют сразуСервер сам знает, где стоит игрок и что он сделал
Позицию персонажаВ Roblox её и так реплицирует клиентПроверять скорость перемещения, а не положение
Свой уровень, деньги, флаг «админ»Любое из этого — подстановкаНикогда не принимать состояние; только намерения

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

Шаг 2. Три проверки на каждом входе#

Каждый обработчик начинается с одних и тех же трёх вопросов. Не два из трёх — все три.

ServerScriptService → Script «Combat» — удар в ближнем бою · Luau
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local attack = ReplicatedStorage:WaitForChild("MeleeAttack")

local REACH = 8          -- дальность удара в стадах, с запасом на задержку
local COOLDOWN = 0.45    -- быстрее оружие бить не умеет
local DAMAGE = 20        -- клиент эту цифру не присылает никогда

local lastHit = {}

local function rootOf(character)
    return character and character:FindFirstChild("HumanoidRootPart")
end

attack.OnServerEvent:Connect(function(player, target)
    -- 1. Форма: то ли это, что мы ждём?
    if typeof(target) ~= "Instance" or not target:IsA("Model") then
        return
    end

    local targetHumanoid = target:FindFirstChildOfClass("Humanoid")
    local attacker = rootOf(player.Character)
    local victim = rootOf(target)

    if targetHumanoid == nil or attacker == nil or victim == nil then
        return
    end

    -- 2. Право: может ли этот игрок сейчас это сделать?
    local now = os.clock()

    if lastHit[player] and now - lastHit[player] < COOLDOWN then
        return
    end

    if targetHumanoid.Health <= 0 or target == player.Character then
        return
    end

    -- 3. Возможность: правдоподобно ли это физически?
    if (attacker.Position - victim.Position).Magnitude > REACH then
        return
    end

    lastHit[player] = now
    targetHumanoid:TakeDamage(DAMAGE)
end)

Players.PlayerRemoving:Connect(function(player)
    lastHit[player] = nil
end)
  • Форма. Тип и структура аргументов. typeof, IsA, проверка на nil. Скрипт, который падает от неверного типа, — уже уязвимость: упавший обработчик перестаёт работать для всех.
  • Право. Может ли игрок сделать это сейчас: перезарядка, наличие предмета, жив ли он сам, есть ли у него это оружие. Всё это сервер знает без клиента.
  • Возможность. Совпадает ли просьба с миром: расстояние, направление, стена между. Здесь и живёт запас на задержку — проверка строгая, но не до пикселя.

Молчать, а не ругаться

Ни в одной ветке отказа сервер не отвечает клиенту «нельзя». Честному игроку это не нужно — его клиент такой запрос не отправит. А тому, кто ищет границы, каждый ответ — подсказка, где они проходят. Отказ — это просто return.

Шаг 3. Скорость и телепорт — проверять траекторию, а не точку#

Позицию персонажа в Roblox по умолчанию задаёт клиент, и это не изменить без перехода на серверную авторитетность. Значит, «полёт» и «спидхак» технически всегда возможны. Отбирать их напрямую сервер не может — но может не давать им ничего заработать.

Мягкая проверка перемещения · Luau
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

local MAX_SPEED = 30      -- обычная WalkSpeed 16 плюс запас на прыжки и задержку
local WINDOW = 1          -- считаем по секундам, а не по кадрам

local track = {}

RunService.Heartbeat:Connect(function()
    local now = os.clock()

    for _, player in ipairs(Players:GetPlayers()) do
        local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")

        if root then
            local last = track[player]

            if last == nil or now - last.at >= WINDOW then
                if last ~= nil then
                    local distance = (root.Position - last.position).Magnitude

                    if distance > MAX_SPEED * (now - last.at) then
                        -- Не кикаем. Возвращаем туда, где видели в прошлый раз,
                        -- и запоминаем: три таких события подряд — повод
                        -- посмотреть журнал, а не автоматически банить.
                        root.CFrame = CFrame.new(last.position)
                        player:SetAttribute("speedFlags", (player:GetAttribute("speedFlags") or 0) + 1)
                    end
                end

                track[player] = { at = now, position = root.Position }
            end
        end
    end
end)

Players.PlayerRemoving:Connect(function(player)
    track[player] = nil
end)

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

Почему не кикать по подозрению

Честный игрок с плохим Wi-Fi выглядит для сервера в точности как читер: рывки, скачки позиции, пропавшие секунды. Автоматический бан за это — потеря живого человека ради одного пойманного. Автоматика откатывает и записывает; решает человек по журналу. Это медленнее, но игра от этого не пустеет.

Шаг 4. Считать невозможное#

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

  • Монеты в час: если игрок заработал за час больше, чем физически даёт карта, — запись в журнал
  • Точность: сто попаданий из ста за вечер — повод посмотреть, а не награда
  • Ритм: одинаковый интервал между нажатиями с точностью до миллисекунды — так не жмут люди
  • Время без движения курсора при постоянных действиях

Здесь особенно важна дисциплина: ни одна из этих цифр не должна сама по себе выгонять. Они собираются в атрибут или в отдельный DataStore, а раз в неделю ты открываешь список и смотришь глазами. Десять минут на это — и большинство ботов находятся без единого ложного бана.

Шаг 5. Что не давать клиенту вообще#

Всё, что попало в ReplicatedStorage, Workspace и StarterPlayer, можно прочитать с клиента. Отсюда правило: там не должно быть ничего, что помогает жульничать.

  • Расположение секретов и ответы на загадки — в ServerStorage, выдаются по мере открытия
  • Формулы урона, шансы дропа, цены — на сервере; клиенту только результат
  • Позиции других игроков вне зоны видимости движок реплицирует и так — лишний раз сообщать их не нужно
  • Модули с «общей логикой» между клиентом и сервером: проверь, что в них нет того, что нужно только серверу

Чего этот гайд не делает#

Здесь нет детекторов инжекторов, проверок целостности клиента и «сканеров памяти». Они не работают: всё, что запускается на клиенте, клиент и отключит. Roblox ловит модифицированные клиенты своими средствами на уровне платформы — это его работа, и в игру её не встроить. Твоя работа — сервер, которому нечего у тебя украсть.

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

Проверь себя#

  • Ни один канал не принимает от клиента число урона, цену, уровень или флаг прав
  • Каждый обработчик проверяет форму, право и возможность — и молча выходит при отказе
  • Неверный тип аргумента не роняет скрипт: обработчик продолжает работать для остальных
  • Проверка скорости считает по секундам, знает про твои телепорты и возвращает, а не кикает
  • Есть журнал, который читает человек, и нет автоматического бана
  • В ReplicatedStorage нет ничего, что помогло бы выиграть

Всё это — тот же принцип, что в админ-командах и магазине, только применённый ко всей игре сразу. Если он держится в каждом канале, отдельный «античит» твоей игре не нужен.

Реклама · игра в Telegram Moki Blast — шарики, боссы и дуэли в Telegram Надуй щёки Моки и собери три шара одного цвета Шесть миров: от руин до Золотого города Король обезьян ждёт на 120-м уровне Дуэли с живыми игроками и турниры Играть в Telegram

Обсуждение

Пиши по делу и без личных данных. Ссылки на сторонние сайты, обмен и «бесплатные робуксы» мы скрываем — почему.

Комментарии пишут те, кто вошёл — так спокойнее всем.

Войти

Пока никто ничего не написал. Будь первым.