Как сохранить обратную совместимость сохраненной игры?

8

У меня сложная сим-игра, в которую я хочу добавить функцию сохранения игры. Я буду обновлять его новыми функциями постоянно после релиза.

Как я могу убедиться, что мои обновления не нарушают существующие игры сохранения? Какой архитектуре я должен следовать, чтобы сделать это возможным?

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

Ответы:

9

Один из простых подходов - сохранить старые функции загрузки. Вам нужна только одна функция сохранения, которая записывает только последнюю версию. Функция загрузки определяет правильную функцию загрузки с поддержкой версий (обычно, записывая номер версии где-то в начале вашего файла сохранения). Что-то вроде:

class GameState:
  loadV1(stream):
    // do stuff

  loadV2(stream):
    // do different stuff

  loadV3(stream):
    // yet other stuff

  save(stream):
    // note this is version 3
    stream.write(3)
    // write V3 data

  load(stream):
    version = stream.read()
    if version == 1: loadV1(stream)
    else if version == 2: loadV2(stream)
    else if version == 3: loadV3(stream)

Вы можете сделать это для всего файла, для отдельных разделов файла, для отдельных игровых объектов / компонентов и т. Д. Именно то, что лучше всего разделить, будет зависеть от вашей игры и количества состояний, которые вы сериализуете.

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

Имейте в виду, что как только вы отправите свою игру, она «сделана». Вы можете выпускать DLC или другие обновления со временем, но они не будут особенно большими изменениями в самой игре. Возьмем, к примеру, большинство MMO: WoW уже много лет поддерживается новыми обновлениями и изменениями, но это все еще более или менее та же самая игра, в которой она была, когда она только появилась.

Для раннего развития я бы просто не беспокоился об этом. Сохранения эфемерны в раннем тестировании. Это другая история, когда вы попадете в публичную бета-версию.

Шон Миддледич
источник
1
Эта. К сожалению, это редко работает так же красиво, как рекламируется. Обычно эти функции загрузки полагаются на вспомогательные функции ( ReadCharacterмогут вызываться ReadStat, которые могут изменяться или не изменяться от одной версии к другой), поэтому вам нужно будет сохранять версии для каждой из них, что усложняет и усложняет их сопровождение. Как всегда, серебряной пули нет, и сохранение старых функций загрузки - хорошая отправная точка.
Панда Пижама
5

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

Скажем, у нас есть сериализованный объект, который выглядит так:

ObjectType
{
  m_name = "a string"
  m_size = { 1.2, 2.1 }
  m_someStruct = {
    m_deeperInteger = 5
    m_radians = 3.14
  }
}

Должно быть легко увидеть, что у типа ObjectTypeесть члены с данными m_name, m_sizeи m_someStruct. Если вы можете циклически перебирать или перечислять элементы данных во время выполнения (каким-либо образом), то при чтении этого файла вы можете прочитать имя члена и сопоставить его с фактическим членом в вашем экземпляре объекта.

На этом этапе поиска, если вы не нашли подходящий элемент данных, вы можете спокойно проигнорировать эту часть файла сохранения. Например, скажите, что у версии 1.0 SomeStructбыл m_nameчлен данных. Затем вы исправляете, и этот элемент данных был полностью удален. При загрузке файла сохранения вы m_nameнайдете подходящего участника и не найдете соответствия. Ваш код может просто перейти к следующему члену в файле без сбоев. Это позволяет вам удалять элементы данных, не беспокоясь о повреждении старых файлов сохранения.

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

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

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

