Конечные автоматы в менюшном движке. Оформляем доисторическое приключение на ink. Часть 3

Продолжение части 2. То что получилось в итоге можно посмотреть по ссылке:
dialas.ru/ink-dino/
Скрин игры на широкоформатном экране:


В этой статье будут рассмотрены финальные штрихи — оптимизация текста для вычитки, добавление картинок, музыки и общий дизайн странички.


Группировка текста

Для того, чтобы в будущем можно было дать на вычитку тексты я сгруппировал их в одном месте и объявил через константы:

//Узел для инициализации значений переменных
=== init_and_start_knot ===
...
CONST T_NOT_WORK = 		"Не сработало. "
CONST T_HUNTER_WAIT = 		"Охотник немного подождал. "
CONST T_THROW_FAILED = 		"Охотник не может подобрать удобную позицию чтобы метнуть копьё, зверюга очень изворотливая."
CONST T_THROW_BLIND = 		"Присмотревшись к кустам, охотник понимает, что вслепую атаковать бессмысленно."
CONST T_COME_CLOSER = 		"Пока динозавр скрывался охотник тихонько подкрался ближе к стволу дерева."
...
Правда, читаемость кода несколько снизилась:

//Состояния героя
=== function print_hero_state(state) ===
{ 
    - state == far_tree: 
        ~ return T_STATE_HERO_FAR
    - state == near_tree: 
        ~ return T_STATE_HERO_NEAR
    - state == up_tree: 
        ~ return T_STATE_HERO_ON_TREE
}

Анимированный логотип и финальная картинка

Небольшой ретро-налёт придаёт логотип из ASCII-графики с небольшой анимацией. Использовал Python-библиотеку asciimatics, нашел пару артов с деревом и охотником. Код под спойлером, при его запуске начинается анимация прямо в консоли. Далее просто записал с экрана. Кому интересно длинный код под спойлером, по сути просто переделка примера из библиотеки.
Спойлер

import random
from asciimatics.screen import Screen
from asciimatics.effects import Cycle, Print, Stars, Wipe, RandomNoise
from asciimatics.renderers import StaticRenderer, SpeechBubble, FigletText, Box
from asciimatics.scene import Scene
from asciimatics.screen import Screen
from asciimatics.sprites import Arrow, Plot, Sam, Sprite
from asciimatics.paths import Path
from asciimatics.exceptions import ResizeScreenError
import sys



last_blink = 2
def _blink2():
    global last_blink
    if last_blink == 2:
        last_blink = 0
    elif last_blink == 1:
        last_blink = 2
    elif last_blink == 0:
        last_blink = 1

    return last_blink


tree_anim = [
    """
                                   .         ;  
      .              .              ;%     ;;   
        ,           ,                :;%  %;   
         :         ;                   :;%;'     .,   
,.        %;     %;            ;        %;'    ,;
  ;       ;%;  %%;        ,     %;    ;%;    ,%'
   %;       %;%;      ,  ;       %;  ;%;   ,%;' 
    ;%;      %;        ;%;        % ;%;  ,%;'
     `%;.     ;%;     %;'         `;%%;.%;'
      `:;%.    ;%%. %@;        %; ;@%;%'
         `:%;.  :;bd%;          %;@%;'
           `@%:.  :;%.         ;@@%;'   
             `@%.  `;@%.      ;@@%;         
               `@%%. `@%%    ;@@%;        
                 ;@%. :@%%  %@@%;       
                   %@bd%%%bd%%:;     
                     #@%%%%%:;;
                     %@@%%%::;
                     %@@@%(o);  . '         
                     %@@@o%;:(.,'         
                 `.. %@@@o%::;         
                    `)@@@o%::;         
                     %@@(o)::;        
                    .%@@@@%::;         
                    ;%@@@@%::;.          
                   ;%@@@@%%:;;;. 
               ...;%@@@@@%%:;;;;,..
"""
]

