Авторизация
×

Логин (e-mail)

Пароль

Интерактивные истории, текстовые игры, квесты и визуальные новеллы
Гиперкнига

Библиотека    Блог

Бесконечное число сохранений и РПГ на 210 рас

Борис Семёнов (Morych), 16.02.14 | Практика ASM

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

Суть такова: возьмём и сцепим значения всех объектов в одну строку текста, которую покажем игроку. Игрок сможет скопировать эту строку в буфер обмена и сохранить её в текстовом файле. Затем при необходимости эту строку состояния игры можно будет вставить в текстовое поле и вернуться к сохранённому моменту. Кроме того сохранённую таким образом игру можно будет продолжить на любом другом браузере и, что более важно, даже на другом устройстве! А ещё можно обеспечить связь между несколькими частями игры, и в следующей части, например, играть персонажем из предыдущей.

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

:: StoryTitle
Приключенец

:: StoryAuthor
Morych

:: StorySubtitle
История одного приключенца и демонстрация возможности сохранения состояния игры в строку и восстановления из строки.

:: StoryMenu
[[*Сохранить в строку|Сохранение]]
[[*Восстановить из строки|Восстановление]]

:: Start
<<fade = 500>>
# Характеристики персонажа:
# Имя
<<set $name = "Неведомый">>
# Раса
<<set $race = "человек">>
# Уровень
<<set $level = 1>>
# Текущее значение здоровья
<<set $hp = 100>>
# Максимум здоровья
<<set $hpMax = 100>>
# Сила
<<set $str = 10>>
# Признак того, что персонаж отдыхает
<<set $sleep = false>>
# Массив для инвентаря
<<set $inventory = []>>
# Действие игрока
<<set $action = 0>>
# Разделитель значений объектов
<<set $separator = "|">>
----
===''[[Создать героя|СозданиеГероя]]''===
===''[[*Продолжить игру|Восстановление]]''===

В параграфе «Start» мы объявили необходимые объекты, присвоив им начальные значения. Объекты у нас получились самых разных типов: строковые, числовые, один логический и даже целый массив. Особое внимание обратите на объект $separator. Его значение должно быть уникальным, оно не должно встречаться среди значений других объектов. В данном случае я уверен, что вертикальная черта не встретится ни в имени персонажа, ни в названии расы. Посмотрим же на генерацию персонажа:

:: СозданиеГероя
<<display 'СлучайноеИмя'>>
<<set $name = $result>>
<<display 'СлучайнаяРаса'>>
<<set $race = $result>>
<<random $rnd = 100>>
<<set $hpMax = $rnd + 100>>
<<set $hp = $hpMax>>
<<random $rnd = 10>>
<<set $str = $rnd + 10>>
<<display 'ЛистПерсонажа'>>
----
===''[[Готово (все довольны)|Игра]]''===
===''[[Хочу другого героя|СозданиеГероя]]''===

:: СлучайноеИмя
# Чтобы получить больше имён, просто добавьте их части в массивы
<<set $partOne = ["Алани", "Огне", "Боро", "Элен", "Пиро", "Миро", "Уле", "Яро", "Сати", "Вели", "Добро", "Зло", "Дву", "Пере", "Старо", "Феро"]>>
<<set $partTwo = ["дор", "мир", "эль", "сил", "хил", "мон", "тон", "кун", "сян", "пит", "гнев", "гил", "дил", "дел", "мер", "мен"]>>
<<set $partOneLastIndex = $partOne.length - 1>>
<<set $partTwoLastIndex = $partTwo.length - 1>>
<<random $num1 = $partOneLastIndex>>
<<random $num2 = $partTwoLastIndex>>
<<set $result = $partOne[$num1] + $partTwo[$num2]>>

