разработка игр, программирование, сценарии, графика и музыка

English По-русски

Создание собственного сценарного языка для моего игрового движка

Одно из самых больших преимуществ создания собственного игрового движка — это возможность полностью контролировать процесс разработки игры. Я могу менять 3D модели, текстуры, объекты, звуки и другие данные прямо во время игры, потом нажать на клавишу и в один момент обновить все ресурсы без повторной компиляции. Это удобно и быстро. Мне такое нравится.

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

Система уже работает. Хочу поделится опытом в этой статье.

Требования

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

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

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

Поэтому мне не нужно реализовывать полноценный язык вроде Lua или hscript. Мне нужно что-то похожее на SCUMM или триггеры в Warcraft III World Editor. Вообще-то для этой цели я мог бы хранить данные в JSON файлах, как я уже делал в Hypnorain и Speebot.

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

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

Сценарий

После того, как я определил требования, я написал набросок для нового сценарного языка.

Event [enter_zone]:
	Play music [fantasy1.ogg]
	Show area name [Potion Shop]
	
Interaction with entity [npc_potion_seller] [Potion seller] hitbox [hit-box]:
	Actor [Potion seller] [friendly] [2] says [ps1 # Welcome to my potion shop, traveler. What would you like?]
	Label [Choice point]
	Option [ps2 # I'm going into battle. Give me your strongest potion.]:
		Actor [Potion seller] [grin] [2] says [ps3 # You don't know what you ask, traveler. My strongest potions will kill a dragon, let alone a man!]
		Actor [Potion seller] [friendly] [2] says [ps4 # Anything else?]
		Go to [Choice point]
	Option [ps5 # I'd like a weaker potion.]:
		Actor [Potion seller] [grin] [2] says [ps6 # That will be 10 gold coins.]
		Option [Sure. (10 coins)] if [money] >= [10]:
			Actor [Potion seller] [grin] [2] says [ps7 # A wise choice!]
			Modify [money] subtract [10]
			Obtain [weak_potion] [1]
		Option [ps8 # I don't have that much money!] if [money] < [10]:
			Actor [Potion seller] [angry] [2] says [ps9 # Stop wasting my time, traveler.]
		Option [ps10 # Maybe later.]:
			Actor [Potion seller] [disappointed] [2] says [ps11 # Very well.]
	Option [ps12 # Nothing right now.]:
		Actor [Potion seller] [friendly] [2] says [ps13 # Well, come back when you change your mind.]

Interaction with block [exit_doorway] [To market district] if ([door_unlocked] is true):
	Zone [Market District]

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

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

Самый высокий уровень файла сценария состоит из прослушивателей событий. Я могу прослушивать глобальные события, такие как "enter_zone", а также конкретные взаимодействия между сущностями. Каждый прослушиватель может дополнительно использовать условный оператор "if" и будет запускаться только в том случае, если условия соблюдены.

Переменные, такие как "money" в примере, могут быть трёх типов — booleans, integers, strings, и внутри хранятся в трёх хэш-картах менеджером сценариев игры. Инвентарь игрока хранится аналогичным образом. Эти объекты являются общими для всех скриптов и будут включены в файлы сохранения.

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

Разбор и компиляция

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

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

Сборка состоит из двух этапов:

  • Текстовый файл проанализирован. Обнаружены и удалены лишние пробелы, закомментированные строки и тому подобный мусор. Анализатор создает иерархию объектов YsComponent. Компонент содержит два массива: токены и дочерние элементы. Токены — это просто массив строк, которые я получаю, разбивая строку пробелами, за исключением того, что каждое значение (заключенное в квадратные скобки) считается одним токеном. Дочерние элементы — это команды в следующем блоке с отступом. На этом этапе компилятор может выдавать ошибки, если обнаружен неправильный отступ или квадратные скобки.
  • Дерево компонентов превращено в дерево команд. Каждый тип команды является экземпляром определенного класса, например, YsComGoTo. Каждая команда имеет свою собственную логику, которая может выполняться движком при необходимости. Если компилятор не распознает комбинацию токенов, то выдаётся ошибка. Ошибка также выдаётся, если значение имеет другой тип (например, ожидается число вместо строки). На этом этапе выполняется проверка синтаксиса каждой команды, и в итоге я получаю дерево конкретных объектов команд, которые можно выполнять при необходимости.

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

Бегуны

Теперь, когда компилятор построил дерево объектов, мне нужен способ запустить его и выполнить всю эту логику. Для этого запускается система, которую я называю "бегуном" (runner).

Всякий раз, когда ловится какое-то событие, описанное в файле скрипта, я создаю новый объект Runner, который последовательно выполняет все команды в блоке. Интересно то, что каждая команда сама решает, когда перейти к следующему шагу. Таким образом, команда под названием "Play music" начнёт воспроизведение песни и немедленно переведёт бегуна к следующей команде. После этого команда под названием "Dialog info" откроет диалоговое окно для игрока, и бегун не будет продвигаться вперед, пока игрок не прочитает сообщение и не нажмёт "ОК".

Вот пример последовательного появления двух диалоговых окон:

Event [enter_zone]:
	Label [beginning]
	Dialog info [title1 # First!] [msg1 # Informational dialog boxes can be used for tutorials.]
	Sleep [20]
	Dialog info [title2 # Second!] [msg2 # Insert insightful game tip here.]
	Sleep [60]
	Go to [beginning]

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

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

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

YumeScript для хранения данных

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

Actor type [Potion seller]:
	Name [actor_potion_seller # Potion seller]
	Emotion [grin] portrait [potion_seller_grin.png]
	Emotion [friendly] portrait [potion_seller_friendly.png]
	Emotion [disappointed] portrait [potion_seller_disappointed.png]
	Emotion [angry] portrait [potion_seller_angry.png]

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

Вывод

В целом, я очень доволен тем, как всё получилось. Этот язык — ещё один инструмент, помогающий мне в разработке игр, и он уже доказал свою полезность. Реализовать такой язык не составило особого труда и получилось довольно быстро. Теперь каждый раз, когда я внедряю новую функцию в игру, я могу добавить для неё сценарную команду, а затем сразу начать с ней экспериментировать.

Следующая статья

Работа над новой игрой-головоломкой

Подписаться

Получайте уведомления о новых статьях, чтобы:

Следить за процессом разработки моей следующей игры.

Читать статьи об искусстве и технологиях создания игр и движков.

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

Подписка бесплатная, просто нажмите на кнопку!