Пишемо текстову гру на Python/Ren'Py

Як зробити текстову гру? Та як завгодно. Як зробити кроссплатформенную текстову гру російською з ілюстраціями, звуком, що працюють збереженнями, без проблем з кирилицею, і з яким-ніяким геймплеєм? Та ще й у вільний час, не відриваючись від основної роботи? Ось це вже цікавіше і насправді — досить нескладно. Зацікавлених прошу під кат.

image

Приблизно рік тому ми з товаришем задумали зробити невелику текстову гру приблизно в дусі Sunless Sea і 80 days: про мореплавство, торгівлю, дослідження дивних поселень і спілкування з дивними особистостями. Там мала фігурувати релігія, а краще декілька, головного героя хотілося бачити не спасителем, героєм країни і прославленим мореплавцем, а помірно невдахою підприємцем/авантюристом, до якого і справи нікому немає, а модний вибір між меншим і більшим злом замінити на вибір між добром і добром: ніякого набила оскому гримдарка заради гримдарка. Досить швидко придумались основні фракції і персонажі, великі порти, політична обстановка і купа симпатичних дрібниць на зразок підводного полювання на восьминогів (зображена на КДПВ) та геніальної ідеї дати майже всім персонажам угорські імена, які звучать екзотичний звичних європейських і викликають деяку неявную симпатію. Загалом, дерев'яних будиночків понабигало чимало.

В команді у нас на той момент був один письменник і один програміст (тобто я). Вимоги в попередньому абзаці відносяться скоріше до сетингу і духу гри, так що виконувати їх повинен був мій товариш, а переді мною постали питання геймдизайну і функціональності движка. По-перше, більшу частину часу гравець буде витрачати, читаючи текст і вибираючи дії головного героя. Для цього потрібна тільки стерпна типографіка і можливість писати сценарій з меню, параметрами та змінними. Незабаром підключилася художниця, так що треба було думати ще й про ілюстрації. По-друге, гра про дослідження й торгівлю, тому потрібно десь в доступному гравцеві вигляді зберігати інформацію про зібрані чутках і куплених товарах (а також всіляко її обробляти). І, нарешті, в грі про мореплавство потрібна карта і можливість переміщатися по ній; просто команда «поплисти до тартарам і послухати казки морських коней» явно не відповідає духу проекту. Значить, движок повинен ще й підтримувати хоча б нескладні міні-ігри, а не обмежуватися тільки показом тексту та обрахуванням ігрових змінних.

Чому Ren'Py

Відразу скажу, що писати движок з нуля ми навіть не намагалися: велосипедостроение цікаво саме по собі, але малоефективно, якщо стоїть мета все-таки випустити гру до виходу на пенсію. Також ми не розглядали парсерную Interactive Fiction: у неї і на англійській мові дуже невелика аудиторія, а російською наш проект, будь він парсерным, міг би зацікавити в кращому випадку кілька сотень людей. А хочеться якщо не заробити грошей, то хоча б пройти гринлайт і набрати яку-ніяку репутацію. На щастя, більшість нинішніх англомовних розробників текстових ігор перейшло від некомерційних хобі-проектів до професійного геймдеву буквально кілька років тому. Тому основні движки або опенсорсны, або, у всякому разі, безкоштовні. Давайте подивимося, що нам пропонують.

Перший варіант, який прийшов мені в голову – Storynexus Failbetter games, розробників Fallen London і Sunless Sea. Проекти на ньому змінюються через браузер, хостятся Failbetter і через браузер ж доступні гравцям. Можливості для монетизації з минулого року видалили. Головний мінус, однак, не в цьому, а в тому, що в Fallen London велика частина подій представлена картами, злущується з колоди, і зробити на Storynexus гру, не використовує цю метафору – завдання нетривіальне. Та й взагалі намертво прив'язувати свій проект до стороннього сервера з закритим кодом, який теоретично може взагалі припинити роботу в будь-який момент, досить ризиковано.

Є ще два хороших пропрієтарних движка для Choose Your Own Adventure, тобто приблизно ігор нашого типу: ChoiceScript і Inklewriter. Обоє обіцяють прекрасну типографіку, простоту розробки (браузерний редактор у Inklewriter, скриптова мова у ChoiceScript) і можливість комерційної публікації. На жаль, обидва дозволяють робити тільки чисте CYOA: немає ніякої можливості додавати в гру щось крім власне тексту, меню і иллюстрациий. Уважний читач скаже: “Але як же так? 80 days адже був досить складний інвентар і інтерфейс подорожей, вірно? А в Sorcery! я точно бачив боївку!" На жаль, ці системи розроблялися Inkle Studios під конкретні гри і в редакторі немає ні їх, ні хоч який-небудь можливості зробити собі такі ж. З тієї ж причини (а також тому, що він, ем, своєрідний) ми відмовилися від Twine.

