Recall Engine. Часть 4.

Recall Engine. Часть 4.

AmeliePick

В этой статье по движку Recall ознакомимся с инструментами для создания визуальной новеллы, а именно:

  1. Скриптовый язык.
  2. Формат сценария.
  3. Система сборки RESBuild.
  4. API модуля визуальной новеллы.

Также напишем простой пример новеллы.


Глава 1. Структура.

1.1 Модуль ВН.

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

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


1.2. Система скриптинга.

Recall имеет свою собственную систему скриптинга и кодогенерации, которая является довольно универсальной. С её же помощью создаются игры и моды к ним. Так как движок является инструментом для создания и просто прикладного ПО, а скриптовый язык основан на C++, то с помощью этого скрипта можно сделать и обычное приложение. Также планируется использовать этот язык для системы разметки для проектирования графического интерфейса.

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

Одним из преимуществ данной системы, над, например, популярным движком для новелл RenPy, является разделение сценария и кода. Так, код скрипта новеллы не содержит текста самой новеллы, что делает написание кода более проще и читабельней, к тому же упрощается написание самого сценария, что может сэкономить время, ведь сам сценарист может писать текст по заданному формату. К ещё одному преимуществу, можно отнести тот факт, что все скрипты являются компилируемыми, в отличии от RenPy, где всё работает на Python. Если читатель устанавливал хоть раз моды для The Witcher 3, то может помнить, как при запуске игры, сперва появлялось уведомление о том, что движок компилирует скрипты:

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


1.3 Требования.

Все скрипты должны находиться в файлах с расширением revns(Recall Engine Visual Novel Script), а текст сценария в одном единственном файле с расширением revnt(Recall Engine Visual Novel Text).


Глава 2. Разработка скрипта. Обзор.

2.1 Создание объектов.

Вся игра может состоять как из одного файла, так и из нескольких. Основной файл, в котором находится точка входа, должен всегда называться script.revns. Объявление переменных и прочих объектов всегда происходит в файле init.revns. Разумеется, всё содержимое этого файла будет видно в любом другом файле, поэтому нет необходимости вручную подключать его.

Создадим двух персонажей:

VNCharacter anja  {L"Anja", Color::Red};
VNCharacter erika {L"Erika", Color::ForestGreen};

Модуль визуальной новеллы имеет встроенные объекты, один из таких – класс VNCharacter, который необходим для представления персонажа. В нём содержится его имя, спрайты. В файл init можно добавлять переменные и стандартных типов:

int karma = 0;
void* ptr = nullptr;


2.2 Код новеллы.

Система скриптинга имеет несколько требований, первое касается названий файлов и упомянуто было немного ранее. Второе требование — файл с названием script.revns обязательно должен иметь токен start, который обозначает точку входа в скрипт и откуда начинает выполняться весь код. Токены оборачиваются знаком $. С их помощью можно создавать, например, обычные функции:

$functionExample$
{
   // do something
   return;
}
 
 
$start$
   functionExample();
   // do something
   return;

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

Пример скрипта новеллы:

$start$ // Required token.
intro:
{
   ChangeBG("River");
   ShowText(1, 5);
   сh1.ChangeSprite("AnjaSmile");
   ch1.Move(50, 50, 2000, true);
   ChangeCS("AnjaSC");
 
   if (karma < 0)
       goto day_1;
}

Внутри токенов можно объявлять метки(блоки) на которые можно переключаться по ходу выполнения. В данном примере из метки intro, символизирующее начало игры, происходит переход на метку day_1, суть которой понятна из её названия. В самом же коде мы можем менять фон и сцену через функции ChangeBG и ChangeSC соответственно. Чтобы выводить текст самой новеллы на экран, используется функция ShowText. Первый аргумент указывает с какой строки выводить текст, второй аргумент указывает по какую строку. То есть диапазон 1; 5; указывает выводить текст с первой строки по строку 5 или до 6 строки. Да, начало отсчёта тут не с нуля, а с единицы, что сделано для удобства, так как текстовые редакторы показывают первую строку как... первую, а не нулевую. После того, как пользователю будет показан весь текст, выполнение возвращается из функции ShowText в скрипт новеллы. Например, вывели пару строк, в которых о чём-то рассказывается, изменили спрайты персонажей под ситуацию. Для перемещения персонажа по экрану, используется метод Move, где первые два аргумента это x и y координаты экрана, третий аргумент – время в миллисекундах, в течении которого, спрайт меняет свою позицию. Последний аргумент указывает, выполнять ли перемещение асинхронно или нет. С помощью этой же функции можно реализовывать и простые анимации. В скрипте можно использовать не только условия, но и вызывать какие-либо функции из самого C++ или его библиотек, если они не запрещены политикой безопасности движка. Например, получим текущее имя пользователя ПК.

