Roblox GuidebookБаза знаний
Русский ⌄

Разработка / ROBLOX

Первое сохранение прогресса в Roblox: чтение, изменение и повторный вход

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

Обновлено:

Что именно должно пережить выход #

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

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

Отдельная игра для эксперимента #

Создай отдельный тестовый experience и опубликуй его с ограниченным доступом. Новый place внутри действующей игры не обеспечивает такого же разделения: хранилища доступны между places одного experience. Проверь в Creator Hub, какой именно проект открыт, прежде чем менять настройки.

Roblox описывает настройку доступа Studio к API в разделе Security окна Experience Settings. Используй её только в отдельной тестовой игре. Название окна или расположение пункта может меняться с интерфейсом Studio; сверяйся с текущей документацией. Не включай доступ ради быстрого эксперимента в проекте с настоящими сохранениями игроков.

Где должна работать логика #

Операции с DataStore выполняет серверный Script. Для учебного проекта размести его в ServerScriptService. LocalScript может показать состояние загрузки и результат игроку, но не должен сам назначать сохранённый счётчик или утверждать, что запись завершилась.

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

Имя хранилища и ключ игрока #

Выбери постоянное имя тестового хранилища и ключ на основе Player.UserId. Пример собственного соглашения: хранилище GuidebookOrdersTest_v1 и строка ключа User_ плюс идентификатор пользователя. Эти имена — предложение для упражнения, а не обязательный формат Roblox.

Не используй Display Name: разные люди могут иметь одинаковые отображаемые имена. Не меняй имя хранилища между первым и вторым запуском проверки: тогда ты будешь читать другую область и ошибочно решишь, что запись пропала. Запиши использованные имена в заметки теста вместе с названием experience.

Пустое значение и ошибка — разные состояния #

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

Заведи явное состояние: Loading, Ready или LoadFailed. Пока Loading, действия, меняющие прогресс, недоступны. При LoadFailed покажи понятное сообщение и не сохраняй ноль поверх неизвестных данных. Для первого упражнения достаточно остановить изменение прогресса и завершить тест; более сложный механизм повторов требует отдельного решения.

Результаты чтения и допустимое действиеОткрыть изображение крупнее ↗
Оригинальная схема: ошибка чтения не означает отсутствие записи.
ЭтапОжидаемое поведение
LoadingИзменение прогресса недоступно
ReadyРазрешённые сервером действия доступны
LoadFailedНе записывать начальный профиль

Обработка отказа без ложного успеха #

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

Состояние «попытка отправлена» отличается от «сохранение подтверждено». В Output оставляй отдельные диагностические сообщения для чтения, изменения и записи. В интерфейсе не показывай «сохранено», пока сервер не получил успешный результат. Не выводи весь профиль или личные данные в пользовательскую ошибку: для проверки достаточно этапа и короткого понятного объяснения.

Как выбрать способ изменения #

Для первого изучения полезно сравнить SetAsync и UpdateAsync. SetAsync записывает переданное значение, а UpdateAsync предлагает преобразование текущего значения. При одновременной записи с нескольких серверов простая установка старого локального значения может затереть другое изменение.

У UpdateAsync есть важное ограничение: callback не должен приостанавливаться, например через task.wait. Внутри преобразования нельзя выдавать внешнюю награду, рассчитывая, что оно всегда исполнится ровно один раз. Сначала опиши правило изменения данных; затем отдельно сообщай игроку подтверждённый результат. Подробную защиту конкурентных профилей вынесем в следующий материал.

Учебный код ниже помести в ModuleScript с именем StoreExperiment внутри ServerScriptService. Серверный Script получает тестовое хранилище через DataStoreService:GetDataStore("GuidebookOrdersTest_v1"), подключает модуль через require и создаёт адаптер вызовом StoreExperiment.new(store). Для чтения вызови adapter:Load(player.UserId). После проверки настоящего учебного действия сервером вызови adapter:CompleteTestOrder(player.UserId); это не обработчик кнопки и не автоматическая выдача за вход. Успех возвращает true и число, отказ — false и LoadFailed либо SaveFailed. Сравни результаты с таблицей. Пример допускает только целые значения от 0 до 1 000 000 и останавливается на пределе; лимит выбран для упражнения. Повторный отдельный вызов CompleteTestOrder увеличит значение ещё раз: защиты от повторной выдачи здесь нет. Код проверен локально в Luau с подставным хранилищем, без Studio и настоящих API.

