
Сохранение в играх — настолько привычная и базовая вещь, что многие воспринимают ее как нечто само собой разумеющееся и не особо хитрое. Открыли сундук, сменили локацию, вышли из дома персонажа, победили босса — и в углу экрана на долю секунды мигает иконка сохранения.
Кажется, что игра просто скинула файл на диск. На деле за эти доли секунды успевает отработать длинная цепочка: игровой движок превращает состояние виртуального мира в поток байт, операционная система решает, куда их писать, контроллер SSD выбирает физические ячейки памяти, а между делом происходят миллионы операций в процессоре. Чтобы понять, почему это так устроено, стоит сначала посмотреть, как система сохранений вообще появилась в играх — а потом уже разобрать современный конвейер сохранения, от объекта в памяти до заряда в транзисторе.

Как вы внедряете ИИ?
Поделитесь своим опытом за 7–10 минут. Разыгрываем 5 сертификатов по 1 500 ₽ на маркетплейсе и 10 наборов мерча.
Краткая история развития сохранения в играх
В самых первых играх сохранять было нечего: партия в Pong заканчивалась вместе с матчем, и все начиналось заново. Первым подобием «прогресса» стали рекорды в аркадных автоматах — сама игра оставалась одноразовой, но результат хотелось запомнить.

Здесь есть техническая деталь, которую часто упускают. Плата оригинального автомата Space Invaders построена на Intel 8080 с парой килобайт обычной статической памяти, без батарейки. Стоило выключить автомат, и рекорд обнулялся до стартовых 3 000 очков. Поэтому владельцы залов держали автоматы включенными сутками и неделями напролет: рекорд жил, пока не пропадало питание.
На уровне ассемблера 8080 все было предельно просто — под текущий счет и под рекорд отводились фиксированные ячейки памяти (значения хранились в BCD, по цифре на полубайт, чтобы проще было выводить их на экран), и после гибели персонажа одна короткая подпрограмма сравнивала два числа и при необходимости копировала новое значение поверх старого.
В начале 1980-х индустрия перешла на энергонезависимую память — ту же SRAM, но с припаянной литиевой батарейкой-таблеткой, которая держала напряжение на чипе, пока автомат выключен. Позже появились готовые модули вроде Dallas Semiconductor DS1225Y, где батарейка и память объединены в одном корпусе; в них хранили уже не только рекорды, но и настройки автомата, счетчики монет и прочую служебную информацию. К концу 80-х часть данных стала перекочевывать в EEPROM, которой батарейка вообще не нужна.
Возможность сохраняться дала играм то, чего раньше не было в принципе, — время. Раз прогресс можно было зафиксировать и вернуться к нему позже, появился смысл строить сюжет длиннее одного захода: The Legend of Zelda и Zork стали одними из первых игр, где сохранение превратилось в часть геймдизайна. Домашние ПК какое-то время обходились паролями — набором символов, который игрок переписывал на бумажку и вводил заново, — но довольно быстро перешли на кассеты и дискеты. Консольные картриджи получили встроенную RAM с батарейным питанием, и сохранение впервые стало физическим объектом: друзья могли принести картридж друг к другу и продолжить чужое прохождение.

В 90-х выросли и объемы, и продолжительность игр — средняя игра требовала для прохождения сюжета уже больше семи часов, Duke Nukem 3D — около девяти, Final Fantasy VII — порядка 60.
Сложились три модели сохранения:
через паузу и меню,
автоматически после события или квеста,
через точки сохранения — места, которые нужно было найти. Последний вариант заметнее всего повлиял на геймдизайн. То, как часто игрок возвращается к точке сохранения, насколько сложно ее обнаружить и как вокруг нее выстраивается управление ресурсами (лечение, патроны), стало отдельным инструментом сложности.
Sony довела идею переносимого сохранения до логического предела, выпустив карты памяти для PlayStation. Теперь между игроками переносился не картридж целиком, а только файл прогресса, и несколько человек в семье могли держать свои сохранения отдельно друг от друга.