#include <Windows.h>
#pragma comment(lib, “Advapi32.lib”)

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

Здесь либо сразу указывается путь до библиотеки, либо просто её название. Если же мы указываем просто название, то в дальнейшем(см. главу 4.) путь к этой библиотеке необходимо указать в манифесте сборки.

$start$
   char buffer[100];
   DWORD size = 100;
   if (GetUserName(buffer, &size))
       AppendToText(strcat(buffer, “ твоё настоящие имя?”))

Получаем строку, которая содержит имя пользователя, конкатенируем его с каким-либо текстом(при необходимости) и выводим на экран.


Глава 3. Пишем сценарий.

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

anja: Hello!
erika: Oh! Hi there!

Сперва указывается имя переменной персонажа, определённой в файле init.revns, затем следует сама реплика персонажа. Если необходим просто повествующий текст, то в init необходимо определить соответствующий объект, где вместо имени персонажа можно оставить пустую строку и не указывать спрайты:

Character Author("", Color::Gray);
Character HeroMind("Mind", Color::Yellow);

Также, можно использовать и разные стили текста, и задержку вывода содержимого на экран с помощью тегов:

+---+-------------------------------------+
| w | wait, ожидание ввода пользователя.  |
+---+-------------------------------------+
| i | выделить текст курсивом             |
+---+-------------------------------------+
| b | выделить текст жирным шрифтом.      |
+---+-------------------------------------+
| u | подчеркнуть текст.                  |
+---+-------------------------------------+

Пример:

erika: Привет Аня!{w} Я вечером посмотрела фильм «{i}Невероятный Батон{i}»

На момент написания данной статьи(Июль 2024), к одному диапазону текста может быть применён только один тег стиля.


Глава 4. Система сборки.

В начале статьи эта тема была поверхностно затронута, теперь разберём её подробнее. Движок Recall имеет свою систему сборки RESBuild, которая опирается на манифест сборки. Любой проект, который собирается этой системой, должен иметь следующую структуру:

Root
├── RESBuild - директория с файлами сборщика.
└── UserProject
   ├── Build - каталог сборки
   │  ├── tmp - каталог временных файлов.
   │  ├── BuildManifest.ini
   │  └── script.dll - собранный и готовый код.
   ├── script - здесь находятся сами скрипты.
   │  ├── init.revns
   │  ├── script.revns
   │  └── userDefinedScriptModule.revns
   └── scenario.revnt - текст новеллы.

Каталог временных файлов будет создан автоматически после кодогенерации. Он необходим будет для компилятора и компоновщика. После того, как будет собрана библиотека dll, временный каталог будет удалён. BuildManifest.ini – сам манифест сборки, рассмотрим его подробнее. Типичное его содержание выглядит следующим образом:

[Compiler]
add_dirs = "D:\Game\src"
 
[Linker]
add_libs = "BaseKernel.lib" "IOAPI.lib"
add_lib_dirs = "D:\Game\libs" # equal to /LIBPATH:

Секция Compiler описывает параметры компилятора. Так параметр add_dirs или Additional Directories, соответствует аргументу /I компилятора и указывает дополнительные каталоги включаемых файлов.

Секция Linker содержит параметры для компоновщика.

Параметр add_libs(Additional Libraries) указывает дополнительные зависимости. Именно в этот параметр необходимо вписать библиотеку Advapi32.lib из примера, где мы получали имя пользователя, если мы в том коде не использовали pragma comment с типом lib.

add_lib_dirs – указывает каталоги зависимостей.

Сама система сборки уже содержит некоторые стандартные библиотеки, но, если среди них нет нужной, её необходимо добавить и в параметр прописать путь до каталога с файлом. Именно сюда добавляется путь из примера с именем пользователя, если в коде не использовалась прагма вообще, либо было указано только название библиотеки. Подробнее об использовании компилятора и компоновщика всегда можно почитать на MSDN.