-- ModuleScript: StoreExperiment (ServerScriptService).
-- Learning adapter only; no sessions, receipts, retries or production economy.
local StoreExperiment = {}

local function keyFor(userId)
    assert(type(userId) == "number" and userId > 0
        and userId < math.huge and userId == math.floor(userId), "Invalid UserId")
    return "User_" .. tostring(userId)
end

local function counter(value)
    if value == nil then return 0 end
    assert(type(value) == "number" and value >= 0 and value <= 1000000
        and value == math.floor(value), "Unexpected counter format")
    return value
end

function StoreExperiment.new(store)
    local adapter = {}

    function adapter:Load(userId)
        local key = keyFor(userId)
        local ok, result = pcall(function()
            return counter(store:GetAsync(key))
        end)
        if not ok then return false, "LoadFailed" end
        return true, result
    end

    function adapter:CompleteTestOrder(userId)
        -- Call only after the server verifies the exercise action.
        -- A failed read never authorizes a write of initial data.
        local loaded = self:Load(userId)
        if not loaded then return false, "LoadFailed" end
        local key = keyFor(userId)
        local ok, result = pcall(function()
            return store:UpdateAsync(key, function(current)
                local value = counter(current)
                assert(value < 1000000, "Exercise counter limit reached")
                return value + 1
            end)
        end)
        if not ok then return false, "SaveFailed" end
        return true, result
    end

    return adapter
end

return StoreExperiment

Что пока не входит в упражнение #

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

Не сохраняй после каждого кадра или каждого изменения текста на экране. Частоту записи выбирают с учётом ограничений сервиса и допустимой потери прогресса. В этом упражнении будем проверять одну осознанную запись и её чтение в новом сеансе. Это помогает понять механизм, но не является обещанием, что такой частоты достаточно любой игре.

Два последовательных входа #

Составь журнал проверки до запуска. В первом сеансе дождись успешной загрузки, выполни одно тестовое действие, получи подтверждение записи и запиши показанный счётчик. Затем заверши сеанс. Во втором запусти ту же тестовую игру тем же аккаунтом, с тем же хранилищем и ключом.

Сравни именно загруженное значение с ожидаемым. Не подменяй проверку сохранения перезапуском одного интерфейса внутри старого сеанса. Для дополнительных проверок учитывай, что GetAsync может использовать кэш: два чтения подряд не всегда доказывают независимое обращение к актуальному состоянию хранилища.

Путь проверки сохраненияОткрыть изображение крупнее ↗
Оригинальная схема: сравнить данные между сеансами.

Проверка ошибок и чужого прогресса #

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

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

ПроверкаРезультат для зачёта
Повторный входЗагружен подтверждённый счётчик
Другой игрокОтдельный ключ и значение
Ошибка чтенияНет записи пустых данных
Ошибка записиНет сообщения об успешном сохранении

Если после входа снова ноль #

Иди по цепочке: открыт ли тот experience, совпало ли имя хранилища, тот ли UserId вошёл, успешно ли завершилась предыдущая запись и не создала ли логика начальный профиль после отказа чтения. Смотри сообщения Output по порядку, а не только последнюю красную строку.

Не исправляй проблему массовым удалением данных. Сначала сохрани журнал теста и установи причину на отдельном ключе. Если упражнение случайно затронуло действующую игру, останови экспериментальные записи и разберись с доступными инструментами управления данными; простое повторение запуска может увеличить ущерб.

Когда первый этап закончен #

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

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

Первоисточники

Roblox Creator Hub — Data stores
Roblox Creator Hub — Best practices for data stores