GameMaker Studio 2. Урок 7. Сохранения и их загрузка. Бонус в конце!

Привет!
Сегодня поговорим о реализации сохранений в GMS, их разницу со встроенными сохранениями и немного затронем тему работы с файлами.

Ссылки на предыдущие гайды:

Первый гайд - Знакомство.

Второй гайд - События отрисовки, коллизия, скрипты.

Третий гайд - Камера и разрешение экрана.

Четвертый гайд - Иерархия объектов. Глобальные переменные.

Пятый гайд - Структуры данных. Сетка комнаты и размещение объектов по сетке.

Шестой гайд - Алгоритмы поиска путей.

Краткий план на сегодня. Интересна конкретная тема - поиск по странице вам в помощь.
- Теория. Как работают сохранения?
- Встроенный функционал GMS. Как и когда использовать? Плюсы и минусы.
- Реализация собственной системы сохранений.
- Бонус! 

Теория. Как работают сохранения?

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

В общем случае, сохранение представляет собой файл, в который, по определённой разработчиком системе, записана информация.
Самый простой пример - это текстовый файл, в котором содержится информация о параметрах дисплея пользователя, просто строчкой "1920х1080". Самое главное - что данная информация уже где-то есть и в ходе работы программы может изменяться.

После записи информации в файл, её необходимо считать и провести с ней какие-либо действия. Поэтому важна системность - хаотичный набор строк разбирать сложно. :)

Встроенный функционал GMS. Как и когда использовать? Плюсы и минусы.

В GMS'е имеется две функции с лаконичными названиями, которые вы можете использовать в своём проекте.
Сохранение игры:

game_save("Название_Файла.Формат") - где ".Формат" официальный мануал предлагает использовать .dat

Загрузка игры:

game_load("Название_Файла.Формат")

Соответственно, самая простая система загрузки сохранений, которую вы можете реализовать, выглядит следующим образом.
Перейдём в объект oGameManager и создадим у него два события: нажатие на F5 и нажатие на F6. Первое будет сохранять файл, второе - загружать.
Важно: везде добавим проверку, что мы находимся в основной комнате. В противном случае, будут вылетать ошибки при попытке загрузки из любой другой комнаты.

В событии нажатия F5:

if room == Room1

{

game_save("save.dat");

}

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

if file_exists("save.dat") and room == Room1
{
game_load("save.dat");
}

Всё. Самая простая система сохранений готова и работает. Как видно, нам хватило буквально четырёх строк кода (двух, если убрать проверки), чтобы её сделать.

Теперь поговорим о тонкостях работы.

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

Важно заметить, что данные сохранения перестают работать при любом обновлении кода. Стоит вам добавить одну строку кода (даже будь это вывод сообщения в консоль) - всё. Сохранение не работает. Таким образом, любые обновления игры будут ещё тем геморроем для ваших игроков и вас.

А ещё эта система сохранений в будущем, скорее всего, окончательно станет нерабочей.

Итак, из плюсов у нас только скорость установки. Минусы перечислены выше.
Как итог, скажу следующее: использовать данную систему следует на свой страх и риск. Если вы знаете, что ваша игра не нуждается в нормальных сохранениях и носит исключительно экспериментальный характер, то сойдёт и так. Если же вы игрой заняты серьёзно, то вам нужно будет создать свою систему сохранений.
Благо, что это просто! :)
И плюсов у неё много. Например, устойчивость к обновлениям как игры, так и движка ;)

Реализация собственной системы сохранений.

Для ЛЛ.
Общий алгоритм:
Сохранение -> Заносим всю информацию о каждом изменяемом объекте (в т.ч. о тайлах) комнаты в файл.
Загрузка:
- Если файл сохранения есть -> Удаляем все объекты из комнаты -> Читаем информацию из файла -> Создаём объекты заново и вносим в них правки.

Прежде, чем что-то писать, для начала изменим немного то, что уже есть.
Перейдём к объекту oGrid и создадим у него события: User Event 0, 1.

GameMaker Studio 2. Урок 7. Сохранения и их загрузка. Бонус в конце! Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия, Обучение, Видео, Без звука

Это, как понятно из названия, пользовательское событие. Мы можем его вызывать только тогда, когда нам нужно, используя event_user(0) (где 0 - это номер события).

Итак.
В событие №0 мы переносим весь код из Create, кроме объявления cells_x/y, а также заполнения тайлмапа и создания grid_array.
В событие №1 перенесём код, который у нас создаёт карту.

