Инструменты
November 16, 2022

Puzzlescript. Прототип игры за вечер

Расскажу о движке Puzzlescript, на котором легко сделать прототип для небольшой головоломки. Движок написан на HTML5, поэтому в прототипы можно будет играть даже в браузере телефона.

Только надо сразу оговориться, что движок заточен под определенный жанр. Самое быстрое, что можно на нем сделать, это игры в жанре «Сокобан» — игры, где есть главный герой, который перемещается по полю и взаимодействует с некими объектами, например, двигает ящики к целям. Доступное управление — движение в четырех направлениях, одна кнопка действия, отмена хода и рестарт уровня.

Цель сокобана: передвинуть ящики на цели

У меня на этом движке получилась такая небольшая игра:

Рекомендую также посмотреть галерею лучших игр на сайте самого движка.

Я предполагаю, что на движке также можно реализовать игры, где нет главного героя, но которые управляются стрелками. Например, можно сделать «2048»-подобную игру.

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

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

Дальше я расскажу, как сделать первый прототип на таком движке.

Если эта статья покажется вам полезной, то можете также подписаться на телеграмм-канал: https://t.me/SecretRoom_Gamedesign.

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

Знакомство с редактором

Давайте познакомимся с редактором и поймем, как устроены уровни в игре.

Открываемредактор игры: https://www.puzzlescript.net/editor.html

Если вы открываете сайт в первый раз, то откроется базовый пример: простейший сокобан.

Редактор поделен на три больших блока: Код, Игра, Консоль. Играем в правом верхнем блоке страницы. Если нужно перезапустить игру, то нажимайте Run в верхней панели сайта.

Можно загрузить другие примеры через меню Load Example. Если хотите вернуться к примеру с сокобаном, то загружайте «Basic»:

Давайте отредактируем один из уровней игры. Нажимаем на Level Editor в верхней панели сайта. Блок с игрой перейдет в режим редактирования уровня.

В верхней части блока мы увидим панель выбора тайлов. Здесь у нас представлен набор всех возможных объектов и их комбинаций в игре:

  • Фон
  • Стена
  • Герой
  • Ящик
  • Ящик + Цель. (На тот случай, если их нужно поставить на одну на клетку.)
  • Цель.

Выбираем нужные тайлы, как в палитре, и ставим их на уровне в нужных местах.

Чтобы сделать уровень больше, нажимайте левой кнопкой мыши на пространство по краям уровня. Чтобы сделать уровень меньше — нажимайте правой.

Обратите внимание на то, что можно одновременно играть и редактировать уровень, на котором играете. В дальнейшем, это будет очень полезно в экспериментах и тестировании идей.

Если в палитре тайлов нажать мышкой на большую букву S, то в блок консоли выведется код уровня:

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

Можем сразу добавить сообщения между уровнями, которых нет в этом примере, через конструкцию:

Message Text of your message between levels

Нажимаем в верхней панели Rebuild, затем Run, и играем в новую последовательность уровней.

Код

Весь код игры отображается в левом блоке страницы. Это действительно весь код, в котором есть описание правил, «графики» и уровней. Давайте разберем из каких частей он состоит.

К слову, комментарии в коде оставляются в скобках:

Вступительная информация об игре

Сначала идет различная мета-информация об игре: название, автор, ссылка на страницу автора.

Дополнительные атрибуты у игры можно посмотреть тут. Так, например, здесь мы можем поменять цвет фона и текста внутри игры.

background_color blue
text_color white

Объекты и их внешний вид

Дальше идет участок, описывающий объекты игры: фон, цель, стена, главный герой и ящик. Каждый объект представляет собой картинку размером 5x5 пикселей.

Формат простой: после объявления имени объекта перечисляются цвета, которые используются этим объектом. Затем с помощью цифр от 0 до 9 построчно размечаются пиксели в этой картинке, где 0 — это первый цвет, 1 — второй цвет и так далее. Точка — означает прозрачный пиксель.

В качестве цвета может использоваться набор из специальных слов, таких, как red, blue, white. Можно указать цвет в 16-ричном RGB-формате через решетку, например, #ffffff (белый).

Можно подобрать палитру цветов на сайте: lospec.com. Например, можно взять такую болотную: https://lospec.com/palette-list/swampy-summer

Или такую универсальную: https://lospec.com/palette-list/colorpop-28

Там же на сайте lospec.com есть редактор для пиксель-арта, которым вы можете воспользоваться для скетчей объектов.

Предлагаю перекрасить объекты и сделать игру на острове, где вместо стен будет открытое море.

========
OBJECTS
========

Background
#9aa208 #80b32e
11111
01111
11101
11111
10111

