Система сохранения и загрузки для игры на Arcade (Python)

Добавление системы сохранения и загрузки в игру значительно улучшает опыт игрока. Она позволяет сохранять прогресс, возобновлять сессии и экспериментировать с разными стратегиями, не теряя достижений. С помощью Arcade это сделать проще, чем кажется.
Что мы создаём
В примерах ниже мы реализуем простую игру, где игрок — синий прямоугольник — двигается влево и вправо. Затем добавим класс состояния, методы сохранения/загрузки в формате JSON, автосохранение, хранение рекордов и проверку данных.
Код в статье можно адаптировать под вашу игру. Все примеры показаны на Python 3 и библиотеке arcade.
Быстрая демонстрация: простая игра
Создайте файл simple-game.py и добавьте следующий код. Этот код создаёт окно, рисует игрока и позволяет двигать его стрелками влево/вправо.
import arcade
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
PLAYER_SPEED = 5
BLUE = arcade.color.BLUE
class GameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
# Позиция игрока по X
self.player_x = width // 2
def on_draw(self):
arcade.start_render()
arcade.draw_rectangle_filled(self.player_x, 50, 50, 50, BLUE)
def update(self, delta_time):
pass
def on_key_press(self, key, modifiers):
if key == arcade.key.LEFT:
self.player_x -= PLAYER_SPEED
elif key == arcade.key.RIGHT:
self.player_x += PLAYER_SPEED
def main():
window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT)
arcade.run()
if __name__ == '__main__':
main()Результат: окно с синим прямоугольником, который вы двигаете стрелками.
Управление состоянием игры
Состояние игры — это данные, которые нужно сохранить (позиции, очки, уровни и т.д.). В простом примере это только координата игрока по X. Вынесем эти данные в отдельный класс GameState.
class GameState:
def __init__(self):
self.player_x = 0Вынос состояния в отдельный объект упрощает сериализацию, тестирование и расширение набора сохраняемых полей.
Сохранение данных игры
Добавим метод save_game в класс окна. Для простоты используем JSON и файл save.json. Метод собирает словарь с нужными полями и сериализует его.
import json
class GameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
self.game_state = GameState()
def save_game(self):
data = {
'player_x': self.game_state.player_x
}
with open('save.json', 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=2)
print('Saved:', data)
def on_draw(self):
arcade.start_render()
arcade.draw_rectangle_filled(self.game_state.player_x, 50, 50, 50, BLUE)
def update(self, delta_time):
pass
def on_key_press(self, key, modifiers):
if key == arcade.key.LEFT:
self.game_state.player_x -= PLAYER_SPEED
elif key == arcade.key.RIGHT:
self.game_state.player_x += PLAYER_SPEED
elif key == arcade.key.S:
self.save_game()Нажмите клавишу S, чтобы сохранить текущую позицию игрока в save.json.
Загрузка данных игры
Добавим метод load_game, который читает save.json и восстанавливает состояние. Если файла нет, метод аккуратно игнорирует ошибку.
class GameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
self.game_state = GameState()
self.load_game()
def load_game(self):
try:
with open('save.json', 'r', encoding='utf-8') as file:
data = json.load(file)
self.game_state.player_x = data.get('player_x', self.game_state.player_x)
print('Loaded:', data)
except FileNotFoundError:
# Файл отсутствует — продолжаем с дефолтным состоянием
pass
def on_key_press(self, key, modifiers):
if key == arcade.key.L:
self.load_game()Теперь можно нажать L, чтобы загрузить состояние из файла.
Дополнительные возможности
Ниже — часто используемые расширения системы сохранения.
Сохранение рекордов
Полезно хранить рекорды вместе с состоянием. Добавим поле high_score и сохраним его вместе с координатой.
class GameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
self.player_x = width // 2
self.high_score = 0
def load_game(self):
try:
with open('save.json', 'r', encoding='utf-8') as file:
data = json.load(file)
print('Loaded:', data)
self.player_x = data.get('player_x', self.player_x)
self.high_score = data.get('high_score', self.high_score)
except FileNotFoundError:
pass
def save_game(self):
data = {
'player_x': self.player_x,
'high_score': self.high_score
}
with open('save.json', 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=2)
print('Saved:', data)
def on_key_press(self, key, modifiers):
if key == arcade.key.LEFT:
self.player_x -= PLAYER_SPEED
elif key == arcade.key.RIGHT:
self.player_x += PLAYER_SPEED
# Пример обновления рекорда
self.high_score = max(self.high_score, int(self.player_x))Пример выше показывает простую логику для демонстрации. В реальной игре рекорд рассчитывается иначе.
Автосохранение
Автосохранение снижает риск потери прогресса. Метод update проверяет прошедшее время и вызывает save_game.
import time
class GameWindow(arcade.Window):
def __init__(self, width, height):
super().__init__(width, height)
self.game_state = GameState()
# Сохранять каждые 6 секунд
self.autosave_interval = 6
self.last_save_time = time.time()
def update(self, delta_time):
current_time = time.time()
time_diff = current_time - self.last_save_time
if time_diff >= self.autosave_interval:
self.save_game()
print('Autosaved')
self.last_save_time = current_timeНастройте интервал автосохранения под ваш тип игры.
Валидация данных
Важно проверять загруженные данные, чтобы избежать падений и читинга. Включим простые проверки в класс GameState.
class GameState:
def __init__(self):
self.player_x = 0
def save_state(self):
if self.is_valid_state():
data = {'player_x': self.player_x}
with open('save.json', 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=2)
def load_state(self):
with open('save.json', 'r', encoding='utf-8') as file:
data = json.load(file)
if self.validate_loaded_data(data):
self.player_x = data['player_x']
else:
print('Ошибка: некорректные данные')
def is_valid_state(self):
# Пример: позиция игрока должна быть числом
return isinstance(self.player_x, (int, float))
def validate_loaded_data(self, data):
if not isinstance(data, dict):
return False
px = data.get('player_x')
return isinstance(px, (int, float))Валидация должна учитывать диапазоны, типы и логические зависимости (например, нельзя восстановить уровень, который ещё не открыт).
Лучшие практики
- Шаблон сериализации: держите отдельный слой для преобразования состояния в JSON и обратно.
- Версионирование: добавляйте поле save_version в файл, чтобы поддерживать обратную совместимость.
- Бэкапы: перед перезаписью файла сохраняйте старую версию как save.json.bak.
- Защита от одновременного доступа: в многопоточной игре используйте блокировки при записи.
- Юнит-тесты: тестируйте save/load для каждого поля.
Шифрование чувствительных данных
Если сохраняете личные данные или покупки — шифруйте файл. Используйте проверенные библиотеки (например, cryptography) и храните ключи безопасно. Не изобретайте собственный криптографический протокол.
Обработка ошибок
При операциях ввода-вывода ловите FileNotFoundError, PermissionError и JSONDecodeError. Информируйте игрока дружелюбным сообщением и логируйте подробности для разработчика.
Тестирование
Покройте сценарии: сохранение/загрузка в разных состояниях, загрузка повреждённого файла, миграция версий, автосохранение при прерывании приложения.
Критерии приёмки
- При сохранении создаётся файл save.json и содержит ожидаемые поля.
- При загрузке состояние точно восстанавливается для тестовых случаев.
- При отсутствии файла загрузка не вызывает исключений.
- При повреждённом JSON приложение продолжает работать и показывает понятное сообщение.
- Автосохранение запускается через заданный интервал.
Тестовые случаи и приёмочные сценарии
- Сохранение базового состояния
- Действие: установить player_x = 123, нажать S.
- Ожидаемо: в save.json поле player_x: 123.
- Загрузка существующего файла
- Действие: нажать L при наличии корректного save.json.
- Ожидаемо: позиция игрока обновлена, в консоли вывод Loaded.
- Отсутствие файла
- Действие: удалить save.json, запустить загрузку.
- Ожидаемо: приложение не падает, состояние остаётся по умолчанию.
- Повреждённый файл
- Действие: записать в save.json текст “not json” и выполнить загрузку.
- Ожидаемо: ловится JSONDecodeError, выводится сообщение об ошибке.
- Автосохранение
- Действие: запустить игру и подождать интервал автосохранения.
- Ожидаемо: файл save.json обновляется автоматически.
Альтернативные подходы
- Бинарные файлы: быстрее и компактнее, но менее удобны для отладки. Подходит для больших состояний.
- SQLite: удобно, если вам нужно хранить множество слотов сохранений или метаданные (даты, пользовательские профили).
- Серверное хранение: сохраняйте прогресс в облаке для кросс-платформенной синхронизации. Учитывайте безопасность и приватность.
Когда простое решение не подходит
- Если у вас много объектов и сложная сцена, flat JSON станет громоздким. Используйте базы данных или бинарную сериализацию.
- Если важна защита от читинга (мультиплеер с прогрессом), местные файлы легко подделать — нужен серверный контроль.
Роль‑ориентированные чек-листы
Для разработчика:
- Отделить модель состояния от UI и логики.
- Добавить версионирование формата сохранения.
- Реализовать бэкап перед перезаписью.
- Написать юнит-тесты для сериализации и валидации.
Для QA:
- Проверить сценарии из раздела тестовые случаи.
- Сымитировать потерю доступа к файлам (PermissionError).
- Проверить поведение при обновлении формата (миграция).
Mini-методология внедрения
- Выделите GameState и интерфейсы save/load.
- Реализуйте базовый JSON с минимальным набором полей.
- Добавьте ручные клавиши для проверки (S/L).
- Напишите тесты для save/load.
- Добавьте автосохранение и бэкапы.
- Введите версионирование и миграцию при изменениях схемы.
Совместимость и миграция
- Добавляйте поле save_version: 1, 2 и т.д.
- При загрузке выполняйте проверку версии и, при необходимости, функцию миграции, которая преобразует старый формат в новый.
- Храните старые файлы как .bak перед изменением формата.
Пример простой миграции
def migrate(data):
version = data.get('save_version', 1)
if version == 1:
# пример трансформации из версии 1 в 2
data['save_version'] = 2
data['new_field'] = default_value
return dataПриватность и GDPR
- Не сохраняйте персональные данные без согласия.
- Если сохраняете email или идентификаторы, обеспечьте шифрование и доступ только по необходимости.
- Позволяйте пользователю удалять локальные профили и связанные данные.
Риски и способы смягчения
- Риск: потеря прогресса. Смягчение: автосохранение и бэкапы.
- Риск: повреждённый файл. Смягчение: валидация и откат на .bak.
- Риск: читинг. Смягчение: хранить критичные проверки на сервере.
Краткое резюме
Система сохранения и загрузки повышает удобство игры и удержание игроков. Для простых проектов достаточно JSON-файлов и небольшой модели состояния. Для более сложных проектов используйте базы данных, версионирование, шифрование и серверную синхронизацию. Всегда добавляйте валидацию, обработку ошибок и тесты.
Внедряя описанные практики, вы получите надёжную и расширяемую систему сохранения, которую легко адаптировать к росту проекта.
Важно: начинайте с простой реализации, а затем ступенчато добавляйте функции (рекорды, автосохранение, шифрование, миграции). Это уменьшает риск ошибок и упрощает тестирование.
Похожие материалы
10 известных фотографов в Instagram для вдохновения
MetaMask — как начать и обезопасить кошелёк
Установка обновлений Windows 11 — 4 способа
Как блокировать СМС на Android
Как поменять обои на Chromebook