Создание игры на 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. Теперь при прохождении курсора над таким объектом он будет отмечаться контуром.
Идем дальше. Игрок щелкнул мышкой в пределах рабочего контейнера. Генерируется событие «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 с примерами и пусть сами пользователи его заполняют? Ау, разработчики! Я лично готов закинуть пару примеров.
Тестовая сборка
Исходники