Pokemon пошла еще дальше и сделала передачу данных через Game Boy Link Cable центральным игровым механизмом — коллекция монстров и статистика игрока путешествовали между картриджами напрямую, без карты памяти.
С приходом внутренних жестких дисков в Xbox 360, PlayStation 3 и Wii объем перестал быть проблемой в принципе — по оценкам, на 60-гигабайтный накопитель PS3 помещалось порядка 200 тысяч файлов сохранений. Освободившееся место позволило разработчикам перестать заставлять игрока думать о сохранении вообще: так появилось автосохранение в фоне, которое постепенно вытеснило ручные точки сохранения из большинства жанров.
Игровой мир не хранится как единое целое
Важно сразу отказаться от идеи «сохранить всю игру». Никакого единого объекта игрового мира в памяти не существует — есть тысячи независимых объектов. Например:
Player Enemy #42 Enemy #43 Chest #11 Tree #205 Weather Quest Manager NPC Database Physics Inventory
Каждый живет своей жизнью. У каждого десятки или сотни полей. Например, Player:
class Player { Vector3 position; int health; int stamina; int money; Inventory inventory; QuestLog quests; };
Игрок, десятки врагов, сотни деревьев, погода, менеджер квестов, база NPC, инвентарь — все это отдельные объекты, каждый со своим жизненным циклом. Во время игры таких объектов могут быть сотни тысяч.
Современная AAA-игра в зависимости от настроек запросто держит в оперативной памяти 8–20 ГБ, а файл сохранения весит от силы несколько мегабайт. Разница объясняется просто: почти все, что лежит в памяти во время игры, — это ресурсы, которые и так есть на диске в неизменном виде (текстуры, модели, звук, шейдеры, геометрия карт). Пересохранять их бессмысленно. Сохраняется только то, что изменилось относительно исходного состояния мира.
Условно, игра сохраняет не «меч» целиком со всеми текстурами и анимациями, а лишь сохраняет запись «игрок держит предмет с ID 271». Это по сути diff между исходным состоянием уровня и текущим, а не полный снимок памяти.

Игровой сервер с криперами и порталом в Незер.
Добывайте ресурсы, стройте объекты, исследуйте мир Selectel в Minecraft и получайте призы.
Сериализация
Дальше данные нужно превратить в последовательность байт, которую можно записать на диск и потом однозначно прочитать обратно. Это называется сериализацией. Например, в игре есть:
Player { Health = 95 Money = 850 }
На диске это превращается примерно в:
5F 00 00 00 ; Health = 95 (little-endian, int32) 52 03 00 00 ; Money = 850
Объектов, классов и указателей на этом уровне уже нет — только числа в заданном порядке байт. Открыв такой файл текстовым редактором, вы увидите нечитаемую кашу вроде ÏØaƲ╣È, потому что это не текст, а бинарные данные. Чтобы увидеть структуру, нужен hex-редактор (HxD, 010 Editor, Hex Workshop) — там уже видно повторяющиеся сигнатуры, заголовки и блоки фиксированной длины:
53 41 56 45 ; "SAVE" — сигнатура формата 01 00 00 00 ; версия формата = 1 E8 03 00 00 ; смещение до блока Player ...
Большинство движков хранят перед данными небольшой заголовок: магическую сигнатуру (чтобы отличить свой формат от чужого файла), версию формата (нужна при загрузке старого сохранения после патча игры) и иногда контрольную сумму — CRC32 или хэш вроде SHA-1 от содержимого файла, чтобы при загрузке проверить, что файл не побился. Именно по контрольной сумме игра часто определяет, что сохранение повреждено, еще до попытки его распарсить.
Почему игры почти никогда не перезаписывают файл сохранения напрямую
Дальше данные попадают на накопитель. Операционная система не указывает физический адрес — она обращается к SSD примерно так: «вот 64 КБ данных, запиши куда-нибудь». Дальше всем распоряжается контроллер накопителя и его слой трансляции адресов (Flash Translation Layer, FTL): он сам решает, в какие физические блоки писать, какие ячейки уже сильно изношены (флеш-память выдерживает ограниченное число циклов перезаписи) и какие данные пора перенести, чтобы равномерно распределить износ по чипу. Логический адрес файла и физический адрес ячейки — две разные вещи, и связь между ними знает только контроллер SSD, а не операционная система.
На физическом уровне запись — это изменение количества электронов в ячейках памяти с плавающим затвором или в их более современных аналогах на 3D NAND. То есть уменьшение здоровья персонажа со 100 до 95 в буквальном смысле оборачивается изменением электрического заряда внутри кремния.
При загрузке все повторяется в обратном порядке: SSD читает физические ячейки, FTL собирает из них логический файл, ОС отдает игре массив байт, а движок десериализует его обратно в объекты — восстанавливает игрока, инвентарь, список заданий. Если между сохранением файла и его загрузкой вышло обновление игры и формат сохранения изменился, здесь же обычно срабатывает миграция — код, который умеет читать старую версию формата (ту самую версию из заголовка файла) и доводить ее до актуальной структуры.
Как это выглядит в конкретных играх
Skyrim
Известная особенность движка Creation Engine — сохранения, которые за долгое прохождение разрастаются до сотен мегабайт, хотя размер видимого мира не меняется. Причина в скриптовой машине Papyrus: каждый активный скрипт, привязанный к объекту или квесту, хранит свое состояние прямо внутри сохранения.
Если скрипт не завершился корректно — например, из-за мода или бага — его состояние остается в файле навсегда и накапливается с каждым новым сохранением. Это тот случай, когда «сохранить только изменения» превращается в проблему. Изменений со временем накапливается больше, чем должно быть, потому что часть из них — мусор, который движок не смог вовремя убрать.