fire_anim = [
"""
                             
                             
                             
               (             
                )            
               (  (          
                   )         
             (    (          
              ) /\  (        
            (  // | (        
          _ -.;_/ \\--._     
         (_;-// | \ \-'.\    
         ( `.__ _  ___,')    
          `'(_ )_)(_)_)'     
""",
"""
                             
               (             
                )            
               (             
                )            
               (  (          
                   )         
             (    (          
              ) /\  (        
            (  // | (        
          _ -.;_/ \\--._     
         (_;-// | \ \-'.\    
         ( `.__ _  ___,')    
          `'(_ )_)(_)_)'     
""",
"""
               )             
               (             
                             
                             
                 (           
               (  (          
                   )         
             (    (          
              ) /\  (        
            (  // | (        
          _ -.;_/ \\--._     
         (_;-// | \ \-'.\    
         ( `.__ _  ___,')    
          `'(_ )_)(_)_)'     
"""
] 



hunter_wait_anim = [
"""
       ,&&&.
       .,.&&
       \=__/
       ,'-'.
  _.__|/ /|
 ((_|___/ |
  ((  `'--|
    \\ \-._/.
   <_,\_\`--'|
     <_,-'__,'
"""
];


kust_anim = [
"""
      |
    \|/|/
  \|\\|//|/
   \|\|/|/
    \\|//
     \|/
     \|/
      |
_\|/__|_\|/____\|/_
"""
];


def _speak(screen, text, pos, start):
    return Print(
        screen,
        SpeechBubble(text, "L", uni=screen.unicode_aware),
        x=pos[0] + 4, y=pos[1] - 4,
        colour=Screen.COLOUR_CYAN,
        clear=True,
        start_frame=start,
        stop_frame=start+50)

			
class FireSprite(Sprite):
    def __init__(self, screen, path, colour=Screen.COLOUR_WHITE, start_frame=0,
                 stop_frame=0):
        super(FireSprite, self).__init__(
            screen,
            renderer_dict={
                "default": StaticRenderer(images=fire_anim,
					animation=_blink2),
                "left": StaticRenderer(images=[fire_anim]),
                "right": StaticRenderer(images=[fire_anim]),
                "down": StaticRenderer(images=[fire_anim]),
                "up": StaticRenderer(images=[fire_anim]),
            },
            path=path,
            colour=colour,
            start_frame=start_frame,
            stop_frame=stop_frame)			
			