Для всех перечисленных трёх параметров можно указывать несколько значений, это осуществляется путём вставки пробела между ними(см. параметр add_libs в листинге).

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

 

4.1 Компилируем и собираем.

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

#include "VNAPI/VNAPI.h"
#include "Engine/CodeGen/Script.h"
 
int main()
{
   VNAPI vnAPI;
   ParseVNStoryData("Game\\GameStory.revnt");
   ParseScriptData("Game\\GameMain.revns");
   return 0;
}

Сперва инициализируется модуль визуальной новеллы, который запускает необходимые части движка. Далее вызываются две функции парсинга, с указанием файлов. Функцию для скрипта необходимо вызывать для каждого файла с кодом. После завершения выполнения этих функций, в директории Game\Build\tmp будут находиться готовые файлы в формате C++. Кодогенератор старается генерировать код, максимально совместимый с компилятором, а потому выходные файлы будут иметь кодировку и кодовую страницу, которая по умолчанию применена для пользователя. Так, для русской версии Windows скорее всего это будет windows-1251. Так как разработчик может использовать в скрипте вместе с латинскими символами, символы других алфавитов, то во избежание двойной кодировки, кодогенератор преобразовывает файл в кодировке по умолчанию для пользователя, что полностью соответствует нотации к компилятору.

Теперь можно было бы сделать следующее:

   Compile("Game\\GameMain.cxx");
   Link("Game");
   LoadScript("Game");

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

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

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

 

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


Глава 5. Новелла.

Напишем небольшую новеллу из трёх частей. Здесь разберём только одну часть и запуск кода, чтобы не увеличивать статью. Тем более, основной смысл не меняется от части к части, а в исходниках довольно самодокументирующийся код.


5.1 Скрипт.

#include "AudioAPI/AudioAPI.h"
 
static REHandle Music = nullptr;
static REHandle nightCityAmbient = nullptr;
static REHandle scene3Music = nullptr;

Так как в скрипте необходимо загружать аудиофайлы и работать с ними, то подключаем сперва API аудио модуля движка. Затем определяем три переменные. Они используются только внутри этого файла, поэтому нет необходимости объявлять их в init файле.

Так как новелла состоит из трёх частей, то каждая часть – это просто функция:

  game.Mizuhara.ChangeSprite(L"MizuharaNormal");
  game.Mizuhara.Hide();
  game.Mizuhara.Move(-100, 0, 100, false);
   
  ChangeBG(L"Town");
  REAudio::Play(scene3Music, 1);
  REAudio::Play(nightCityAmbient, -1);
   
   
  ShowText(36, 37);
  game.Mizuhara.Show();
  game.Mizuhara.Move(800, 0, 3000, true);
  ShowText(38, 46);

Рассмотрим пример 3 сцены, так как он довольно короткий, но показывает работу со спрайтами, звуком.

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

Далее меняем фон, указывая его название. Где название, это имя файла изображения. Town.jpg = Town. Затем начинаем воспроизводить два звука: музыку и фон города. Два этих звука находятся в разных аудио потоках и это будет рассмотрено позже.

Выводим на экран две строки, включаем отображение спрайта и в течении трёх секунд, асинхронно перемещаем изображение примерно в центр экрана. Так как операция Move выполняется асинхронно, то мы продолжаем выводить текст с 38 по 46 строку.

 

Теперь рассмотрим точку входа в скрипт:

$start$
   Music = REAudio::CreatePlayer(2, 11400, 1);
   REAudio::SetPlayerVolume(Ambient(), 0.5);
   REAudio::SetPlayerVolume(Music, 0.6);
 
   LoadBGImages(LoadMode2D::APPEND, { L"Game\\res\\BG\\Town.jpg" });
   game.Mizuhara.LoadSprites(LoadMode2D::APPEND, { L"Game\\res\\Sprites\\MizuharaNormal.png" });
   
   // Загружаем звук в поток Ambient
   nightCityAmbient = REAudio::LoadSound(Ambient(), L"Game\\res\\Audio\\nightcityAmbient.mp3", 0);
   scene3Music = REAudio::LoadSound(Music, L"Game\\res\\Audio\\scene3Music.mp3", 0);
   
   scene_3(game);

