Создание игры на Blend4Web. Путь программиста

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

Можно сколько угодно любоваться переливами на морском дне. Пришло время реальной работы. Прежде всего нужно разобраться с программированием и сделать первые шаги в написании кода.
Так уж случилось, что я пишу код для игр исключительно на C#, а JavaScript мне мало знаком. Но изучить еще один язык программирования — несложная задача. Опыт работы с Unity позволил создать мне основные архитектурные заготовки, которые кочуют из проекта в проект, что позволяет развернуть базовый костяк нового приложения буквально за 15 минут.

Уже традиционно, игровой проект я разбиваю на несколько сцен:

  • App содержит глобальные объекты управления игровым процессом, показом рекламы, социальными функциями.
  • Splash — буферный уровень, где я обычно показываю процесс загрузки игровых данных, скачиваю рекламу или подключаюсь к социальным службам.
  • MainMenu — главное меню программы.
  • Level1x — текущий игровой уровень без GUI.
  • GameGUI — собственно игровой GUI, подгружаемый текущим уровнем.


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

Конечно, нечто подобное я хотел бы использовать и для новой игры. Но нужно учитывать возможности JavaScript и особенности работы с Blend4Web. Как выяснилось, многое из опыта программирования для Unity оказалось неприемлемо для Blend4Web. Теперь обо всем по порядку.

Платформа Blend4Web предназначена для разработки приложений WebGL. Соответственно, нужно создать небольшую обвертку в HTML. Типичный код выглядит следующим образом:





    
    
    


    


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

Интересно, что разработчики предлагают два варианта скомпилированного движка: b4w.min.js и b4w.full.min.js. По названию ясно, что первый файл — это облегченная версия. Вот только непонятно, что из нее убрали (информацию в справке так и не нашел), поэтому я решил использовать b4w.full.min.js. Кстати, для работы с физикой нужно подключать еще один файл uranium.js.

Взгляните на строку script type=«text/javascript» src=«js/game_app.js». В ней объявляется файл game_app.js — это основной скрипт для управления игрой. Я уже показывал в прошлых статьях простейшие примеры программирования для Blend4Web. Но мне бы хотелось выжать из game_app.js больше, чем простую инициализацию движка. Здесь я собираюсь сконцентрировать все глобальные функции игры.

Само программирование начинается с регистрации своего модуля в пространстве b4w. Модуль app здесь уже присутствует. Именно поэтому к названию своих скриптов я решил прибавлять префикс «game_», чтобы в дальнейшем не иметь каких-либо проблем.