:: СлучайнаяРаса
# Чтобы получить больше рас, добавьте их в массив
<<set $races = ["человек", "эльф", "орк", "дварф", "гном", "хоббит", "гоблин", "демон", "балрог", "трот", "мерзик", "тролль", "данмер", "двеммер", "великан", "огр", "вампир", "карлик", "дракон", "ангел"]>>
<<set $racesLastIndex = $races.length - 1>>
<<random $num1 = $racesLastIndex>>
<<set $result = $races[$num1]>>
# С 70% вероятностью получаем смешанную расу:
<<random $rnd = 99>>
<<if $rnd lt 70>>
    <<set $result = "полу" + $result>>
    <<random $num2 = $racesLastIndex>>
    <<if $num2 eq $num1>>
        <<set $num2++>>
        <<if $num2 gt $racesLastIndex>>
            <<set $num2 = 0>>
        <<endif>>
    <<endif>>
    <<set $result = $result + "-полу" + $races[$num2]>>
<<endif>>
# В массиве 20 чистых рас, плюс 190 всевозможных их сочетаний — итого 210 различных рас! Так-то!

:: ЛистПерсонажа
===''Лист персонажа''===
----
<<nop>>
{{{.......Имя: }}}''<<print $name>>''
<<br>>
{{{......Раса: }}}//''<<print $race>>''//
<<br>>
{{{...Уровень: }}}''<<print $level>>''
<<br>>
{{{..Здоровье: }}}''<<print $hp>> / <<print $hpMax>>''
<<br>>
{{{......Сила: }}}''<<print $str>>''
# Можно заменить точки перед названиями характеристик на неразрывные пробелы
<<endnop>>

Итак, генерация персонажа готова. И тут есть один момент, на который нужно обратить внимание. Если бы мы предоставили игроку возможность набрать имя персонажа с клавиатуры, он теоретически мог бы ввести значение объекта $separator, а мы помним, что это значение должно оставаться уникальным. Решение: сразу после ввода заменяем зарезервированные символы на что-нибудь другое методом replace(). Ну что же, осталось наполнить инвентарь предметами и заодно предоставить игроку возможность исполнить мечту любого приключенца — получить 80 уровень.

:: Игра
<<if $action eq 1>>
    # Повышение уровня
    <<set $level++>>
    <<set $hpMax = Math.round($hpMax * 1.05)>>
    <<set $str = Math.round($str * 1.05)>>
<<elseif $action eq 2>>
    # Получение предмета
    <<display 'СлучайныйПредмет'>>
    <<set $inventory.push($result)>>
    <<set $itemCode = $result>>
<<elseif $action eq 3>>
    # Отдых (восстановление здоровья)
    <<set $hp = Math.min(Math.round($hp + $str), $hpMax)>>
<<endif>>
<<display 'ЛистПерсонажа'>>
<<if $action eq 2>>
    <<display 'НаименованиеПредмета'>>
    ===<<print $name>> получает <<print $result>>.===
<<endif>>
<<set $action = 0>>
<<if $level lt 80>>
    <<if $sleep>>
        ===<<print $name>> сейчас отдыхает...===
        ----
        ===''[[Отдохнуть ещё|Игра {$action = 3}]]''===
        ===''[[Проснуться|Игра {$sleep = false}]]''===
    <<else>>
        ===<<print $name>> ищет приключений...===
        ----
        ===''[[Получить уровень|Игра {$action = 1}]]''===
        ===''[[Раздобыть предмет|Игра {$action = 2}]]''===
        ===''[[Заглянуть в сумку|Инвентарь]]''===
        ===''[[Отдохнуть|Игра {$sleep = true}]]''===
    <<endif>>
<<else>>
    ===<<print $name>> достиг 80 уровня и отправился на заслуженную пенсию.===
    ===''Конец игры''===
    ----
    ===''<<restart 'Заново'>>''===
<<endif>>

:: Инвентарь
===''Инвентарь''===
----
<<if $inventory.length eq 0>>
    В сумке ничего нет.
<<else>>
    В сумке <<print $name>> несёт:
    <<set $i = 0>>    
    <<loop $inventory.length>>
        <<set $itemCode = $inventory[$i]>>
        <<display 'НаименованиеПредмета'>>
        * <<print $result>>
        <<set $i++>>
    <<endloop>>