Minecraft
Мир хранится не единым файлом, а поблочно: карта разбита на чанки 16×16×384 блока, а чанки, в свою очередь, сгруппированы по 32×32 в region-файлы формата .mca. Каждый чанк сериализуется в формат NBT (Named Binary Tag — собственный бинарный формат Mojang, похожий по духу на JSON, но компактнее) и сжимается zlib отдельно от соседних чанков. Из-за этого игра может дозаписывать только те чанки, которые реально изменились с последнего автосохранения, не трогая остальной region-файл, — похожая логика на «сохранить diff», только примененная не к объектам, а к кускам карты.

Dark Souls
У серии принципиально другая философия: один-единственный слот сохранения, который перезаписывается почти постоянно — при любом значимом изменении состояния (подбор предмета, смерть и т. д.), а не только по команде игрока. Это осознанное дизайнерское решение против хитрых геймеров, которые используют сохранения для исправления допущенных ошибок: игрок не может «откатиться» на пару минут назад, перезагрузив старое сохранение, потому что старого сохранения уже физически не существует. Дополнительно файл сохранения защищен контрольной суммой, которую игра сверяет при запуске, — отчасти как защита от повреждения, отчасти как барьер против ручного редактирования файла в hex-редакторе.

Рогалики (Hades, Slay the Spire, FTL)
Здесь сохранение раздваивается на два принципиально разных куска данных. Первый — «прогрессия аккаунта»: разблокированные предметы, апгрейды, статистика, встреченные концовки. Он живет долго, копится от забега к забегу и хранится примерно так же, как обычное сохранение в любой другой игре.
Второй — «состояние текущего забега»: карта уровня, здоровье, инвентарь, случайно выпавшие предметы. Этот блок данных при смерти персонажа попросту удаляется, а не архивируется и «загружается заново с последней точки», как это было бы в линейной игре.
Технически смерть в рогалике — это не загрузка старого сохранения, а сброс части состояния в дефолтные значения при сохранении прогрессии аккаунта. Из-за этого в рогаликах физически невозможен классический save-scumming (то самое хитрое поведение со старыми сохранениями, описанное чуть выше): перезаписывать нечего, старой версии забега не существует ни на диске, ни во временных файлах.
Процедурная генерация
В играх с процедурно генерируемыми мирами — Minecraft, No Man's Sky, Terraria — возникает отдельная задача: сохранить не готовый уровень, а способ получить его заново. Основа мира в таких играх — числовой сид, из которого генератор детерминированно (то есть при одинаковом входе всегда выдавая одинаковый результат) строит рельеф, биомы, расположение ресурсов. Если бы игра каждый раз пересчитывала весь мир заново из сида, она вообще не занимала бы места на диске — но у игрока накапливаются изменения поверх сгенерированной основы: сломанные блоки, построенные дома, перенесенные с места на место предметы и т. д.
Поэтому реальная схема гибридная. Сид хранится один раз при создании мира, а дальше поверх него сохраняются только различия от того, что сгенерировал бы алгоритм сам по себе. В Minecraft это видно буквально по структуре region-файлов: если чанк ни разу не посещался игроком, он попросту не существует на диске — движок сгенерирует его на лету при первом приближении, используя сохраненный сид. А вот однажды посещенный и измененный чанк уже записывается на диск целиком, со всеми блоками, потому что для него дельта относительно исходной генерации становится слишком велика, чтобы хранить ее отдельно.

