Для сохранения прогресса Roblox используйте DataStoreService только в серверном Script, ключ на основе Player.UserId, сетевые вызовы внутри pcall() и UpdateAsync() для записи. Загружайте данные при входе, сохраняйте при выходе и тестируйте на отдельной опубликованной версии.
DataStore хранит значения между игровыми сессиями и доступен всем places одной experience. Это подходит для монет, уровней, инвентаря и настроек. Вызовы идут по сети и иногда завершаются ошибкой, поэтому код обязан иметь ветку неудачи. LocalScript не может обращаться к DataStore. Сервер остаётся владельцем прогресса и не принимает готовый баланс от клиента.
User_ плюс UserId, чтобы имя не менялось вместе с ником.
UpdateAsync меняет актуальную версию значения через функцию.
pcall отделяет сетевой сбой от логики игры и сохраняет сообщение.

DataStore Roblox: код загрузки и сохранения прогресса
Пример хранит только число монет, поэтому его легко проверить. Script поместите в ServerScriptService. Перед тестом опубликуйте отдельную тестовую experience и разрешите Studio Access to API Services именно для неё. Не включайте Studio-доступ к боевым данным: случайный тест может перезаписать настоящий профиль.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local coinsStore = DataStoreService:GetDataStore("Coins_v1")
local function getKey(player)
return "User_" .. player.UserId
end
local function savePlayer(player)
local coins = player:GetAttribute("Coins") or 0
local success, message = pcall(function()
coinsStore:UpdateAsync(getKey(player), function()
return coins
end)
end)
if not success then
warn("Save failed for", player.UserId, message)
end
end
Players.PlayerAdded:Connect(function(player)
local success, savedCoins = pcall(function()
return coinsStore:GetAsync(getKey(player))
end)
player:SetAttribute("Coins", success and savedCoins or 0)
end)
Players.PlayerRemoving:Connect(savePlayer)- Первый вход: Coins равен 0
- Следующий вход: загружается сохранённое число
Это учебная основа, а не полный профиль игрока. В реальном проекте нужен контроль повторной загрузки, автосохранение, обработка закрытия сервера, схема данных и стратегия повторных попыток. Если загрузка не удалась, нельзя бездумно разрешать игроку продолжить с нулём и потом перезаписать существующий профиль. Для ценных данных используйте отдельное состояние загрузки и блокируйте сохранение неуспешно загруженного профиля.
DataStore, MemoryStore или Attributes
| Элемент | Что означает | Что делать |
|---|---|---|
| DataStore | Данные между сессиями | Монеты, инвентарь, уровень |
| OrderedDataStore | Числа с сортировкой | Глобальный рейтинг |
| MemoryStore | Временные быстрые данные | Очередь матчмейкинга, кэш |
| Attributes | Текущее состояние Instance | Монеты в активной сессии |
| Обычная таблица Luau | Память одного сервера | Временный профиль до сохранения |
Не сохраняйте данные после каждого подобранного предмета. Запись имеет лимиты, а UpdateAsync расходует бюджет чтения и записи. Меняйте профиль в памяти, запускайте разумное автосохранение и делайте финальную попытку при выходе. Для одной горячей записи со множеством серверов продумайте конкуренцию и не храните весь мир под одним ключом.