<<endif>>
----
===''[[Закрыть сумку|Игра]]''===

:: НаименованиеПредмета
<<set $partOne = ["зелье", "эликсир", "настойку", "меч", "булаву", "кольцо", "шлем", "броню", "рукавицы", "свиток", "амулет", "кинжал", "лук", "кристалл", "штаны", "сапоги"]>>
<<set $partTwo = [" драконьего пламени", " силы земли", " уныния", " кошачьей грации", " всевластья", " раздвоения личности", " пафоса", " бурного веселья", " совершенства", " праведного гнева", " ангельской пыли", " вечности", " невидимости", " алчности", " вечной славы", " абсолютного нуля"]>>
<<set $num1 = (($itemCode + "").split(".")[0]) * 1>>
<<set $num2 = (($itemCode + "").split(".")[1]) * 1>>
<<set $result = $partOne[$num1] + $partTwo[$num2]>>

:: СлучайныйПредмет
<<set $itemCode = "0.0">>
<<display 'НаименованиеПредмета'>>
<<set $partOneLastIndex = $partOne.length - 1>>
<<set $partTwoLastIndex = $partTwo.length - 1>>
<<random $num1 = $partOneLastIndex>>
<<random $num2 = $partTwoLastIndex>>
<<set $result = $num1 + "." + $num2>>

Массив инвентаря $inventory у нас хоть и текстовый, но содержит не наименования предметов, а их номера. Это сделано для того, чтобы строка сохранения получалась не такой длинной. А теперь код параграфа сохранения состояния:

:: Сохранение
----
===''Сохранение''<<br>>//Скопируйте строку состояния и сохраните её в текстовом файле://===
# Первым делом запоминаем текущий параграф:
<<set $save = $$title>>
# Пристыковываем значения объектов через разделитель
<<set $save = $save + $separator + $name>>
<<set $save = $save + $separator + $race>>
<<set $save = $save + $separator + $level>>
<<set $save = $save + $separator + $hp>>
<<set $save = $save + $separator + $hpMax>>
<<set $save = $save + $separator + $str>>
<<set $save = $save + $separator + $sleep>>
# Добавляем длину массива и все его значения
<<set $save = $save + $separator + $inventory.length + $separator + $inventory.join($separator)>>
# Кодируем строку состояния и показываем код игроку
<<set $i = 0>>
<<loop $save.length>><<if $i gt 0>>-<<endif>><<print $save.charCodeAt($i)>><<set $i++>><<endloop>>
----

Внимательные читатели могли заметить, что мы сохраняем значения не всех объектов. А некоторые из них сохранять и не нужно. Например, объект $action, значение которого отлично от нуля только в момент перехода на параграф «Игра», а затем снова устанавливается в ноль. Другим служебным объектам значения присваиваются непосредственно перед их использованием.

Остановимся на кодировании строки состояния. Вообще, можно сохранить игру и без кодирования. Напишем <<print $save>>, и игрок получит, например, такую строку состояния:

Игра|Аланисил|полуэльф-полудварф|10|130|203|22|false|3|4.11|12.5|9.8

И сразу возникает соблазн чего-нибудь в сохранении подкрутить, наставить себе 9999 жизней и силы побольше, чтобы наносить каждому по 100 урона. Но в нашем случае вместо каждого символа строки состояния показывается его код в кодировке Unicode. Коды символов разделены дефисами, чтобы можно было выполнить обратное преобразование. Наш игрок теперь видит такую строку состояния:

1048-1075-1088-1072-124-1040-1083-1072-1085-1080-1089-1080-1083-124-1087-1086-1083-1091-1101-1083-1100-1092-45-1087-1086-1083-1091-1076-1074-1072-1088-1092-124-49-48-124-49-51-48-124-50-48-51-124-50-50-124-102-97-108-115-101-124-51-124-52-46-49-49-124-49-50-46-53-124-57-46-56

