Интерактивные истории, текстовые игры и квесты
Регистрация / Вход
Посетите наш новый сайт AXMAJS.RU
Бесконечное число сохранений и РПГ на 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.
Профиль
Закрыть
Показать все комментарии
17.02.14 10:15
Спасибо ДД за своевременный выпуск 2.5.1. И как он угадал, что нам понадобится? :)
25.02.14 12:58
Про сохранения вижу, а где тут 210 рас? O__o
26.02.14 14:50
Это был тест на внимательность и приманка для любителей РПГ :)Комментарий в параграфе "СлучайнаяРаса" гласит: "В массиве 20 чистых рас, плюс 190 всевозможных их сочетаний — итого 210 различных рас! Так-то!"
Для того, чтобы оставлять комментарии, необходимо зарегистрироваться и подтвердить в профиле указанный
При использовании любых материалов блога обязательно указание ссылки на источник
Отличная работа, браво!Но заметил один возможный баг. Если прибавить по совету автора для шифрования достаточно большое число, то можно "выпрыгнуть" за разрешённый предел в 65536, что несколько сужает диапазон возможных ключей шифрования.