Єдиним прийнятним для нас варіантом виявився Ren'Py. Це безкоштовний опенсорсний движок для візуальних новел (наприклад, саме на ньому зроблені «Нескінченне літо» і «Katawa shoujo»), який досить легко налаштовується для наших завдань. Ігри виходять кросплатформені: збірка дистрибутиву під Win/Mac/Linux – питання натискання однієї кнопки, причому навіть не треба мати під рукою цільову ОС. Android і iOS також заявлені і Ren'Py-релізи під мобільні осі існують, але ми самі поки на мобільний ринок не цілимось і розробки для нього розповісти не можемо. До того ж у Ren'Py дуже доброзичливе і живе співтовариство на російською і англійській.

Найпростіший сценарій на Ren'Py

Ren'Py написаний на Python 2.7 + Pygame і має власний DSL. На цій мові, по-перше, за рахунок команд типу “Показати bg_city_night_53.png в якості фону без анімації" або «Промовити репліку „Сем… СЕМПАЙ!!!“ від імені персонажа nyasha1» в імперативному стилі пишеться власне сценарій. По-друге, підмножиною мови є Screen Language, на якому можна в декларативному стилі збирати з обмеженого набору Displayables (тобто віджетів: кнопок, зображень, текстових полів тощо) екрани і налаштовувати їх функціональність. Якщо вбудованих можливостей недостатньо, то за допомогою Python можна додавати власні. Цим ми займемося в наступній статті, а поки розберемося зі сценарієм.

Сценарій Ren'Py складається з послідовності реплік, дій з екранами та введення гравця. Про екрани і введення трохи нижче, а для початку ми розберемося з персонажами. У візуальній новелі вони створюються так (код з офіційного туториала, з незначними правками):

define m = Character('Me', color="#c8c8ff")
define s = Character('Sylvie', color="#c8ffc8")
image sylvie smile = "sylvie_smile.png"
label start
m "Um... will you..."
m "Will you be my artist for a visual novel?"
show sylvie smile
s "Sure, but what is a \"visual novel?\""

Створено два персонажа: протагоніст і Сільві, обидва пишуть блідо-синім кольором стандартне віконце внизу екрану. У Сільві до того ж є портрет, який з'явиться на екрані перед тим, як вона почне говорити. Виглядає це ось так:

image

Якщо б ми створювали візуальну новелу, то продовжували в тому ж дусі, але ми не збираємося показувати портрети персонажів, так і ілюстрацій пара десятків на всю гру. Велика частина тексту того ж не є прямою мовою персонажів, так що нелогічно було б прив'язувати її до когось із них. Краще створимо віртуального персонажа-оповідача:

define narrator = Character(None, kind = nvl, what_color="#000000", size = 12)

Його звуть narrator; це спеціальне ім'я, яке віддає йому весь текст, явно не аттрибутированный іншим персонажам (строго кажучи, його звуть None, а narrator, як і m і s в попередньому прикладі – змінна, в яку поміщається об'єкт персонажа і з якої викликаються його методи, наприклад, say) Аргумент kind приймає два значення: adv і nvl. Перше – це дефолтний поведінка, описане вище, а друге включає nvl-режим, в якому портрети не показуються, а текстове поле займає більшу частину екрана. Якраз те, що нам було потрібно. Цей режим описується екраном nvl_screen у файлі screens.rpy і групою стилів styles.nvl* (файли screens.rpy і options.rpy відповідно), в яких ми задамо шрифт, фон текстового поля, колір меню і все інше.

image

label start:
image bg monet_palace_image = Image('images/1129_monet_palace.jpg', align=(0 .5, 0.5)) 
nvl clear
hide screen nvl
scene bg monet_palace_image 
$ Ren'Py.pause(None) 
" — Я завжди говорив: твої пісеньки — лайно, Люсьєн, і я не розумію, де ти знаходиш музикантів, згодних це виконувати!"