No Man's Sky пошла еще дальше. Галактика с триллионами планет физически не может быть сохранена целиком ни на каком накопителе, поэтому персистентными делаются только те немногие места, куда игрок реально успел вмешаться — построил базу, оставил маркер, изменил ландшафт. Все остальное при повторном визите генерируется заново из того же сида и по факту неотличимо от «сохраненного».
Save state в эмуляторах
Отдельный случай — save state, которым пользуются эмуляторы консолей и спидраннеры. Это не то же самое, что штатное сохранение игры, даже если внешне выглядит похоже.
Обычное сохранение — это, как разбиралось выше, узкий срез данных, который сама игра решает записать: позиция игрока, инвентарь, флаги квестов. Save state устроен грубее и универсальнее. Эмулятор просто дампит целиком всю память эмулируемой консоли, состояние регистров процессора, содержимое видеопамяти и звукового чипа — то есть буквально все, что нужно, чтобы через миллисекунду продолжить эмуляцию ровно с того же такта, на котором ее остановили. Игра при этом вообще не участвует в процессе и не знает, что ее «сохранили». С точки зрения игрового кода время просто не двигалось, пока файл save state лежал на диске.
Из-за этого save state работает даже в играх, которые официально сохранения не поддерживают вовсе (одноразовые аркадные порты, ранние NES-игры без батарейки в картридже), но обладает и обратной стороной: он жестко привязан не только к конкретной игре, но и к конкретной версии ядра эмулятора. Обновление эмулятора может изменить внутреннюю структуру памяти или порядок полей в дампе, и старый save state попросту перестанет загружаться. Именно поэтому спидраннеры и TAS-авторы обычно жестко фиксируют версию эмулятора для конкретного прохождения — иначе весь набор промежуточных save state теряет смысл.
Облачные сохранения и проблема двух «сохранений»
Как только сохранение перестало быть привязано к одному диску — Steam Cloud, Xbox Live, PS Plus, обычные сохранения мобильных игр через аккаунт — появилась новая проблема, которой не существовало во времена картриджей: у одного и того же прогресса стало два физических носителя одновременно, и они могут разойтись.

Механизм в общих чертах одинаков у всех платформ. При выходе из игры (или периодически на фоне) клиент сравнивает локальный файл сохранения с версией на сервере (обычно по временной метке последнего изменения, реже по хэшу содержимого). И, если облачная версия новее, подтягивает ее поверх локальной, и наоборот. Проблема возникает, когда обе версии изменились независимо друг от друга. Например, вы играли на ноутбуке без интернета, а потом запустили ту же игру на консоли, которая уже успела получить более старую версию сохранения из облака.
Формально ни одна из версий не «правильнее» другой — обе валидны с точки зрения формата, но описывают разное состояние прогресса. Именно в такой момент игра показывает диалог «Обнаружен конфликт сохранений, выберите, какую версию сохранить» — потому что автоматически безопасно объединить два расходящихся сохранения в общем случае невозможно. Слить, скажем, два разных набора собранных предметов в дереве прокачки можно попытаться, но слить два разных положения игрока в мире уже нельзя в принципе.
Часть игр обходит проблему тем, что вообще не пытается автоматически мержить конфликт, а хранит несколько версий сохранения с метками времени и отдает выбор игроку; часть — жестко привязывает сохранение к последнему устройству, где шла игра, жертвуя гибкостью ради предсказуемости.
Заключение
К моменту, когда надпись «Saving...» исчезает с экрана, успевает отработать вся цепочка целиком: движок собрал изменения по сотням объектов, сериализатор превратил их в поток байт, операционная система атомарно подменила старый файл новым, а контроллер SSD перераспределил электроны по кристаллу флеш-памяти. Каждое звено в этой цепочке рассчитано на то, что где-то посередине может пропасть питание, — и именно поэтому «Сохранение повреждено» на экране встречается куда реже, чем могло бы.
Vindicar
Самое важное так и не написали. Как игра сохраняет пресловутые "сотни тысяч" объектов, не останавливаясь на их полный перебор? Даже на то, чтобы вычислить дельту для каждого объекта (что изменилось относительно дефолтного состояния?), может уйти секунда для достаточно большого массива объектов. А это заметный лаг при сохранении - которого, тем не менее, часто удаётся избежать.
Ну и мелочи вроде того, как избежать поломанного сейва при внезапном падении программы в ходе сохранения. Для текущего ПО это работает просто - создали временный файл, потом потом поменяли местами его и основной файл сохранения. А как это делалось раньше?
unreal_undead2
Всегда было полезно чередовать несколько слотов сохранения, а не пользоваться одним.