Разработка / 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 storesRoblox Creator Hub — Best practices for data stores