Target
red
.....
.000.
.0.0.
.000.
.....

Wall
#014290 #1b7aa4
10100
01010
00101
10001
00101

Player
black #d98f41 white red
.000.
.111.
22222
.333.
.3.3.

Crate
#f4da6d
00000
0...0
0...0
0...0
00000

Чтобы применить изменения в коде, нужно нажать Rebuild в верхней панели редактора.

Давайте создадим новый объект для игры: Пенёк (Stump). Мы будем позволять проходить сквозь такой объект только главному герою. Пенек будет такой своеобразной ступенькой, мешающей толкать ящики.

Stump
#6a3b19 #f5e16e #d7864a 
.000.
01110
01210
01110
.000.

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

Легенда объектов

Следующий участок кода отвечает за условные обозначения объектов в уровнях:

Здесь нужно назначить каждому объекту какой-либо символ. Это нужно для того, чтобы редактор мог в дальнейшем сохранять уровни в таком простом формате:

Обратите внимание, что @ в данном случае обозначает сочетание ящика и цели. Дело в том, что по задумке игры, уровень может начинаться с того, что некоторые ящики уже будут находиться на целях. Поэтому мы должны указать уникальный символ для такой комбинации.

Добавляем обозначение и для нашего пенька. И так, как мы предполагаем, что уровень может начинаться с того, что герой будет стоять на пеньке, добавляем обозначение для комбинации героя и пенька.

=======
LEGEND
=======

. = Background
# = Wall
P = Player
* = Crate
@ = Crate and Target
O = Target
S = Stump
% = Stump and Player

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

Wall = Wall_1 or Wall_2

Пример: Мы хотим сделать два спрайта «стен» с разной картинкой, но с одинаковым поведением. Допустим, для нашего моря мы хотим сделать разнообразие в волнах.

========
OBJECTS
========

(...)

Wall_1
#014290 #1b7aa4
10100
01010
00101
10001
00101

Wall_2
#014290 #1b7aa4
10010
10101
01011
00100
10101

(...)
=======
LEGEND
=======

. = Background
P = Player
* = Crate
@ = Crate and Target
O = Target
S = Stump
% = Stump and Player

# = Wall_1
Z = Wall_2
Wall = Wall_1 or Wall_2

В дальнейшем мы сможем обращаться к типу Wall, предполагая, что на его месте может быть либо первый вариант стены, либо второй.

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

Слои коллизий

Пропустим пока участок с описанием звуков и перейдем к описанию слоев коллизий:

В этом участке кода построчно описано, какие объекты находятся на одинаковых слоях коллизий.

Те объекты (или типы, такие как Wall), которые находятся на одной строчке, не могут проходить сквозь друг друга. В данном случае Player не может попасть на клетку со стеной или ящиком, но может перемещаться на остальные объекты. Например, герой может заходить на клетку с целью или фоном.

Также порядок слоев определяет порядок, в котором объекты рисуются на экране.

В документации сказано, что в игре обязательно должен присутствовать слой Background.

Давайте добавим пенек в отдельный слой коллизии:

================
COLLISIONLAYERS
================

Background
Target
Stump
Player, Wall, Crate

На этом этапе, если все было сделано правильно, код должен компилироваться. Нажмите Rebuild, затем Run и проверьте работоспособность игры.

Соберите уровень с новыми объектами в редакторе.

Правила и условия победы

======
RULES
======

[ > Player | Crate ] -> [ > Player | > Crate ] 

==============
WINCONDITIONS
==============

all Target on Crate

Эти два участка в коде отвечают за всю логику игры.

Правило игры имеет такой формат:

[ Условие ] -> [ Действие ]

Сейчас в правилах написано следующее:

  • Если Player двигается и на его пути движения есть Crate, то двигать Player и двигать Crate
  • Победа, если все цели находятся на клетках, на которых есть Crate

Синтаксис игры устроен хитро. Главная его фишка в том, что он заточен на эксперименты. Давайте попробуем модифицировать код, полагаясь только на интуицию, не заглядывая в документацию.

Во-первых, давайте сделаем правило, согласно которому, ящик не может попадать на клетку с пеньком. И это делается простым добавлением одной строчки:

[ > Crate | Stump ] -> [ Crate | Stump ]

Если расшифровать правило, то получается следующее: Если ящик двигается в сторону пенька, то ящик не двигается и пенек не двигается.

Чтобы применить любые изменения в коде, не забудьте нажать на Rebuild. Можно ребилдить и менять правила на ходу, пока вы играете или редактируете уровень.