def demo(screen):
    scenes = []
    centre = (screen.width // 2, screen.height // 2)
    podium = (8, 5)
    
    #Scene 0
    effects = [
        Cycle(
            screen,
            FigletText("DIALAS", font='big'),
            screen.height // 2 - 8),
        Cycle(
            screen,
            FigletText("PRESENTS", font='big'),
            screen.height // 2 + 3),
        #Stars(screen, (screen.width + screen.height) // 2)
    ]
    scenes.append(Scene(effects))
	
	#scene 0 to 1
    effects = [
	    Cycle(
            screen,
            FigletText("DIALAS", font='big'),
            screen.height // 2 - 8),
        Cycle(
            screen,
            FigletText("PRESENTS", font='big'),
            screen.height // 2 + 3),
        RandomNoise(screen)
    ]
    scenes.append(Scene(effects))
	
    # Scene 1.
    path = Path()
    path.jump_to(centre[0], centre[1])
	
    effects = [
        Sprite(
			screen,
            renderer_dict={
                 "default": StaticRenderer(images=tree_anim)
            },
            path=path,
            colour=Screen.COLOUR_GREEN)
    ]
    scenes.append(Scene(effects))
    
	# Scene 2.
    path2 = Path()
    path2.jump_to(centre[0]-45, centre[1]+7)
    path3 = Path()
    path3.jump_to(centre[0]-25, centre[1]+9)
	#path.jump_to(-20, centre[1])
    #path.move_straight_to(centre[0], centre[1], 10)
    #path.wait(300)

    effects = [
        Sprite(
			screen,
			renderer_dict={
				"default": StaticRenderer(images=tree_anim)
			},
			path=path,
			colour=Screen.COLOUR_GREEN),
        Sprite(
			screen,
			renderer_dict={
				"default": StaticRenderer(images=hunter_wait_anim)
			},
			path=path3,
			colour=Screen.COLOUR_WHITE),
		FireSprite(screen, path2, colour=Screen.COLOUR_RED),
		_speak(screen, "ПРОВЕРКА РУССКОГО", centre, 30)
    ]
    scenes.append(Scene(effects))
	
	
    screen.play(scenes, stop_on_resize=True)

Screen.wrapper(demo)

Финальную картинку нашел в интернете и немного обработал под рисунок карандашем, всё просто.

Дизайн темы

Тему решил взять подходящую под доисторические приключения. Я сам мало что понимаю в дизайне и не люблю копаться в коде верстки, поэтому нашел уже готовую из бесплатных: chocotemplates.com/portfolio/dinosaurs. Сгенерировал веб-версию через графический редактор ink и сделал дополнения:
  • В style.css поставил фон, настроил похожие цвета шрифтов и кнопки
  • В index.html убрал ссылки что написан на ink, для большего погружения в игру.
  • В main.js сделал удаление предыдущего текста, так как надо описание только последней сцены (потом узнал про тэг #CLEAR, но решил оставить по-старому)

Музыка

Взял саундтрек с сайта www.jamendo.com и добавил кнопку для выключения музыки:

<audio id="player" src="Marian_W_-_Darkbreeze_-_Hunter_s_Legend.mp3" autoplay loop></audio>
<div>
    <button onclick="document.getElementById('player').pause()">Выкл. звук</button>
</div> 
Так как не сильно разбираюсь во front-end разработке, не стал делать включения звука под разные браузеры и вывод текста что аудио не будет работать.

Выводы

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

Ссылка на онлайн-версию: dialas.ru/ink-dino
Скачать архив с веб-версией: cloud.mail.ru/public/276U/1ph5r3Y2L
Исходник игры на ink: pastebin.com/N3PKr6fp

3 комментария

Oreolek
Очень странно, что не показал в Ink то, что у него лучше всего получается: нелинейные диалоги.

Такие вот системные фишки — это, конечно, хорошо что они есть, но предполагается же, что упор идёт на диалоги. Их и в константы не надо выносить для вычитки, потому что даже с разметкой они удобно читаются. (Кстати, в константы не весь текст вынесен.)

А правила симуляции и обновления состояния можно и во внешние функции вынести, особенно если хочется посложнее.
alastochkin
Смотри, я старался показать как минимальными средствами можно сделать автомат. У каждого движка есть сильные и слабые стороны. Иллюстрация могла быть на URQ, QSP, AXMA, главное чтобы читатели могли попробовать на чём-нибудь своём. Насчет констант соглашусь, наверное для такого проекта не очень актуально, был сделан задел под большой проект, где тексты пишутся в таблице типа word, а потом уже конвертируются в константы.
Симуляцию и логику как раз я не хотел выносить, потому что тогда теряется переносимость. Ведь весь код ink компилиться в json а ты можешь запустить в консоли, html или Lectrote. Хотя для серьезного проекта или для интеграции с unity это излишне, согласен.
techniX
В случае с Ink, выносить художественный текст в константы — довольно спорное решение.

Сама идея Ink заключается в том, чтобы писать художественный игровой текст в естественном виде, и добавлять в него конструкции для управления потоком. Как результат, скрипт на Ink читать и вычитывать довольно легко. А константы, увы, лишают Ink этого преимущества, заставляя скроллить текст туда-сюда, чтобы понять, что происходит в том или ином эпизоде :(

В целом же — отличная реализация конечного автомата. Особенно благодаря спискам (LIST), которые дают возможность использовать перечисляемый тип данных. Эх, ещё б массивы… :)