var lay_id_blocks = layer_get_id("BlockLayer");
var map_id_blocks = layer_tilemap_get_id(lay_id_blocks);
var lay_id_shadows = layer_get_id("ShadowLayer");
var map_id_shadows = layer_tilemap_get_id(lay_id_shadows);
for (var _y = 0; _y < cells_y; ++_y)
{
for (var _x = 0; _x < cells_x; ++_x)
{
ds_grid_set(global.grid, _x, _y, 0) // 0 - это стоимость клетки. Ноль - значит непроходима.
tilemap_set(map_id_blocks, Tile.ground, _x, _y)
tilemap_set(map_id_shadows, 47, _x, _y)
}
}
Да, всё правильно: grid_array нам больше не нужен. Его наличие будет создавать больше неудобств, чем пользы. Соответственно, нужно будет изменить все места, где он ранее использовался, удалив его. У меня это уже проделано, поэтому рекомендую в последствии скачать проект, если возникнут трудности. Затронуты, в основном, скрипты поиска пути и код у игрока.

P.S. Хоть от grid_array мы и избавились из-за того, что он больше не нужен, нам нужны некоторые формулы.
Получение ID клетки:
id = cell_x + cell_y * max_x;
Соответственно, клетки по x и по y можно получить следующим образом:
cell_x = id mod max_x;
cell_y = id div max_x;

max_x - максимальное количество клеток по оси x, можно получить с помощью функции ds_grid_width(global.grid);


Теперь перейдём в oGameManager. У нас там было событие Room Start, где мы активировали все инстансы. Его нам нужно будет дополнить.

if room == Room1

{

instance_activate_all()

if !instance_exists(oGrid)

{

var inst = instance_create_layer(0, 0, "Instances", oGrid)

inst.cells_x = 65;

inst.cells_y = 55;

with (inst)

{

event_user(0);

event_user(1);

}

}

}

Что мы сделали: если в комнате нет сетки, то мы создаём oGrid, устанавливаем свои значения клеток по x, y и запускаем событие создания сетки. Это нас убережёт от постоянного пересоздания сетки при заходе в комнату.

Наконец, приступаем к созданию скриптов.

Всего их два. scrSave и scrSaveLoad - для сохранения и для загрузки сохранений соответственно.

scrSave - скрипт сохранения
Алгоритм:
Создаём пустой массив. Затем - проходимся по всем изменяемым объектам, сохраняя информацию из них в виде структур в массив.
Затем переводим получившийся массив структур в формат json. Чтобы минимизировать расходы памяти, мы заносим эту информацию в буффер, сохраняем буффер в файл и удаляем буффер.

Кода много, поэтому ссылкой:
https://pastebin.com/YBYejqe0

scrSaveLoad - скрипт загрузки сохранения.
Алгоритм
:
Уничтожаем всё, что есть в нашей комнате.
Получаем айди слоев для тайлов.
Если существует файл сохранения, то загружаем его в буффер, "читаем" в переменную (читай - сохраняем), а буффер удаляем.
Помните, что сохраняли файл мы в json? Теперь мы его "читаем" с помощью команды json_parse. Таким образом, мы получаем массив структур, с которым теперь можем работать.
Проходимся по каждому элементу массива через цикл, проверяя тип объекта, создавая их при необходимости и применяя настройки при необходимости.

Оговорюсь, что в готовом скрипте у нас применяются настройки для oGameManager. Это временная мера, так как настройки игры должны лежать отдельно от файлов сохранения.

Ссылка:
https://pastebin.com/kCNT0zcW

Чтобы проверить работоспособность, на тех же F5 и F6 пропишите эти скрипты.

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

Бонус.

Не думали же вы, что на этом всё? Не, получается слишком просто.
Помните, что мы делали ранее систему персонажей? Давайте зададим подконтрольному персонажу спрайт (можете выбрать любой, в проекте будет шаблон) и научим его ходить!

Это будет скрипт для прямого управления человечком.
Кратко, алгоритм выглядит так.
На стороне игрока:
- Выделяем человечка.
- Нажимая на любую свободную клетку (или ту, что занята, но рядом имеет свободные), ставим конечную точку пути.
- Рассчитываем путь и передаём его человечку.
- Используя массив с точками, которые нужно пройти, создаём встроенный путь (path) и наполняем его, затем - запускаем. Profit!

Для начала, у oPlayer, в Step'е, где мы ранее создавали пути, всё немного допишем и перепишем. Когда устанавливаем первую точку пути, если у нас в этой же клетке есть подконтрольный персонаж - запоминаем его ID.
Когда будем устанавливать вторую точку пути - если ID персонажа существует, то передаём ему получившийся массив с точками пути, "перевернув" его. Нужно это, чтобы ближайшая к персонажу на данный момент точка шла "нулевой".