Тут важно заметить, что правило для ящика должно идти после предыдущего. Так как здесь должна соблюдаться последовательность: Сначала игрок приводит в движение ящик — после чего тот уже пытается двигаться.

Ок, а что будет, если написать, что в результате двигается пенек, а не ящик:

[ > Crate | Stump ] -> [ Crate | > Stump ]

Ящик будет толкать пенек, а сам ящик в этом ходу останется на месте.

Придумываем механики из простых правил

Далее я приведу несколько простых конструкций в правилах, которые возможно сразу натолкнут вас на некоторые идеи в механиках.

Тянуть ящики

Пусть игрок не толкает ящики, а наоборот тянет их за собой:

[ < Player | Crate ] -> [ < Player | < Crate ]

То есть, если игрок двигается от ящика, то двигать Игрока и Ящик (в сторону движения игрока).

Отражение движения

Можно использовать разные направления у героя и ящика.

В этом случае получим отскакивающий ящик:

[ < Player | Crate ] -> [ < Player | > Crate ]

Телекинез

Между героем и ящиком может быть некоторое расстояние. В этом случае, правило должно выглядеть так:

(Если между героем и ящиком должна быть ровно одна клетка)
[ > Player | | Crate ] -> [ > Player | | > Crate ]
(Если расстояние между героем и ящиком 
может быть произвольным)
[ > Player | ... | Crate ] -> [ > Player | ... | > Crate ]
(Если вам не принципиально, где находится ящик.
Вы хотите, чтобы он повторял ваши движения в любой части поля)
[ > Player ] [ Crate ] -> [ > Player ] [ > Crate ]

Преследование

Можно сделать объект, который будет преследовать героя, если находится с ним на одной линии.

[ Crate | ... | Player ] -> [ > Crate | ... | Player ]

Поменяться с ящиком местами

[ > Player | Crate ] -> [ Crate | Player]

Обратите внимание, что после обработки правила герою уже не нужно движение.

Late-правила

Перед правилом может стоять ключевое слово late. Отличия таких правил от обычных в том, что late-правила выполняются уже после совершения движений.

В late-правилах не могут использоваться символы движения. При попытке использовать символ ">" в правиле, компилятор выдаст ошибку.

Late-правила используются в том случае, когда нам нужно дополнительно обработать объекты после совершения хода.

Рассмотрим некоторые примеры.

Метч-3 и мерж

Можем находить все тройки ящиков, стоящие в ряд и удалять их:

late [ Crate | Crate | Crate] -> [ | | ]
Обратите внимание, что приоритет отдается тройке слева

Если нужно обрабатывать метчи более чем в три объекта, добавляйте дополнительные правила:

late [ Crate | Crate | Crate | Crate | Crate] -> [ | | | | ]
late [ Crate | Crate | Crate | Crate] -> [ | | | ]
late [ Crate | Crate | Crate] -> [ | | ]

Можем мержить объекты в новый объект:

late [ Crate | Crate | Crate] -> [ | Crate_2 | ]
Не забудьте добавить описание нового объекта и добавить его в слои коллизии

Примеры условий для победы

Условий победы может быть несколько. Для победы нужно будет выполнить все.

Приведу примеры условий победы из документации:

(На поле не должно остаться ни одного фрукта)
No Fruit

(Все цели должны быть на клетках, где есть ящик)
All Target On Crate

(Все ящики должны быть на целях)
All Crate On Target

(Для победы испеките хотя бы один пирог)
Some Cake

(Достаточно одного ящика на цели)
Some Crate on Target

(Ни одного ящика не должно быть на цели)
No Crate on Target

Игра «Постричь весь газон»

Предположим, что на месте ящика будет газон. Задача игрока — постричь весь газон. По постриженному ходить нельзя. Условие победы — ни одного участка с заросшим газоном.

======
RULES
======
[> Player | Crate] -> [ | Player]
[> Player | no Crate] -> [Player | ]

==============
WINCONDITIONS
==============

No Crate

Принцип работы правил

Я думаю, что на этом этапе ознакомления с Puzzlescipt важно остановиться и разобраться, как движок обрабатывает правила игры.

Для этого попробуем сделать сильного героя, который мог бы толкать сразу несколько ящиков одновременно.

======
RULES
======

[> Player | Crate] -> [> Player | > Crate]
[> Crate | Crate] -> [> Crate | > Crate]

Игрок приводит в движение ящик, тот приводит в движение следующий ящик (уже на пути ящика).

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

Дело в том, что каждое правило «зацикливается» до тех пор, пока оно может быть применимо.

Еще важный момент для понимания состоит в том, что для правил лучше не использовать формулировку «Объект двигается». Корректнее говорить: «Объект ХОЧЕТ двигаться». Правила пока еще не двигают объекты, они лишь помечают объекты некими маркерами движения, куда хотят и хотят ли, вообще, эти объекты двигаться.