Такое сохранение выглядит уже солиднее. Десятичный код символа строки мы получаем при помощи метода charCodeAt(x), где x — индекс символа в строке. При желании строку состояния можно ещё и зашифровать. Простейший метод шифрования — прибавить к каждому коду какое-нибудь число. И гипотетическому хацкеру уже придётся повозиться, чтобы наставить себе бесконечные патроны. При восстановлении не забываем, конечно, отнять от каждого кода это же число, которое называется ключом шифра. Ключом, конечно, может быть и не одно число, а несколько, которые прибавляются по очереди. Но особенно не увлекайтесь шифрованием, иначе при большом количестве объектов игра станет подтормаживать при сохранении и загрузке. Вот, кстати, на загрузку мы сейчас и посмотрим.

:: Восстановление
----
===''Восстановление''<<br>>//Вставьте в текстовое поле строку состояния и нажмите «Ввод»://===
<<input 'Загрузка' $loadCode>>

:: Загрузка
# Преобразуем строку в массив
<<set $loadCode = ($loadCode + "").split("-")>>
# Получаем из кодов символов исходную строку состояния
<<set $load = "">>
<<set $i = 0>>
<<loop $loadCode.length>>
    <<set $load = $load + String.fromCharCode($loadCode[$i])>>
    <<set $i++>>
<<endloop>>
# Разделяем значения объектов
<<set $load = $load.split($separator)>>
# Присваиваем объектам значения
<<set $name = $load[1]>>
<<set $race = $load[2]>>
# Числовые значения умножаем на 1, чтобы преобразовать строку в число
<<set $level = $load[3] * 1>>
<<set $hp = $load[4] * 1>>
<<set $hpMax = $load[5] * 1>>
<<set $str = $load[6] * 1>>
# Восстанавливаем значение логического объекта
<<if $load[7].toLowerCase() eq "true">>
    <<set $sleep = true>>
<<else>>
    <<set $sleep = false>>
<<endif>>
# Получаем длину сохранённого массива
<<set $inventoryLength = $load[8] * 1>>
# Заполняем массив элементами
<<set $inventory = []>>
<<set $i = 1>>
<<loop $inventoryLength>>
    <<set $inventory.push($load[8 + $i])>>
    <<set $i++>>
<<endloop>>
# Переходим на сохранённый параграф
<<goto $load[0]>>

Для получения символа по его коду мы применяем статический метод String.fromCharCode(x), где x — код символа. И чтобы ASM не выдавал ошибку, используйте версию редактора от 2.5.1 и выше. Или можно воспользоваться онлайн-конструктором.

Приведённый метод сохранения имеет некоторые ограничения. Например, нельзя восстановить название предыдущего параграфа. Поэтому лучше запрещать сохранение (не показывать строку состояния) на параграфах, в которых используются элементы навигации <<back>> или <<return>>. Следует также учесть, что методы charCodeAt() и fromCharCode() можно использовать приведённым способом только в пределах Базовой многоязыковой плоскости Юникода (первые 65536 символов). Впрочем, этого разнообразия должно хватить для любой игры ;)

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

Всё, теперь можно задавать вопросы.

P.S. Разрешаю допилить мини-РПГ до чего-нибудь более играбельного. Только не забудьте поменять название и вписать себя в качестве автора.

Комментарии: 7.

Профиль


Закрыть

Показать все комментарии

Morych
17.02.14 10:15

Спасибо ДД за своевременный выпуск 2.5.1. И как он угадал, что нам понадобится? :)
Ajenta
25.02.14 12:58

Про сохранения вижу, а где тут 210 рас? O__o
Morych
26.02.14 14:50

Это был тест на внимательность и приманка для любителей РПГ :)

Комментарий в параграфе "СлучайнаяРаса" гласит: "В массиве 20 чистых рас, плюс 190 всевозможных их сочетаний — итого 210 различных рас! Так-то!"

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

При использовании любых материалов блога обязательно указание ссылки на источник