Сперва инициализируем плеер(он же поток) для воспроизведения музыки. Вообще, в движке определены три потока для: звука, музыки, фона. Но здесь это показано как пример. Так как всегда может понадобиться создать дополнительный плеер. В параметрах плеера указывается, что на вход подаётся двухканальное аудио, частота дискретизации 11.4 кГц(что равно примерно ¼ от частоты Audio CD и уже может требовать довольно качественные треки) и приоритет плеера равен одному. Далее устанавливается громкость каждого потока. Где значение 1.0 это условные 100%, и свыше этого значения, движок будет усиливать звук вплоть до 145 дБ. Поэтому, при установке значения больше 1.0, движок запросит подтверждение на усиление звука.

Загружаем изображение фона и спрайта персонажа. В обоих функциях, первый параметр может иметь два значения: CLEAR и APPEND. Последний просто добавляет загруженный спрайт к списку. CLEAR же очищает список и выгружает спрайты из памяти. Это может быть применимо к разным сценам, где используется определённый набор спрайтов, который более нигде не нужен будет. А также для экономии памяти.

Далее происходит загрузка mp3 файлов, при чём один загружается в поток Ambient, а другой в поток для музыки.

В конце вызывается функция сцены.


5.2 Приложение и запуск.

После того, как у нас есть код игры, его надо внедрить в приложение и настроить ещё несколько элементов.

#include "VisualNovel/VisualNovel.h"
#include "VisualNovel/UI/UI.h"
#include "Engine/CodeGen/Script.h"
#include "KernelAPI/Multithreading/Thread.h"


extern void ScriptMain();
extern TextBox* textBox;
int VisualNovelDemo()
{
  VisualNovel vnAPI;
  vnAPI.GetMainWindow()->ChangeSize(1920, 1080);

  SkipKeys({ VK_SPACE });
   
  RESolidBrush2D bursh(vnAPI.GetRender(), D2D1::ColorF::White);
  CreateTextBox({ 0, 900, 1900, 1080 }, { 100, 900, 1920, 1080 }, L"Arial", 24, 30, &bursh, &bursh, 1.0, L"res\\textBlockFHD.png");
  textBox->GetInfo()->ChangePosition(72, 880);
   
  REThread::Create(&ScriptMain, nullptr, true);

  vnAPI.GetMainWindow()->Show();
  vnAPI.GetMainWindow()->FullScreen();
  vnAPI.GetMainWindow()->Processing();

  return 0;
}

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

Функция SkipKeys принимает список из кнопок, которые будут проматывать и пропускать текст – в данном случае это только пробел. Но при желании можно добавить, например, ЛКМ, тогда при нажатии на кнопку мыши, текст также будет проматываться.

Затем создаётся кисть, которая используется в функции CreateTextBox. Эта функция создаёт объект текстового поля, куда будет выводиться весь текст.

Её параметры:

1) Координаты и размер основного текстового блока, 2) Координаты и размер текстового блока, в котором находится имя персонажа, 3) Название шрифта, 4) Размер основного текста, 5) Размер текста для имени, 6, 7) Два параметра для цвета текста, первый для основного, второй для имени, 8) Прозрачность текста, 9) Путь к фоновой текстуре.

Далее создаётся поток и сразу начинает выполняться, и в котором вызывается функция ScriptMain – точка входа в скрипт. Она не принимает никаких аргументов, поэтому ей передаётся ноль при создании потока.

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

Ссылки на исходники: story.revntinit.revsscript.revnsmain.cpp

Copyright

Все спрайты и некоторые фоны были сгенерированы нейросетью. Авторы фонов во 2 и 3 сценах неизвестны. Анимация танца была записана в Cyberpunk 2077.

Музыка:

  1. Lo-Fi сгенерированный Suno AI.
  2. DIGITAL REY - LAST WISH
  3. Little V – I Really want to stay at your house.
  4. upusen - Halogen Lights


Заключение.

Подводя итог данной статьи, можно сказать, что Recall обзавёлся очень гибкой и простой в использовании, системой написания кода, которая может использоваться при разработке игр и модов к ним, а также при создании обычного прикладного ПО. По мере развития движка, система будет дальше модернизироваться и улучшаться, равно как и модуль ВН.

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

Дорожную карту проекта, как всегда, можно найти на своём месте.

Конец.







Report Page