Система сохранения/загрузки состояний [статья]
Доброго всем времени суток. Давненько я ничего не писал. Последняя моя статья вышла очень давно, и мы там остановились на довольно расплывчатой тематике. Однако сегодня я хотел бы поговорить с вами про систему сохранений. Да, я знаю, что она уже внедрена в движок и работает. Но, если рассматривать ее в рамках чего-то большего, то ненароком ловишь себя на мысли: А можно-ли наладить процесс сохранения для, скажем, ролевой игры, в которой можно создавать несколько персонажей?
Да, можно, и qsp предоставляет такие возможности. Всё зависит от степени вашей втянутости и желания поломать голову. Если у вас уже есть опыт в работе с движком, то вы и так прекрасно знаете, как реализовать возможность сохранения нескольких персонажей. Если же нет, добро пожаловать в мой длиннопост, и сегодня мы будем разбираться в том, как же сделать эту заковыристую систему сохранений.
Не знаю как вы, а я задумался об этом ещё пару лет назад. Реализовал это дело и отложил в долгий ящик. Сейчас данная система используется в “Sanctuary” и парочке других демок. Конечно, сейчас бы вспоминать старые наработки и думать, зачем всё это было сделано! На волне конкурса QSPCOMPO 2020 и вспоминания игры “Гринд”, я вдруг решил, а почему бы самому не создать простую гриндилку на qsp? Задача не слишком сложная, однако требует серьезного подхода и обдумывания механик. Одной из таких механик стала система сохранения/загрузки состояния. Грубо говоря, у игрока есть хаб, в котором находятся все созданные персонажи. Ему не нужно беспокоится о сохранении, поскольку игра всё сохраняет сама и позволяет обращаться к любому персонажу без лишних поисков сохраненных файлов. У персонажей есть не только свои ячейки для сохранения данных, но и общий сундук, в который они могут складывать свои вещи и делиться друг с другом находками. Это довольно полезно, если мы говорим о гринд-игре. Неплохо иметь несколько персонажей разных классов и специализаций, чтобы быстрее получить требуемые предметы.
Встает один интересный вопрос: А как сделать так, чтобы сохранять сразу несколько персонажей, но при этом активным будет являться только подконтрольный на данный момент персонаж? Давайте разбираться вместе.
(Если у кого-то есть свои предложения, по реализации данной механики, буду рад услышать ваше мнение. Если же вы не видите смысла в данных действиях, тогда зачем вы это читаете?)
Итак, давайте начнём с простых вещей. У нас есть персонаж, которого вы (со всей любовью и старанием) создали. У него есть своё имя, свои характеристики типа силы и интеллекта. У него также есть навыки и параметры жизни. Всё это дело у нас хранится в переменных (лично я храню это в массивах и вам советую).
К примеру:
$dataPlayer['Name'] = 'Svartberg'
dataPlayer['STR'] = 10
dataPlayer['INT'] = 11
dataPlayer['HP.all'] = 100
dataPlayer['skill.Sword'] = 12
dataPlayer['skill.Witch'] = 13
Отлично, у нас есть данные, которыми можно пользоваться во время игры. Это всё прекрасно сохранится. А теперь я хочу, создать нового персонажа и некоторое время поиграть им. Что мне делать? При создании нового персонажа данные сотрутся. Да, мы можем сделать что-то типа этого:
$dataPlayer['1.NAME'] = 'Svartberg'
dataPlayer['1.STR'] = 10
$dataPlayer['2.NAME'] = 'Device'
dataPlayer['2.STR'] = 11
Теперь у нас есть два персонажа, но вот обращаться к ним стало немного труднее. У каждого появился свой порядковый номер. Да и в принципе, в реалиях ролевой игры, может понадобится достаточно много ячеек в массиве dataPlayer. И самих персонажей может стать куда больше. Не хочется раздувать из мухи слона.
Сейчас мы немного отойдем в сторону от механики сохранения и загрузки и поговорим про хранение данных в массивах. Поскольку у нас может быть достаточно большое количество данных, вопросы их хранения встают ребром. Нужно не забывать о том, что хранить неиспользуемые данные — не очень верное решение. Игрок может захотеть удалить персонажа и что тогда? Прописывать удаление для каждой ячейки dataPlayer конкретного персонажа? Это глупо и неудобно. Гораздо проще хранить данные персонажа блоками. Вызывать их при надобности и с легкостью удалять при ненадобности. В этом нам помогут индексы, индивидуальные номера персонажей и маркеры. Зачем и как они нам помогут, будем сейчас разбираться.
Давайте вернемся к первому примеру и немного дополним его:
$dataPlayer['ID'] = '83Mt0r'
$dataPlayer['NAME'] = 'Svartberg'
dataPlayer['STR'] = 10
....
$dataPerson[] = '[ID]<<$dataPlayer[''ID'']>>[/ID][NAME]<<$dataPlayer[''NAME'']>>[/NAME][STR]<<dataPlayer[''STR'']>>[/STR]....'
Что мы можем увидеть в данном примере? У персонажа появился новый параметр — ID (идентификатор), уникальный параметр, который может быть только у данного персонажа. Также мы записали все данные персонажа в индексируемый массив, сохранив его в новый слот. Поскольку это первый персонаж, его индекс приравнивается к 0. В массиве dataPerson в ячейке 0 теперь хранятся данные нашего персонажа. В самой ячейке находятся маркеры, которые позволят нам обращаться к конкретному параметру. К примеру, [ID] и * являются маркерами для параметра *ID.
Довольно просто. Мы сохраняем состояние персонажа в ячейку для дальнейшего взаимодействия.
А теперь вернёмся к нашей теме и подумаем над тем как нам реализовать механику сохранения персонажа (не важно, будет ли это сразу после создания персонажа или вызова конкретного действия сохранения).
Представим ситуацию, когда нам нужно сохранить только что созданного персонажа. Все его данные сейчас сохранены в массиве dataPlayer. Их достаточно много. Для начала нам нужно не забыть проверить вероятность того, что сгенерированный идентификатор персонажа не попадается в общем массиве персонажей, чтобы избежать ошибок. Для этого достаточно функции ARRCOMP. Данная функция возвращает индекс элемента массива с использованием регулярных выражений (более подробную информацию о работе функции вы сможете получить в справочнике).
SAVE.id = ARRCOMP(0,'dataPerson','.*' + $dataPlayer['ID'] + '.*')
IF SAVE.id = -1: SAVE.id = ARRSIZE('$dataPerson')
Если функция вернет значение -1, то всё в порядке, искомого идентификатора нет в массиве. Можно назначить переменной SAVE.id свободный индекс в массиве dataPerson. Если же SAVE.id изначально вернет индекс элемента, значит нам необходимо будет сгенерировать новый идентификатор.
P.S. Позволю себе некоторые допущения и предположу, что вы уже знаете, как генерировать текст. Есть лишь небольшой совет, используйте длину генерации около 6-8 букв. Это в разы уменьшит вероятность выпадения одинакового текста. Я использую 8-значный идентификатор для персонажей игрока, 10-значный для NPC и 12-значный для предметов.
Отлично, теперь, когда мы убедились, что данного ID ещё нет в массиве, мы можем приступить к сохранению данных. Для упрощенного обращения к системе загрузки (о которой позже) добавим наш ID в отдельный массив.
$SAVE.data[] = $dataPlayer['ID']
В массиве SAVE.data у нас будут храниться только ID персонажей.
Теперь нам стоит составить маркеры для генератора сохранения. Делаются они достаточно просто:
KILLVAR '$SAVE.mark' & !Очищаем массив SAVE.mark от ранее записанных данных, если они были записаны.
$SAVE.mark[] = '$ID' & $SAVE.mark[] = '$NAME' & $SAVE.mark[] = 'STR' & $SAVE.mark[] = 'INT'
$SAVE.mark[] = 'HP.all' & $SAVE.mark[] = 'MP.all' & $SAVE.mark[] = 'XP.all'
....
Чтож, с этим справились, не забывайте, что писать названия для маркеров необходимо точно так же, как названы ячейки в массиве dataPlayer, иначе ничего не сохранится. Теперь подготовим простенький цикл для переноса данных из dataPlayer в dataPerson. Для того, чтобы у нас не было проблем с определением типа переменной, при указании маркера, ставьте метку типа данных. В данном случае, в месте, где участвует текстовый тип данных указана метка ‘$’. Она поможет распределить данные по условию.
:plCHAR.save
IF A < ARRSIZE('$SAVE.mark'):
IF MID($SAVE.mark[A],1,1) = '$':
$SAVE.mark[A] = REPLACE($SAVE.mark[A], '$')
$dataPerson[SAVE.id] += '[' + $SAVE.mark[A] + ']' + $dataPlayer['<<$SAVE.mark[A]>>'] + '[/' + $SAVE.mark[A] + ']'
ELSE:
$dataPerson[SAVE.id] += '[' + $SAVE.mark[A] + ']' + dataPlayer['<<$SAVE.mark[A]>>'] + '[/' + $SAVE.mark[A] + ']'
END
A += 1
JUMP 'plCHAR.save'
END
Отлично, мы сохранили данные в массив и теперь сможем обращаться к нему по мере надобности. Теперь быстро рассмотрим вариант, когда нам необходимо получить данные из нескольких ячеек массива dataPerson. Это не займет много времени, а механика работы почти такая же. После получения индекса из dataPerson, мы можем спокойно вытащить необходимые нам данные с массива.
A = 0 & B = 0 & KILLVAR '$LOAD.mark'
:plLOAD.hub
IF A < ARRSIZE('$SAVE.data'):
LOAD.id = ARRCOMP(0,'dataPerson','.*' + $SAVE.data[A] + '.*')
$LOAD.mark[] = 'ID' & $LOAD.mark[] = 'NAME'
:plLOAD.hub/markcut
IF B < ARRSIZE('$LOAD.mark'):
LOAD.value['MARK.new'] = INSTR($dataPerson[LOAD.id],'['+$LOAD.mark[B]+']') + (LEN($LOAD.mark[B])+2)
LOAD.value['MARK.end'] = (INSTR($dataPerson[LOAD.id],'[/'+$LOAD.mark[B]+']')) - LOAD.value['MARK.new']
!Получение результата;
$LOAD.txt['INFO/<<LOAD.id>>.<<$GEN.mark[B]>>'] = MID($dataPerson[LOAD.id], LOAD.value['MARK.new'], LOAD.value['MARK.end'])
B += 1
JUMP 'plLOAD.hub/markcut'
END
A += 1
JUMP 'plLOAD.hub'
END
Для задания границы обрезки текста из массива нам нужно для начала определить длину искомого текста. В этом нам помогут LOAD.value[’MARK.new’] и LOAD.value[’MARK.end’]. Они указывают начальное и конечное значение для обрезки текста. Всё, что нам остается - передать данные в нужную нам переменную.
Отлично, сегодня мы узнали, как записывать много данных в массив и как их изымать. В следующей части статьи мы разберемся, как наладить автоматическую систему сохранений и привязать всё это дело к нашей механики записи.