Создание интерактивного приложения немыслимо без обработки событий, будь-то опрос клавиатуры или работа таймера. Реализация этих важных действий зависит от платформы, языка программирования. В этой статье я хочу рассказать о некоторых особенностях использования событий для WebGL-движка Blend4Web. К сожалению, информации по этой теме совсем немного.



Простой подход


В любом приложении есть главный цикл, который, например, опрашивает клавиатуру, выполняет некие логические действия или отвечает за движение объектов. Так, в популярном движке Unity имеется специальное событие Update (), которое разработчики используют для большинства указанных выше задач. Причем, если продолжать рассматривать подходы разработчиков Unity к этому вопросу, то наиболее важной будет парадигма “объект — событие”, т.е. когда для каждого объекта в сцене имеются управляющие скрипты и индивидуальная “подписка“ на события.

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

Вернёмся к началу статьи и рассмотрим механизм “главного цикла”.

Для не желающих изучать событийную модель, есть простейший вариант — функция append_loop_cb() из модуля main.

Пользоваться ей просто. Вызываете эту функцию, например, после инициализации движка и указываете имя обработчика в качестве параметра:
var m_main= b4w.require ("main");
m_main. append_loop_cb(update_cb);
…
function update_cb() {
//логика приложения
}

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

Функция append_loop_cb () проста в использовании и годится для несложной логики. Собственно, она считается устаревшей и оставлена в API для совместимости с ранее созданными приложениями. Сейчас появился более крутой механизм, основанный на событиях.

Событийная модель Blend4Web требует осмысления и некоторого привыкания. Но это только в начале. Главное понять принципы её работы, тогда всё становится ясным и удобным.

В основе ее находится понятие “сенсор”. Сенсор — это отдельная программная единица, ответственная за генерацию события на конкретное действие. Так, есть сенсоры клавиатуры, мыши, таймера, физики и т.д. Для создания каждого сенсора имеется своя функция. Кстати, вся подноготная событийной модели заключена в модуле controls.

Продолжим рассмотрение создания замкнутого цикла, но уже с помощью событий. Для этого в наборе сенсоров имеется специальная функция create_elapsed_sensor(). Например, так можно оформить ее вызов:
var m_ct= b4w.require ("controls");
var _elapsed_sensor = m_ctl.create_elapsed_sensor();

Сущность создана, но толка нет. И правильно, ведь на событие еще нужно подписаться. В API движка есть одна, универсальная функция, с большим количеством параметров: create_sensor_manifold(obj, id, type, sensors, logic_fun, callback, callback_param).

Весь код целиком:
var m_ct= b4w.require ("controls");

//создали сенсор
var elapsed_sensor = m_ctl.create_elapsed_sensor();

//”привязали”
m_ctl.create_sensor_manifold(null, "MAIN", m_ctl.CT_CONTINUOUS, [elapsed_sensor], null, main_cb);
…
//вызываемая функция при наступлении события
function main_cb () {
//логика приложения
}

После этого, каждый кадр будет вызываться функция main_cb, где можно разместить необходимую логику. Результат напоминает работу append_loop_cb, не правда ли? Вот только выглядит это действие несколько сложнее, но не спешите с выводами…

Вся изюминка работы с событиями заключена в параметрах, что передается в create_sensor_manifold. Разберем ключевые моменты ранее приведенного кода:

  • null. Первый параметр — это объект, который “подписывается” на событие. Здесь указан null, т.е. объектом является вся сцена.
  • “MAIN”. Уникальный идентификатор, который присваивается созданному множеству.
  • m_ctl.CT_CONTINUOUS. Тип генерации события. Вариантов несколько и в данном случае обозначает, что событие будет генерироваться постоянно. Например, если указать m_ctl.CT_SHOT, то оно произойдет однократно (упрощенно, дальше будут пояснения).
  • [_elapsed_sensor]. Ранее созданный сенсор.
  • main_cb. Функция-обработчик, вызываемая при наступлении события.


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

События в действии