Скрипт на переворачивание массива.
https://pastebin.com/uL5MhpmX

Новый код для создания пути:
https://pastebin.com/dWJVuFuB
П.С. Переменную char нужно предварительно создать в Create.

Create у oCharPlayer:

path = noone;
bpath = noone;
spd = 4;
User Event(0) у oCharPlayer:
if path_exists(bpath)
{
path_delete(bpath)
}
bpath = path_add();
var max_x = ds_grid_width(global.grid);
path_set_closed(bpath, false);
for (var i = 0; i < array_length(path); ++i)
{
var path_x = (path[i] mod max_x) * CellWidth + 16;
var path_y = (path[i] div max_x) * CellWidth + 16;
// Стоимость пути. Выше стоимость клетки в DS grid = ниже скорость.
var cost = 100 div ds_grid_get(global.grid, path[i] mod max_x, path[i] div max_x);
path_add_point(bpath, path_x, path_y, cost);
}
path_start(bpath, spd, path_action_stop, false)
Вкратце: если путь уже существует, то мы его удаляем. Затем создаём новый.
Делаем путь "открытым". Соль в чём: "закрытый" путь = зацикленный, то есть достигнув конечной точки пути, наш человечек затем вернётся в самое его начало. Соответственно, открытый путь эту проблему решает.
Затем мы добавляем точки в путь. Важно, передаём мы их в виде координат комнаты, а не сетки. Как бонус - код выше учитывает стоимость клетки, поэтому "замедление" человечка уже реализовано.
Хотите сделать возможность ещё и ускорения - меняйте базовое значение пустой клетки.

Draw у oCharPlayer:
draw_self();
if path_exists(bpath)
{
draw_path(bpath, x, y, false)
}
Задача: просто отрисовываем путь в технических целях. :)

Step у oCharPlayer:
if path_exists(bpath)
{
if x == path_get_x(bpath, 1) and y == path_get_y(bpath, 1)
{
path_delete(bpath);
}
}
Задача: удалить путь, когда он нам больше не нужен.

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

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

P.S. юниты не считаются за "препятствие", а потому спокойно могут проходить сквозь друг друга.

Ссылка на исходник:
https://disk.yandex.ru/d/HjiC4fmpLvsYLw

Итак. Нам осталось разобрать только работу со звуком. В результате - готовая заготовка игры, которую остаётся дорабатывать напильником и активно наполнять.
Происходить это уже будет немного в другом формате, более коротком и информативном с технической точки зрения. С исходниками.

И да. В следующем гайде, скорее всего, без бонусов тоже не обойдётся. Я немного разленился, но постараюсь это исправить. :)

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

Спасибо всем, кто читал и удачи тем, кто будет пытаться повторить. Вы зайки и умницы!
Будут вопросы - задавайте, постараюсь на все ответить максимально подробно.
Есть пожелания или замечания - буду рад выслушать.

Лига Разработчиков Видеоигр

6.8K постов22.2K подписчиков

Добавить пост

Правила сообщества

ОБЩИЕ ПРАВИЛА:

- Уважайте чужой труд и используйте конструктивную критику

- Не занимайтесь саморекламой, пишите качественные и интересные посты

- Никакой политики


СТОИТ ПУБЛИКОВАТЬ:

- Посты о Вашей игре с историей её разработки и описанием полученного опыта

- Обучающие материалы, туториалы

- Интервью с опытными разработчиками

- Анонсы бесплатных мероприятий для разработчиков и истории их посещения;
- Ваши работы, если Вы художник/композитор и хотите поделиться ими на безвозмездной основе

НЕ СТОИТ ПУБЛИКОВАТЬ:

- Посты, содержащие только вопрос или просьбу помочь
- Посты, содержащие только идею игры

- Посты, единственная цель которых - набор команды для разработки игры

- Посты, не относящиеся к тематике сообщества

Подобные посты по решению администрации могут быть перемещены из сообщества в общую ленту.

ЗАПРЕЩЕНО:

- Публиковать бессодержательные посты с рекламой Вашего проекта (см. следующий пункт), а также все прочие посты, содержащие рекламу/рекламные интеграции

- Выдавать чужой труд за свой

Подобные посты будут перемещены из сообщества в общую ленту, а их авторы по решению администрации могут быть внесены в игнор-лист сообщества.


О РАЗМЕЩЕНИИ ССЫЛОК:

Ссылка на сторонний ресурс, связанный с игрой, допускается только при следующих условиях:

- Пост должен быть содержательным и интересным для пользователей, нести пользу для сообщества

- Ссылка должна размещаться непосредственно в начале или конце поста и только один раз

- Cсылка размещается в формате: "Страница игры в Steam: URL"