Думаю, станет понятнее, если мы разберем полный алгоритм обработки правил:

  • Игрок вводит команду движения с клавиатуры.
  • Объект Player получает маркер движения, соответствующий команде, введеной игроком с клавиатуры.
  • Построчно обрабатываются обычные правила игры (без слова late).
  • Если правило подходит под ситуацию на поле, оно применяется до тех пор, пока оно может быть применено.
  • Правило [> Player | Crate] -> [> Player | > Crate] интерпретируется так: Если на поле есть игрок с маркером движения и рядом с ним есть ящик, то оставить маркер движения у игрока и дать маркер движения ящику.
Ящик приобретает маркер движения
  • Так как на поле не осталось ящиков, которые стоят рядом с героем на пути его движения, то выполнятся переход к следующему правилу
  • Правило [> Crate | Crate] -> [> Crate | > Crate] интерпретируется так: Если на поле есть ящик с маркером движения и рядом с ним есть ящик, то оставить маркер движения у ящика и дать маркер движения ящику.
  • И, так как предыдущее правило повторяется до тех пор, пока оно может быть применимо, все ящики будут маркироваться движением. То есть запустится такой своеобразный цикл обработки правила
  • После того, как все обычные правила были обработаны, происходит движение всех объектов с маркерами движения
  • Если объекты не могут передвинуться из-за слоев коллизий, то они не двигаются
  • После того, как объекты были перемещены, применяются Late-правила.
  • Late-применяются также построчно, и, также как в обычных правилах, переход к новому правилу будет происходить только после того, как предыдущее правило будет «исчерпано», то есть больше не сможет быть применимо ни к одной ситуации на поле

Зная алгоритм, теперь становится понятно, почему не работает следующий код:

[ > Crate | Crate ] -> [ > Crate | > Crate ]
[ > Player | Crate ] -> [ > Player | > Crate ]

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

Помните, что игру можно сломать, если бесконечно зациклить какое-то правило. Например, так:

late [Player | Crate] -> [Crate | Player]

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

В этом случае консоль выдаст вам ошибку в конце хода: «Got caught looping...»

Для закрепления материала можно самостоятельно разобрать такой пример правила:

[ > Player | Crate ] -> [ Crate | > Player]

Что произойдет при движении героя в сторону нескольких ящиков?

Понятия Stationary и Moving

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

Moving

Предположим вы хотите создать правило, которое «приклеивает» ящик к герою, так чтобы тот повторял каждое движения героя. Это можно сделать так:

[> Player | Crate] -> [> Player | > Crate]
[< Player | Crate] -> [< Player | < Crate]
[Up Player | Crate] -> [Up Player | Up Crate]
[Down Player | Crate] -> [Down Player | Down Crate]

Также можно записать эти же правила в одну строчку:

(Копировать движение героя)
[Moving Player | Crate] -> [Moving Player | Moving Crate]

Stationary

Возьмем такое правило:

[ > Player | Crate ] -> [ > Player | > Cratе ]

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

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

[ > Player | Stationary Crate ] -> [ > Player | > Cratе ]

Второй случай применения этого ключевого слова: Когда вам нужно снять маркер движения, если он уже есть.

В этом примере ящик будет двигаться вверх при каждом ходе.

(Вешаем на ящик маркер движения вверх)
[Crate] -> [Up Crate]
(Правило, которое по сути ничего не делает)
[Crate] -> [Crate]

А в этом примере ящик уже не будет двигаться:

(Вешаем на ящик маркер движения вверх)
[Crate] -> [Up Crate]
(Снимаем маркер движения с ящика)
[Crate] -> [Stationary Crate]

Этой информации должно хватить, чтобы начать экспериментировать и делать прототип с какой-то новой механикой игры.

Если нужно более глубокое погружение в тему, то можете:

  • Изучить документацию.
  • Посмотреть примеры игр на сайте пазлскрипта. Они все имеют открытый код. Найдите интересную вам, и изучите как она устроена. В каждой игре есть ссылка на код (жмите hack внизу страницы).
  • Посмотреть игры размером в один уровень в подборке Confounding Calendar.
  • Прочитать более объемную статью-перевод на хабре, где затрагиваются многие дополнительные аспекты, такие как анимация и звуки. (Но некоторые моменты в статье, такие как порядок выполнения правил, мне были непонятны, поэтому я разобрал их у себя отдельно с картинками).

(Этот пост также является некой шпаргалкой для самого себя, поэтому он может пополняться новыми примерами.)

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