Recall Engine. Часть 3.

Recall Engine. Часть 3.

AmeliePick

Это третья часть из цикла статей по Recall Engine и в которой будет рассказано о возможностях модулей 2D графики и звука.

Глава 1. 2D графика.

Вся графика в Recall Engine работает на основе DirectX 12, хотя не исключена поддержка более ранних 11 и 10 версий.

Движок предоставляет довольно большой набор возможностей: создание обычного изображения, gif, разные типы кистей, геометрические фигуры и возможность рисовать собственную геометрию. А также UI элементы такие как: настраиваемые кнопки, текст с возможностью различного форматирования, и т.д. Также, ко всем этим объектам могут быть применены любые эффекты, поддерживаемые DirectX(а также встроенные): изменение цвета, трансформация и т.д.

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

Пример визуальной новеллы.

Но описание таких объектов и их функционала – это уже материал для одной из следующих статей.

 

Глава 2. Создаём сцену.

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

Пример сцены.

Сцена состоит из трёх объектов – это анимированный фон, спрайты указателя и автомобиля. При чём, для спрайтов применён эффект 3D перспективы.

Изначальные спрайты.

Начнём писать код:

D2Engine engine2D;
REGif background(engine2D.GetRender(), D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR, L"street.gif", false);

Сначала происходит инициализация 2D движка - комбинация модулей ядра, ввода-вывода, 2D графики. Далее с диска загружается гифка.

Параметры конструктора:

1 - рендер, который выделяет ресурсы,
2 - способ интерполяции текстуры - в 3D она же фильтрация текстур. Так как фоновое изображение у нас не трансформируется, используется самый дешёвый по производительности метод интерполяции - метод ближайшего соседа.
3 - путь к файлу на диске.
4 - флаг, указывающий откуда загружается файл: с диска или из ресурсов приложения.


Как и в предыдущем коде, загружаем картинку с диска и устанавливаем её в координаты x: 100; y: 310;

REImage sign(engine2D.GetRender(), D2D1_INTERPOLATION_MODE_ANISOTROPIC, L"sign.png", false);
sign.ChangePosition(100, 310);


Все графические объекты движка поддерживают эффекты DirectX, в том числе и встроенные. Один из таких встроенных эффектов, а именно 3D перспектива, нам нужен, чтобы изменить отображение указателя.

ID2D1Effect* per3D;
engine2D.GetRender()->CreateImgEffect(CLSID_D2D13DPerspectiveTransform, &per3D);
per3D->SetInput(0, sign.GetRaw());
per3D->SetValue(D2D1_3DPERSPECTIVETRANSFORM_PROP_PERSPECTIVE_ORIGIN, D2D1::Vector3F(0.0f, 192.0f, 0.0f));
per3D->SetValue(D2D1_3DPERSPECTIVETRANSFORM_PROP_ROTATION, D2D1::Vector3F(0.0f, 50.0f, 0.0f));
sign.ApplyEffect(per3D);

Рендер инициализирует эффект, затем мы настраиваем параметры перспективы и применяем к изображению указателя. Подробнее про эффекты можно почитать на MSDN. После применения эффекта к изображению, эффект освобождает занимаемые ресурсы, поэтому переменную per3D можно использовать для создания другого эффекта.

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

REImage car(engine2D.GetRender(), D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR, L"car.png", false);
car.ChangePosition(500, 350);
engine2D.GetRender()->CreateImgEffect(CLSID_D2D13DPerspectiveTransform, &per3D);
per3D->SetInput(0, car.GetRaw());
per3D->SetValue(D2D1_3DPERSPECTIVETRANSFORM_PROP_PERSPECTIVE_ORIGIN, D2D1::Vector3F(0.0f, 190.0f, 0.0f));
per3D->SetValue(D2D1_3DPERSPECTIVETRANSFORM_PROP_ROTATION, D2D1::Vector3F(0.0f, 30.0f, 0.0f));
car.ApplyEffect(per3D);


Теперь остаётся указать, чтобы все три изображения отображались. Для этого необходимо вызвать метод Draw:

background.Draw(true); sign.Draw(true); car.Draw(true);

Как можно догадаться, с помощью этого метода можно показывать и скрывать изображения.


Далее, добавляем их в рендер в определённом порядке:

engine2D.GetRender()->Add(&background);
engine2D.GetRender()->Add(&sign);
engine2D.GetRender()->Add(&car);

Очевидно, в какой последовательности будут обрабатываться объекты - самый первый будет перекрыт остальными.

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

engine2D.GetRender()->Add(DynamicLambda([&img]()
{
       static float value = 1.0f;
       static float sign  = -0.01f;
 
       value += sign;
       img.SetOpacity(value);
 
       if (value <= 0.0f)      sign = 0.01f;
       else if (value >= 1.0f) sign = -0.01f;
}));

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


Наконец, показываем окно приложения и включаем его обработку:

engine2D.GetMainWindow()->Show();
engine2D.GetMainWindow()->Processing();


Результат с мигающим фоном:

Гифка сильно сжата для телеграфа - 4,45 МБ. Лучше открыть в новом окне.


Добавим спрайт персонажа. Так как мы будем им управлять, у спрайта должна быть и анимация. Recall Engine предоставляет класс REAnimImage для создания анимации на основе спрайт-листа.

Спрайт-лист персонажа.

Добавим всё это в код:

REAnimImage anim = REAnimImage(engine2D.GetRender(), D2D1_INTERPOLATION_MODE_LINEAR, L"hero.png", false, 80,
   DynamicLambda([&anim](uint64 key)
   {
       static uint64 lastKey = key;
 
       if (lastKey != key)
       {
           switch (key)
           {
           case 0x41:
               anim.ModifyAnim(1008, 180, 144, 179, -1, 8);
               break;

           case 0x44:
               anim.ModifyAnim(144, 0, 144, 179, 1, 8);
               break;
 
           default:
               break;
           }
 
           lastKey = key;
       }

       anim.AnimStep();
   }));
anim.Draw(true);
engine2D.GetRender()->Add(&anim);

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

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

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

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

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


Глава 3. Аудио.

Дальнейшее повествование ведётся в контексте стерео.

Движок поддерживает большое количество аудио форматов и контейнеров. Так можно, например, загружать аудио в mp3, aac, flac, RIFF контейнерах(wav, avi, midi).

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

Очевидно, что есть методы для запуска, паузы, остановки проигрывания, регулировки громкости. Особое внимание уделим эффектам и средствам управления ими. Самые простые – это изменение высоты тона и панорамирование. При чём панорамирование поддерживает не только стерео и 4 канала (квадрофония), но вплоть до 7.1. К более сложным эффектам можно отнести реверберацию, 4х полосный эквалайзер и другие встроенные XAudio эффекты. Разумеется, присутствует возможность создавать и свои собственные эффекты, которые основаны на интерфейсе Audio Processing Object, поэтому информацию об их создании можно найти на MSDN.

 

Напишем код для воспроизведения музыки.

REHandle player = CreatePlayer(1, 11400, 0);

Сначала необходимо создать плеер. В данном случае создаётся плеер с одним входным каналом, частотой дискретизации в 11.4 КГц, приоритет текущего плеера по отношению к другим. Чем ниже число – тем выше приоритет. Объекты плеера могут использоваться для разного типа аудио. Например, один плеер будет играть музыку, тогда как другой может воспроизводить звуки или реплики. Благодаря данному механизму мы можем массово влиять на определённый тип аудио, например, снизить громкость только музыки или звуков в приложении.

Загрузим сам аудио файл:

REHandle music = LoadSound(player, L”track.mp3", nullptr 8);

Для этого функции передаётся, ранее созданный плеер, указывается путь до файла, далее можно указать название метки, которую движок будет искать при загрузке файла, если такая не будет найдена – файл не будет загружен. Последним параметром указывается возможное число эффектов для данного трека. В данном случае, мы можем на трек наложить до 8(включительно) эффектов.

Либо просто не указывать метку и эффекты:

REHandle music = LoadSound(player, L"music.mp3", 0);


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

SetVolume(sound, 0.5);
Pan(sound, 1.0f);

Здесь, у ранее загруженного трека, меняется громкость и панорамирование на правый канал. -2 и +2 соответственно перенесут звук полностью на левый или правый канал.


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

engine2D.GetMainWindow()->AddHandler(DynamicLambda([&sound]()
{
   static float value = 1.0f;
   static float sign  = -0.01f;
 
   value += sign;
   Pan(sound, value);
 
   if (value <= -1.9f)     sign = 0.01f;
   else if (value >= 1.9f) sign = -0.01f;
}));

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


Но если добавить ещё немного кода:

REHandle sound2 = DuplicateSound(player, sound);
SetVolume(sound2, 0.8);
Play(sound, 1);
Play(sound2, 1);

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


Глава 4. Объединяем графику и звук. 

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

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

bool isPlaying = false;
auto commonCallback = [&isPlaying, &steps, &anim](uint64 key, float delta)
{
   if (!isPlaying)
       {
           Play(steps, 1);
           isPlaying = true;
       }
 
       POINTF heroPos = anim.GetPosition();
       anim.ChangePosition(heroPos.x + delta / (key == 0x41 ? -5 : 5), heroPos.y);
       anim.Trigger((void*)key);
};

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

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

 

Добавим эту лямбду в обработчик ввода движка:

engine2D.input.AddCallback(LambdaWrapper<_void, double*>([&commonCallback](double* frameTime) // A
{
   commonCallback(0x41, *frameTime);
}, nullptr));
 
engine2D.input.AddCallback(LambdaWrapper<_void, double*>([&commonCallback](double* frameTime) // D
{
   commonCallback(0x44, *frameTime);
}, nullptr));

Метод AddCalback возвращает идентификатор добавленного обработчика. Обычно он начинается от нуля. Поэтому в данном случае, так как у нас всего два обработчика, мы можем не хранить нигде эти идентификаторы, и сами знаем их значения: это 0 для кнопки A и 1 для кнопки D.

 

Включаем обработку ввода по нажатию кнопок.

AddKeyPressedEventHandler(LambdaWrapper<void, uint32>([&engine2D](uint32 key)
{
   if (key == 0x41 || key == 0x44) // A and D.
       engine2D.input.EnableCallback(key == 0x44);
}, nullptr));


Когда кнопка будет отпущена, отключаем обработчики.

AddKeyEventHandler(LambdaWrapper<void, uint32>([&engine2D, &steps, &isPlaying, &anim](uint32 key)
{
   if (key == 0x41 || key == 0x44)
   {
       Stop(steps);
       isPlaying = false;
       engine2D.input.DisableCallback(key == 0x44);
       anim.Stop();
   }   
}, nullptr), KeyState::KEY_UP);

Здесь, мы отключаем не только сам обработчик, но также останавливаем звук шагов и анимацию. По-хорошему, тут стоит оставить только отключение обработчика ввода, а остальной код вынести в отдельную функцию, но ради экономии количества кода, оставим как есть, в конце концов это пример (>‿<)

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

 

Наконец, добавляем обработку логики – в данном случае это только опрос ввода:

engine.GetMainWindow()->OnFrame(LambdaWrapper<_void, double*>([&engine2D](double* frameTime)
{
   engine2D.input.Process(frameTime);
}, nullptr));

Теперь при отображении каждого кадра на мониторе, будет опрашиваться ввод и вся его логика для формирования следующего кадра.

 

Не забудем показать окно и включить его обработку:

engine2D.GetMainWindow()->Show();
engine2D.GetMainWindow()->Processing();


Итог:

Полный исходник находится тут.


Заключение.

Продемонстрированный в статье функционал 2D и аудио модулей далеко не всё, что имеет движок по этой теме, иначе эта статья была бы очень длинной, а читать много никто не любит (>‿<). Можно было добавить и пример кастомного аудиоэффекта, но тут пришлось бы расписать и немного теории по обработке сигнала. Написание кастомных визуальных эффектов тоже требует знаний в конкретной области.

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

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

Конец.







Report Page