Лучший способ изучения чего-то нового — это практическая работа. Можно сколь-угодно сотрясать воздух теорией, но результат должен быть осязаем. Меня всегда умиляла непосредственность юных дарований, вопрошающих на форумах: “какой игровой движок выбрать, какой лучший язык программирования, сколько можно заработать…” Совет — сядьте и делайте. Нет ничего ценнее, чем собственноручно набитые шишки.

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

Идея новогодней открытки проста и навеяна детскими воспоминаниями. Помните ключевую фразу “Раз, два, три — ёлочка гори”? Нечто подобное я решил воплотить в жизнь. Есть три кнопки, каждая из которых зажигает лампочки на елке соответствующего цвета. При этом звучит звук определенной высоты. Задача — найти правильную последовательность включения ламп. После нажатия кнопок, программа выполняет проверку результата и выводит сообщение: либо подсказку, либо переход на финал. Так как это все же больше новогодняя открытка, нежели игра, после нескольких неудачных попыток дается правильный вариант.



Движок Blend4Web не содержит встроенного GUI, поэтому предлагается два варианта: использовать объекты в Blender или работать через стандартные возможности HTML. Мне проще работать с Blender. В редакторе были созданы плоскости с текстурами кнопок и добавлены к ним анимации “выхода”. Вы можете открыть файл проекта и посмотреть, как всё сделано.

В API движка имеется специальный сенсор create_selection_sensor, который способен генерировать событие при выделении объекта мышью. Например, можно использовать такой код для отслеживания нажатия:
m_ctr = b4w.require("controls");
...
sel_sensor = m_ctr.create_selection_sensor(_btRed, true);
m_ctr.create_sensor_manifold (_btRed, "btRed", m_ctr.CT_SHOT, [sel_sensor],null, sel_sensor_cb, null);

function sel_sensor_cb () {
//обработка нажатия кнопки
}

Здесь к объекту btRed (кнопка, включающая красный свет) добавлен сенсор sel_sensor. Генерация события ограничена типом CT_SHOT, т.е. происходит однократно. Теперь при каждом щелчке мышью по кнопке будет вызываться функция-обработчик sel_sensor_cb(), в которой сосредоточена вся магия: анимация кнопки, включение подсветки вокруг нее, активация ламп в сцене.

Кто-то, наверное, уже заметил, что предложенный код имеет один существенный недостаток — это событие только выделения объекта, поэтому кнопка всегда получается включенной. Конечно, особых проблем здесь нет. Достаточно добавить в программу переменную-триггер и переключать режимы ON/OFF в зависимости от ее значения. Но задача состоит немного в другом. Необходимо однократное нажатие кнопки с последующей её блокировкой. А вот после того, как пользователь прощелкает все кнопки в сцене, нужно проверить правильность порядка и принять решение — дать ему еще шанс (выключить свет и кнопки) или включить финальный этап.

Именно здесь приходит на помощь ещё один мощный инструмент событийной модели — логическая функция:
create_sensor_manifold(obj, id, type, sensors, LOGIC_FUN, callback)

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

В состав API входит множество готовых сенсоров, но есть один, смысл которого заключается в хранении некой переменной: create_custom_sensor(value). Пользователь с помощью специальных функций может изменять и контролировать значение value в любое время. Например, ничего не стоит создать на его основе событие, которое будет генерироваться при наступлении взрыва. При этом переменная будет хранить какое-то важное значение, ту же силу взрыва. А уже на основании полученных данных, подписавшиеся объекты, могут так или иначе реагировать. К тому же, значение value можно учитывать при построении логической функции. Именно этим свойством я воспользовался, чтобы блокировать в нужный момент выполнение сенсора create_selection_sensor.

Идея заключается в том, что после выделения кнопки, вызов обработчика события наступает только в том случае, если значение custom_sensor равно нулю. Стоит только изменить его на другую цифру и обработчик блокируется.

Создание custom_sensor ничем не отличается от ранее рассмотренных сенсоров. При этом нужно сразу указать начальное значение value:
m_ctr.create_custom_sensor(0);

Так как необходимо отслеживать еще и сенсор выделения, то конструкция инициализации становится несколько иной — все сенсоры объединяются в массив:
var m_ctr = b4w.require("controls");
...
var sel_sensor = m_ctr.create_selection_sensor(_btRed, true);
_st_sens1= m_ctr.create_custom_sensor(0);
var sens = new Array ();
sens = [sel_sensor, _st_sens1];