"use strict"
b4w.register("game_app", function(exports, require) {
var m_app = b4w.require("app");
var m_data = b4w.require("data");
var m_main  = require("main");
exports.init = function() {
    m_app.init({
        canvas_container_id: "canvas3d",
        callback: init_cb,
        physics_enabled: false,
        autoresize: true
    });
}


Это первый этап — где регистрируется новый модуль game_app и описывается функция инициализации движка:

  • canvas_container_id — название контейнера для вывода должно совпадать с таким же в HTML.
  • callback — функция, которая будет вызвана после выполнения инициализации.
  • physics_enabled — использование физики. Пока мне она в игре не нужна, поэтому я поставил false. Учтите, что активация физики требует подключения к проекту файла uranium.js.
  • autoresize — опция, заставляющая движок автоматически изменять размеры экрана в соответствие с окном браузера. В моем случае она бесполезна, но если вы распахиваете канву на всю страницу, то без нее не обойтись.


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

// инициализация закончилась
function init_cb(canvas_elem, success) {
    loadMainMenu();
}

// загрузка главного меню
function loadMainMenu() {
    m_data.load("mainmenu.json", mainmenu_cb);
}

//загрузка меню завершена
function mainmenu_cb() {
}


Хочу остановиться на использовании GUI. Его в Blend4Web нет. Это сначала напрягает, но есть два пути решения проблемы.

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

Необходимо учитывать еще один нюанс при загрузке сцены функцией data.load. Насколько я понял — это единственный вариант, что есть в API и очень своеобразный.

m_data.load("mainmenu.json");
m_data.load("scene.json");


Как вы думаете, что произойдет при выполнении этих двух строк кода? Если вы считаете, что замена одной сцены на другую, то ошибаетесь. В действительности, в mainmenu.json добавятся элементы scene.json. Функция data.load не уничтожает предыдущую сцену, а подгружает в нее новую. Этот подход позволяет динамически загружать объекты, что очень важно при разработке веб-приложения, а дополнительные параметры функции помогают управлять этим процессом (см. справку).

Поэтому, чтобы заменить одну сцену другой — нужно уничтожить предыдущую. Для этого есть команда data.unload (id). В качестве ID можно указать порядковый номер, который сцены получают во время загрузки. К примеру, примитивный код замены может быть следующим:

data.load("mainmenu.json");
data.unload ();
data.load("scene.json");


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

Еще один момент, который меня интересовал на данном этапе — как сделать «полоску загрузки». Это меня настолько озадачило, что я перерыл официальный форум и уже собирался задать сей вопрос разработчикам, но, к счастью, не успел. Все оказалось очень просто и в справке SDK имеются соответствующие функции. Поэтому банальный вывод — смотри документацию!

Разработчики предлагают специальный модуль, который так и называется preloader. Здесь есть готовые функции для создания самых различных «полосок». Заметьте, не банальной передачи процентов загрузки файла, а полноценные, уже визуальные решения. Но я пока решил не заморачиваться и выбрал самый простой вариант.

Это пример, как создается «полоска загрузки» средствами Blend4Web:

var m_preloader = require("preloader");
//инициализация прелоадера с соответствующими параметрами
function init_cb(canvas_elem, success) {
    m_preloader.create_simple_preloader({
            bg_color:"#00000000",
            bar_color:"#FFF",
            background_container_id: "preloader",
            canvas_container_id: "canvas3d",
            preloader_fadeout: true});
    load();
}

//загрузка файла
function load() {
var p_cb = preloader_cb;
m_data.load("scene.json", load_cb,p_cb,true);
}

//обновление "полоски”
function preloader_cb(percentage) {
       m_preloader.update_preloader(percentage);
}

Так же в HTML нужно добавить отдельный контейнер для загрузчика:

    


Итак, главное меню загружено, пользователь щелкает по кнопке «Start» и переходит в игру. Эту цепочку и нужно создать.

В своей работе я всегда руководствуюсь простым правилом — не смешивать в одном «флаконе» действия от разных блоков. Один скрипт обеспечивает общее управление приложением, другой отвечает за GUI, третий за социальные функции и т.д. Такой подход позволяет быстрее находить ошибки или выполнять апгрейд программы. То же самое я хочу использовать и при программирование игры для WebGL. Поэтому обработка функций главного меню должна быть в отдельном файле.

Оказалось, что сделать это несложно. Принцип тот же, что и при создании первого скрипта.

b4w.register("game_mainmenu", function(exports, require) {
var m_mouse     = require("mouse");
var m_scenes     = require("scenes");
var mg_app     = require("game_app");

exports.mainmenu_cb = function() {
    addEventListener("mousedown", main_canvas_down);
    m_mouse.enable_mouse_hover_outline();
}

function main_canvas_down(e) {
    var x = m_mouse.get_coords_x(e);
       var y = m_mouse.get_coords_y(e);
       var obj = m_scenes.pick_object(x, y);
    var obj_name = m_scenes.get_object_name(obj);
    
    if (obj_name == "btStart") {
        m_mouse.disable_mouse_hover_outline();
        mg_app.loadScene(); 
    }
}

});


Это код целиком. Его задача — определить щелчок мыши по объекту-кнопке в меню и запустить загрузку игрового уровня.

Сначала давайте рассмотрим взаимосвязь обоих скриптов. Обратите внимание на кусок кода из файла game_app:

function loadMainMenu() {
    var p_cb = preloader_cb;
m_data.load("mainmenu.json", m_mainmenu.mainmenu_cb,p_cb,true);
}


Итак, loadMainMenu пытается загрузить сцену с кнопкой. При завершении этого процесса будет запущена m_mainmenu.mainmenu_cb. Только в данном случае реализация этой функции находится в другом модуле, который становится доступным после его подключения в заголовке:

var m_mainmenu     = require("game_mainmenu");


Сцена загружена и управление переходит к функции уже другого скрипта game_mainmenu:

exports.mainmenu_cb = function() {
    addEventListener("mousedown", main_canvas_down);
    m_mouse.enable_mouse_hover_outline();
}


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

  • addEventListener () — подписка на событие;
  • m_mouse.enable_mouse_hover_outline () — выделение объекта при нахождении курсора над ним.


Blend4Web имеет заготовки для выделения объектов в сцене. Настройка выполняется непосредственно в Blender. В панели Object нужно включить опции Selectable и Enable Outlining для примитива (см. рис). Кроме того, можно настроить толщину и цвет рамки, а также ее мерцание в панели Render. Теперь при прохождении курсора над таким объектом он будет отмечаться контуром.

image

Идем дальше. Игрок щелкнул мышкой в пределах рабочего контейнера. Генерируется событие «mousedown» и выполняется функция main_canvas_down:

function main_canvas_down(e) {
//получаем координаты мыши в пространстве XY
    var x = m_mouse.get_coords_x(e);
       var y = m_mouse.get_coords_y(e);

//Опознаем 3D объект в данных координатах
       var obj = m_scenes.pick_object(x, y);

//Узнаем его имя и загружаем сцену
    var obj_name = m_scenes.get_object_name(obj);
    if (obj_name == "btStart") {
        m_mouse.disable_mouse_hover_outline();
        mg_app.loadScene(); 
    }
}


Как видите, скрипт game_mainmenu обеспечивает работу только главного меню. Сама загрузка уровня выполняется в скрипте game_app (вызов mg_app.loadScene ()), т.е. мне удалось корректно задать скриптам задуманный функционал.

Выводы


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

Может быть стоит создать что-то типа wiki с примерами и пусть сами пользователи его заполняют? Ау, разработчики! Я лично готов закинуть пару примеров.

Тестовая сборка
Исходники

© Habrahabr.ru