RandyGaul
источник
Это будет полезно для повышения надежности данных и классов. (В .NET эта функция называется «отражение»). Я удивляюсь коллекциям ... мой ИИ сложен и использует много временных коллекций для обработки данных. Должен ли я попытаться избежать их сохранения ...? Возможно, ограничьте сохранение «безопасными точками», где обработка закончена.
Ржаной хлеб
@aman Если вы сохраняете коллекцию, то вы можете записать фактические данные в эти коллекции, как в моем исходном примере, за исключением «массива», как во многих из них подряд. Вы можете применить ту же идею к каждому отдельному элементу массива или любому другому контейнеру. Вам просто нужно написать некоторый универсальный «сериализатор массива», «сериализатор списка» и т. Д. Если вам нужен универсальный «сериализатор контейнера», вам, вероятно, понадобится какой-то реферат SerializingIterator, и этот итератор будет реализован для каждого типа контейнера.
RandyGaul
1
О, да, вы должны стараться избегать сохранения сложных коллекций с указателями как можно больше. Часто этого можно избежать, если тщательно продумать и продумать дизайн. Сериализация может быть очень сложной, поэтому стоит попытаться максимально упростить ее. @aman
RandyGaul
Существует также проблема десериализации объекта, когда класс изменился ... Я думаю, что .NET десериализатор во многих случаях аварийно завершает работу.
Ржаной хлеб
2

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

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

Однако иногда вам захочется изменить структуру файла. Скажите от текстового до бинарного; или из фиксированных полей в размер-значение. В таком случае вы, скорее всего, захотите заморозить исходный файл для чтения старых файлов и создать новый для нового типа файла, как в решении Шона. Убедитесь, что вы изолировали весь унаследованный читатель, иначе вы можете изменить то, что на него влияет. Я рекомендую это только для серьезных изменений файловой структуры.

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

Панда Пижама
источник
0

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

Пример может сделать это более понятным. Предположим, игра моделирует города, и эта версия 1.0 отслеживает общий уровень развития городов, в то время как версия 1.1 добавляет специфичные для Цивилизации здания. (Лично я предпочитаю отслеживать общее развитие, поскольку оно менее нереалистично; но я отступаю.) GuessNewValues ​​() для 1.1, с учетом файла сохранения 1.0, будет начинаться со старого показателя уровня развития, и на основе этого предположим, что в городе были бы построены здания - возможно, с учетом культуры города, его географического положения, центра его развития и тому подобного.

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

Что касается низкоуровневой стороны, я бы одобрил ответ Шона Миддлдича (за который я проголосовал): сохранить существующую логику загрузки, возможно, даже сохраняя старые версии соответствующих классов, и сначала вызвать это, затем конвертер.

ExOttoyuhr
источник
0

Я бы предложил использовать что-то вроде XML (если вы сохраняете очень маленькие файлы) таким образом, вам нужна только одна функция для обработки разметки независимо от того, что вы в нее вставили. Корневой узел этого документа может объявить версию, которая сохранила игру, и позволить вам написать код для обновления файла до последней версии, если это будет необходимо.

<save version="1">
  <player name="foo" score="10" />
  <data>![CDATA[lksdf9owelkjlkdfjdfgdfg]]</data>
</save>

Это также означает, что вы можете применить преобразование, если вы хотите преобразовать данные в «формат текущей версии» перед загрузкой данных, поэтому вместо того, чтобы располагать множеством версионных функций, вы просто получите набор файлов xsl, которые вы выбираете из сделать преобразование. Хотя это может занять много времени, если вы не знакомы с xsl.

Если ваши файлы сохранения имеют большой объем XML, это может быть проблемой, обычно я сохраняю файлы, которые работают очень хорошо, когда вы просто записываете пары ключ-значение в файл, как этот ...

version=1
player=foo
data=lksdf9owelkjlkdfjdfgdfg
score=10

Затем, когда вы читаете из этого файла, вы всегда пишете и читаете переменную одним и тем же способом. Если вам нужна новая переменная, вы создаете новую функцию для ее записи и чтения. вы могли бы просто написать функцию для типов переменных, чтобы у вас были «считыватель строк» ​​и «читатель int», это было бы просто замечательно, если бы вы меняли тип переменных между версиями, но вы никогда не должны этого делать, потому что переменная означает что-то другое в эта точка, поэтому вы должны создать новую переменную с другим именем.

Другой способ, конечно, использовать формат базы данных или что-то вроде CSV-файла, но это зависит от данных, которые вы сохраняете.

война
источник