И уже массив передается в манифолд:
m_ctr.create_sensor_manifold (_btRed, "btRed", m_ctr.CT_SHOT, sens, logic_fun, sensor_cb, null);

Все действие заключено в логической функции logic_fun (её нужно описать до вызова в коде):
var logic_fun = function(s) {
       return s[0]&& s[1]==0
}

Вызов обработчика sensor_cb произойдет, если функция logic_fun вернет оба результата работы сенсоров, как true. Логическая функция выполняется непрерывно и привязана к обновлению кадра. Поэтому считывание параметров сенсоров происходит непрерывно. Если пользователь выделит объект-кнопку на экране, то сенсор s[0] вернет true. Однако, пока равенство s[1]==0 недействительно, обработчик выполняться не будет.

Рассмотрим еще один пример использования сенсоров в приложении. Итак, пользователь нажал все три кнопки на экране. Программа должна решить, в той ли последовательности это было сделано и как отреагировать.

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

Ранее был показан код блокирования кнопок на экране с помощью сенсора custom_sensor. Для этого достаточно было в обработчике поменять значение сенсора на иное с помощью специальной функции:
m_ctr.set_custom_sensor(_st_sens1 , 1);

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

В сцене Blender было подготовлено несколько объектов, содержащих подсказки, на тот случай, если игрок неправильно угадывает комбинацию звуков. Именно они “ожидают” установки custom_sensor кнопок в 1, после чего срабатывает их собственный обработчик, выводящий подсказки на экран.

Вот код, который отвечает за проверку:
var _result_buttons = “”;
...
var logic_wrong= function(s) {
        var sum = m_ctr.get_custom_sensor(_st_sens1)+m_ctr.get_custom_sensor(_st_sens2)+m_ctr.get_custom_sensor(_st_sens3);
        s[0] = false;
        if (sum==3 && _result_buttons !="ABC") s[0] = true;
        return s[0];
    }

st_btWrong = m_ctr.create_custom_sensor(false);
m_ctr.create_sensor_manifold (_btWrong, "btWrong", m_ctr.CT_SHOT, [st_btWrong], logic_wrong, wrong_cb, null);

function wrong_cb () {
//выводим подсказку
}

Здесь используется только custom_sensor. При инициализации его значение устанавливается как false, что позволяет включить своего рода режим ожидания. Функция logic_wrong проверяет нажатие всех кнопок на экране (суммируется значение value сенсоров объектов), а также сравнивается последовательность нажатия кнопок с эталоном “ABC”. И только при соответствии всех требований, логическая функция вернет в манифолд значение true, что в свою очередь запустит обработчик wrong_cb.

Если при изучении этих примеров, вы свыклись с мыслью, что всё с событийной моделью b4w понятно, то спешу “обрадовать” — есть еще несколько нюансов, которые нужно учитывать.

В примерах создания манифолда вы уже встречали параметр type. Обычно я использовал значения CT_SHOT или CT_CONTINUOUS. Всего их в Blend4Web пять вариантов. Однако, главное здесь то, что они работают в связке с логической функцией. Так, чуть выше, функция с типом CT_SHOT вызывала обработчик st_btWrong только, если логическая функция logic_wrong возвращала true.

Список возможных значений type и их реакция на результат возвращаемый логической функцией:
  • CT_SHOT и CT_CONTINUOUS срабатывают при смене значения возвращаемого логической функцией с false на true.
  • CT_LEVEL будет постоянно вызывать обработчик с pulse=1, когда логическая функция возвращает true.
  • CT_TRIGGER вызовет обработчик, если функция изменила свое значение с false на true (вернет pulse=1) или наоборот (тогда pulse=-1). В остальных случаях обработчик не вызывается.
  • CT_CHANGE генерирует импульс при любом изменении значения сенсора, без учета результатов работы логической функции.


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

Ссылка на приложение
Архив с исходниками

Комментарии (1)


  1. gfxdevrus
    25.11.2015 10:11
    +1

    Замечательное новогоднее приложение и очень полезная статья! Спасибо!