Как обеспечить обратную совместимость при изменении данных сохранений в игре
Что проверяют
Думаете ли вы о сохранениях как о контракте с игроком, который переживёт десятки патчей. Следом обычно спрашивают, что будет, если игрок откатится на старую версию игры или файл повредится посреди записи.
Как отвечать
Каркас из четырёх частей.
Версия формата в каждом файле. Первое поле, например int SaveVersion, читается раньше всего остального. Это не версия игры, а версия схемы сохранения, и растёт она только при изменении формата.
Цепочка миграций. На каждый переход пишется маленький шаг v1 → v2, v2 → v3, и при загрузке файл последовательно проходит все шаги до текущей версии. Мигрировать удобно не типизированные классы, а промежуточное дерево вроде JObject из Newtonsoft.Json: тогда старые классы в коде держать не нужно. В Unity его подключают пакетом com.unity.nuget.newtonsoft-json, потому что JsonUtility не сериализует словари и даёт мало контроля над форматом.
Терпимый формат:
- новые поля: получают разумное значение по умолчанию, если в файле их нет;
- переименования: ключ в файле не меняется, старое имя закрепляют через
[JsonProperty("oldName")]; - enum: хранится строкой или с явно заданными числами, иначе вставка значения в середину сдвинет все остальные;
- ссылки на контент: предметы и уровни хранятся по стабильному строковому ID, а не по индексу в массиве, который перестроится в следующем патче.
Надёжность записи: пишите во временный файл и заменяйте основной через File.Replace или переименование, делайте резервную копию перед миграцией. Если файл новее, чем знает клиент, не пытайтесь его читать и честно сообщите об этом игроку.
Тесты: складывайте реальные сохранения каждой выпущенной версии в репозиторий и прогоняйте на них загрузку в CI.
Где спотыкаются
Берут BinaryFormatter. Он небезопасен, в современном .NET объявлен устаревшим и выключен, а формат привязан к именам типов, так что любой рефакторинг ломает сохранения.
Меняют смысл поля без смены версии, например переводят время из секунд в миллисекунды. Файл читается без ошибок, но данные уже неверные.
Мигрируют файл на месте без бэкапа. Ошибка в миграции превращается в потерянный прогресс, который не вернуть.