Практика: сохраните Coins через UpdateAsync и проверьте полный цикл
Соберите минимальный профиль из одного числа: монеты игрока в хранилище Coins_v1. Скрипт кладите в ServerScriptService и тестируйте на отдельной опубликованной experience, где включён Studio Access to API Services. Успех: вы вышли из сессии с 25 монетами, зашли снова и увидели те же 25, а не ноль.
- Script в ServerScriptService. Создайте обычный Script в ServerScriptService и получите хранилище через DataStoreService:GetDataStore с именем Coins_v1. LocalScript тут не подойдёт: клиенту доступ к DataStore закрыт.
- Ключ на основе UserId. Напишите функцию getKey(player), которая возвращает User_ плюс player.UserId. Выведите ключ через print и убедитесь, что он не зависит от отображаемого имени игрока.
- Загрузка в PlayerAdded. В обработчике Players.PlayerAdded оберните GetAsync(getKey(player)) в pcall и положите результат в player:SetAttribute с именем Coins. При первом входе в Explorer у игрока появится атрибут Coins со значением 0.
- Запись в PlayerRemoving. Поставьте Coins равным 25 прямо в свойствах игрока и повесьте savePlayer на Players.PlayerRemoving, где UpdateAsync возвращает текущее число. Остановите тест и проверьте, что в Output нет предупреждения Save failed for.
- Второй вход и сверка. Запустите сессию заново и посмотрите на атрибут Coins сразу после входа. Там должно быть сохранённое 25, а не ноль: значит, цикл чтения и записи замкнулся.
Теперь сломайте ветку отказа специально: временно выключите Studio Access to API Services или передайте в UpdateAsync значение, которое хранилище не умеет сериализовать, например таблицу с функцией внутри. pcall вернёт success равным false, и в Output появится ваш warn с UserId и текстом ошибки. Дальше главное: убедитесь, что после неудачного GetAsync игра не подставила ноль и не записала его поверх настоящих 25. Верните доступ, повторите вход и проверьте, что число на месте.
Дальше замените одно число таблицей профиля с полями для монет, уровня и версии данных, чтобы позже можно было мигрировать формат. Добавьте автосохранение по таймеру и последнюю попытку записи при закрытии сервера, но не пишите после каждой подобранной монеты: у записи есть лимиты, а UpdateAsync расходует бюджет чтения и записи. Для глобального рейтинга заведите отдельный OrderedDataStore, а текущие монеты покажите игроку через leaderstats. Кнопку покупки делайте через RemoteEvent, где цену проверяет сервер.
Частые ошибки и почему они появляются
Потери данных чаще возникают не из-за одной опечатки, а из-за неверного сценария отказа. Игрок вошёл, GetAsync вернул ошибку, игра поставила ноль, а при выходе сохранила его поверх старого значения. Другая проблема появляется, когда два сервера меняют один профиль без контроля версии. Третья связана с тестами прямо на боевом хранилище.
Ник может измениться. UserId остаётся стабильным идентификатором игрока.
Клиентский доступ запрещён и небезопасен. Все чтения и записи выполняет сервер.
После pcall всегда проверяйте флаг и решайте, можно ли продолжать, повторять или остановить сохранение.
Самопроверка
Зачем оборачивать GetAsync и UpdateAsync в pcall?
pcall нужен потому, что DataStore ходит по сети и вызов иногда завершается ошибкой без вины вашего кода. Без pcall скрипт остановится прямо на этой строке, а с ним вы получите флаг success и сообщение и сами решите: повторить попытку, отложить её или запретить сохранение.
Почему ключ строят на UserId, а не на имени игрока?
Ключ строят на UserId, потому что этот номер закреплён за аккаунтом и не меняется. Ник и отображаемое имя игрок может сменить в любой момент, и ключ по Name после переименования укажет на пустой профиль, а запись вида User_ плюс UserId найдёт те же данные.
Можно ли обращаться к DataStore из LocalScript?
Из LocalScript обращаться к DataStore нельзя: доступ есть только у серверного кода. Прогресс принадлежит серверу, и он же решает, можно ли изменить баланс, иначе клиент сможет прислать себе любое число монет.
Почему прогресс игрока обнулился после ошибки загрузки?
Обнуление обычно происходит по такой цепочке: GetAsync вернул ошибку, скрипт молча подставил 0, а при выходе сохранил этот ноль поверх настоящего профиля. Лечится отдельным состоянием загрузки: пока данные не прочитаны успешно, сохранение для этого игрока запрещено.
Как часто можно сохранять данные в DataStore?
Сохранять после каждого подобранного предмета не нужно: у записи есть лимиты, а каждый UpdateAsync тратит бюджет чтения и записи. Держите профиль в памяти сервера, запускайте автосохранение по таймеру и делайте финальную запись при выходе игрока и закрытии сервера.
Чем DataStore отличается от Attributes и MemoryStore?
DataStore хранит данные между сессиями и доступен всем places одной experience: монеты, инвентарь, уровень. Attributes держат текущее состояние Instance только внутри активной сессии, MemoryStore подходит для быстрых временных данных вроде очереди матчмейкинга, а числа с сортировкой для глобального рейтинга ведут в OrderedDataStore.
Что делать дальше
Сначала сохраните одно число и проверьте полный цикл: вход, изменение, выход, новый сервер. Затем добавьте таблицу профиля и поле версии. Логику таблиц и функций отработайте на курсе Lua. Для отображения монет подключите leaderstats, а для кнопки покупки используйте безопасный RemoteEvent, где сервер сам проверяет цену и меняет профиль.