Розберемо порядково: спершу оголошується ярлик start, з якого почнеться гра. Це назва зарезервовано і движок завжди буде переходити на нього після натискання кнопки «Нова гра», де б в сценарії він не знаходився. Все, що слідує за ярликом, логічно знаходиться «всередині» цього ярлика, тому виділяється индентацией: вона в Ren'Py працює так само, як і в чистому пітоні. Ініціалізація картинки досить очевидна, а ось наступна строчка робить важливу річ: прибирає весь текст з екрану nvl_screen. Автоматично це не робиться, тому, якщо не розставляти nvl clear в кінці кожної сторінки, текст спокійно уповзе за межі екрану і буде виводитися туди, поки екран не буде нарешті очищений. Начебто дрібниця, але на налагодження пропущених nvl clear я витратив набагато більше часу, ніж готовий визнати. Свіжовимиту екран ми тимчасово приберемо, щоб дозволити гравцеві помилуватися фоном, покажемо фон, включимо нескінченну паузу (тобто дочекаємося кліка) і почнемо історію. Як тільки на nvl_screen почне виводитися текст, екран сам повернеться на місце.

Рядок з паузою, до речі, вже на пітоні: для включення одиничної рядки її досить почати з '$', а більш довгі шматки коду потрібно писати всередині блоку 'python:'. Будь-код, виконуваний грою, бачить модулі самого Ren'Py і явно імпортувати їх вже не потрібно.

Додаємо розгалуження і змінні

До цього моменту гра являє собою читалку, яка показує текст, змінюючи при необхідності фони. Збереження, перемотування, головне меню та налаштування вже працюють з коробки. Однак якщо б ми хотіли написати ілюстровану повість, то ми б її і написали, вірно? Додамо перед текстом невелике меню:

label start:
menu: 
"Зайти в меню різноманітного дебага": 
$ debug_mode = True
jump debug_menu 
"Пропустити вступ": 
jump the_very_start_lazlo_nooptions
"Почати вступ": 
label the_very_start: 
#show screen nvl
nvl clear 
hide screen nvl 
scene bg monet_palace_image 
$ Ren'Py.pause(None) 
" — Я завжди говорив: твої пісеньки — лайно, Люсьєн, і я не розумію, де ти знаходиш музикантів, згодних це виконувати!" 

Тепер після включення гри користувач (або, швидше, розробник) зможе при бажанні увійти в режим дебага або пропустити вже готовий шматок вступу і почати тестувати одразу шматок з останнього коміта. Рядок show screen nvl закомменчена за непотрібністю – як я вже згадував вище, екран здасться сам собою, коли на ньому оновиться текст. Коменти, як бачите, працюють абсолютно очевидним чином.

Ярлики, меню та інші индентированные блоки можуть бути вкладені до довільної глибини, але на практиці ми намагаємося дробити текст на епізоди в десяток сторінок. Кожен такий епізод описаний всередині окремого ярлика з нульовою индентацией (він вже не зобов'язаний бути всередині ярлика start або навіть в одному з них файлі), а переходи з одного епізоду до іншого здійснюються стрибками. Так ми не тільки боремося з десятками рівнів индентации, але і забезпечуємо модульність коду: кожен епізод може тестуватися окремо і досить нескладно перевірити, які змінні він читає, в які пише і куди дозволяє перейти.

Внутрішньоігрові меню і змінні влаштовані абсолютно так само. Оскільки і змінних, і ярликів навіть у невеликому епізоді на десять хвилин гри розлучається неймовірна кількість, ми прийняли нескладний варіант угорської нотації: ім'я ярлика 'the_very_start_lazlo_nooptions' складається з трьох частин: назви локації the_very_start (тобто період від початку гри до першого виходу в море), назви епізоду lazlo (тобто пиятика у Лазло, на якій можна найняти молодих нероб в матроси) та імені власне ярлика. При такому підході імена виходять досить громіздкими, але краще так, ніж виявити при тестуванні, що три місяці тому хтось вже створив змінну ship_listing, виставив True бозна-де і тепер крен з одного випадкового події впливає на результат іншого випадкового події на іншому кінці моря.

Замість висновку

До цього моменту ми вже відтворили на Ren'Py функціонал згадуваних вище Choicescript і inklewriter. Начебто наш кораблик готовий до відплиття. У наступній статті я покажу, як можна створювати більш складний інтерфейс з використанням екранного мови RenPy і ще більш складний — на чистому пітоні.

image
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.