События

Создаем игру. Часть 1: Персонаж

2014-06-06

Сегодня мы приступим к созданию полноценного игрового приложения, работающего на движке Blend4Web.

Геймплей

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

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

Настройка сцены

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

Откроем файл character_model.blend и настроим персонажа. Переключим рендер а Blend4Web и выделим объект character_collider - физический объект персонажа.

На вкладке Physics выставим настройки, как на изображении выше. Обратите внимание, что тип физики должен быть либо Dynamic, либо Rigid Body, иначе персонаж будет неподвижен.

Объект character_collider является родителем для "графической" модели персонажа, которая, таким образом, будет перемещаться вслед за невидимой физической моделью. Обратите внимание, что нижние точки капсулы и аватара немного отличаются по высоте. Это было сделано для компенсации параметра Step Height, который приподнимает персонажа над поверхностью с целью преодоления невысоких препятствий.

Теперь откроем основной файл game_example.blend, из которого будем экспортировать сцену.

В этот файл подключены по ссылке следующие компоненты:

1) Из файла character_model.blend - группа объектов character.

2) Из файла main_scene.blend - группа объектов environment, в которой находятся статические модели сцены, а также их копии с материалом для соударения.

3) Из файла character_animation.blend - "запеченная" анимация character_idle_01_B4W_BAKED и character_run_B4W_BAKED.

Примечание

Добавить по ссылке компоненты из другого файла можно так: на панели инструментов нажмите File -> Link и выберите файл. Далее перейдите в соответствующий блок данных и выберите нужные элементы. Добавить по ссылке можно всё что угодно - от отдельной анимации до всей сцены.

В настройках сцены необходимо удостовериться, что флажок Enable Physics включён.

Сцена готова, перейдем к программированию.

Подготовка необходимых файлов

Поместим в корневую директорию проекта следующие файлы:

1) Движок b4w.min.js

2) Физический движок в виде двух файлов uranium.js и uranium.js.mem

Работать будем с файлами game_example.html и game_example.js.

Подключаем все необходимые скрипты в HTML-файле:

<!DOCTYPE html>
<html>
<head>
    <title>Petigor's Tale | Blend4Web</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <script type="text/javascript" src="b4w.min.js"></script>
    <script type="text/javascript" src="game_example.js"></script>

    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

        div#canvas3d {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
    </style>

</head>
<body>
<div id="canvas3d"></div>
</body>
</html>

Откроем скрипт game_example.js и добавим следующий код:

"use strict"

if (b4w.module_check("game_example_main"))
    throw "Failed to register module: game_example_main";

b4w.register("game_example_main", function(exports, require) {

var m_anim  = require("animation");
var m_app   = require("app");
var m_main  = require("main");
var m_data  = require("data");
var m_ctl   = require("controls");
var m_phy   = require("physics");
var m_cons  = require("constraints");
var m_scs   = require("scenes");
var m_trans = require("transform");
var m_cfg   = require("config");

var _character;
var _character_rig;

var ROT_SPEED = 1.5;
var CAMERA_OFFSET = new Float32Array([0, 4, 1.5]);

exports.init = function() {
    m_app.init({
        canvas_container_id: "canvas3d",
        callback: init_cb,
        physics_enabled: true,
        show_fps: true,
        autoresize: true,
        alpha: false
    });
}

function init_cb(canvas_elem, success) {

    if (!success) {
        console.log("b4w init failure");
        return;
    }

    m_data.load("game_example.json", load_cb);
}

function load_cb(root) {

}

});

b4w.require("game_example_main").init();

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

Для физического объекта персонажа объявлена глобальная переменная _character, для анимированного скелета - _character_rig. Так же объявлены две константы ROT_SPEED и CAMERA_OFFSET, которые мы используем в дальнейшем.

На этом этапе уже можно запустить приложение и посмотреть на статическую сцену с неподвижным персонажем.

Перемещение персонажа

Добавим следующий код в обработчик загрузки:

function load_cb(root) {
    _character = m_scs.get_first_character();
    _character_rig = m_scs.get_object_by_dupli_name("character",
                                                     "character_rig");

    setup_movement();
    setup_rotation();
    setup_jumping();

    m_anim.apply(_character_rig, "character_idle_01");
    m_anim.play(_character_rig);
    m_anim.set_behavior(_character_rig, m_anim.AB_CYCLIC);
}

Вначале мы сохраняем физическую модель персонажа в переменную _character. Анимируемый скелет сохраняется как _character_rig.

Последние три строки отвечают за установку стартовой анимации персонажа.

animation.apply() - устанавливает анимацию по соответствующему имени,

animation.play() - запускает её,

animation.set_behaviour() - изменяет поведение анимации, в нашем случае - на циклическое.

Перед тем как определить функции setup_movement(), setup_rotation() и setup_jumping(), важно понять как работает система сенсоров в Blend4Web. Рекомендуем прочитать соответствующую статью в руководстве пользователя. Здесь же мы ознакомимся с ней только в общих чертах.

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

Примечание

Со списком всех возможных сенсоров можно ознакомиться в соответствующем разделе документации.

Далее нужно определить логическую функцию, описывающую, в каком состоянии (true или false) должны находится определённые сенсоры в массиве, чтобы обработчик сенсора получал положительный результат. Затем следует создать обработчик, в котором будут содержаться выполняемые действия. И, наконец, нужно вызвать функцию controls.create_sensor_manifold() для создания множества сенсоров, которое и будет отвечать за обработку значений сенсоров. Посмотрим, как это будет работать в нашем случае.

Определим функцию управления setup_movement():

function setup_movement() {
    var key_w     = m_ctl.create_keyboard_sensor(m_ctl.KEY_W);
    var key_s     = m_ctl.create_keyboard_sensor(m_ctl.KEY_S);
    var key_up    = m_ctl.create_keyboard_sensor(m_ctl.KEY_UP);
    var key_down  = m_ctl.create_keyboard_sensor(m_ctl.KEY_DOWN);

    var move_array = [
        key_w, key_up,
        key_s, key_down
    ];

    var forward_logic  = function(s){return (s[0] || s[1])};
    var backward_logic = function(s){return (s[2] || s[3])};

    function move_cb(obj, id, pulse) {
        if (pulse == 1) {
            switch(id) {
            case "FORWARD":
                var move_dir = 1;
                m_anim.apply(_character_rig, "character_run");
                break;
            case "BACKWARD":
                var move_dir = -1;
                m_anim.apply(_character_rig, "character_run");
                break;
            }
        } else {
            var move_dir = 0;
            m_anim.apply(_character_rig, "character_idle_01");
        }

        m_phy.set_character_move_dir(obj, move_dir, 0);

        m_anim.play(_character_rig);
        m_anim.set_behavior(_character_rig, m_anim.AB_CYCLIC);
    };

    m_ctl.create_sensor_manifold(_character, "FORWARD", m_ctl.CT_TRIGGER,
        move_array, forward_logic, move_cb);
    m_ctl.create_sensor_manifold(_character, "BACKWARD", m_ctl.CT_TRIGGER,
        move_array, backward_logic, move_cb);
}

Создадим 4 сенсора для нажатия клавиш - стрелка вперёд, стрелка назад, S и W. Можно было бы обойтись и двумя, но мы хотим продублировать управление на символьных клавишах и на стрелках. Заносим их в массив move_array.

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

Это поведение реализуется с помощью логической функции следующего вида:

function(s) { return (s[0] || s[1]) }

Самое важное происходит в функции move_cb().

Здесь obj - наш персонаж. Аргумент pulse принимает значение 1, когда нажата какая-либо из обозначенных клавиш. По id, соответствующему одному из объявленных ниже множеств сенсоров, мы решаем, двигать персонажа вперед (move_dir = 1) или назад (move_dir = -1). Внутри этих же блоков переключаем два типа анимации: бег и анимация на месте.

Перемещение персонажа реализуется следующим вызовом:

m_phy.set_character_move_dir(obj, move_dir, 0);

В конце функции setup_movement() на персонаже создаются два множества сенсоров для движения вперёд и назад. Они имеют тип CT_TRIGGER, то есть срабатывают каждый раз при изменении значения сенсоров.

На этой стадии персонаж уже может ходить вперед и назад. Теперь добавим поворот.

Поворот персонажа

Определяем функцию setup_rotation():

function setup_rotation() {
    var key_a     = m_ctl.create_keyboard_sensor(m_ctl.KEY_A);
    var key_d     = m_ctl.create_keyboard_sensor(m_ctl.KEY_D);
    var key_left  = m_ctl.create_keyboard_sensor(m_ctl.KEY_LEFT);
    var key_right = m_ctl.create_keyboard_sensor(m_ctl.KEY_RIGHT);

    var elapsed_sensor = m_ctl.create_elapsed_sensor();

    var rotate_array = [
        key_a, key_left,
        key_d, key_right,
        elapsed_sensor
    ];

    var left_logic  = function(s){return (s[0] || s[1])};
    var right_logic = function(s){return (s[2] || s[3])};

    function rotate_cb(obj, id, pulse) {

        var elapsed = m_ctl.get_sensor_value(obj, "LEFT", 4);

        if (pulse == 1) {
            switch(id) {
            case "LEFT":
                m_phy.character_rotation_inc(obj, elapsed * ROT_SPEED, 0);
                break;
            case "RIGHT":
                m_phy.character_rotation_inc(obj, -elapsed * ROT_SPEED, 0);
                break;
            }
        }
    }

    m_ctl.create_sensor_manifold(_character, "LEFT", m_ctl.CT_CONTINUOUS,
        rotate_array, left_logic, rotate_cb);
    m_ctl.create_sensor_manifold(_character, "RIGHT", m_ctl.CT_CONTINUOUS,
        rotate_array, right_logic, rotate_cb);
}

Как видим, она очень похожа на setup_movement().

Добавился elapsed сенсор, постоянно генерирующий положительный импульс, что позволяет внутри обработчика получать время, прошедшедшее с момента отрисовки предыдущего кадра с помощью функции controls.get_sensor_value(). Это нужно для корректного вычисления скорости поворота.

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

Поворот персонажа вокруг вертикальной оси осуществляется функцией:

m_phy.character_rotation_inc(obj, elapsed * ROT_SPEED, 0)

Для регулирования скорости поворота определена константа ROT_SPEED.

Прыжок персонажа

Последняя функция управления - setup_jumping().

function setup_jumping() {
    var key_space = m_ctl.create_keyboard_sensor(m_ctl.KEY_SPACE);

    var jump_cb = function(obj, id, pulse) {
        if (pulse == 1) {
            m_phy.character_jump(obj);
        }
    }

    m_ctl.create_sensor_manifold(_character, "JUMP", m_ctl.CT_TRIGGER, 
        [key_space], function(s){return s[0]}, jump_cb);
}

Для прыжка использована клавиша пробела, при нажатии на которою вызывается метод:

m_phy.character_jump(obj)

Теперь мы можем управлять персонажем!

Перемещение камеры

Последнее, чем мы сегодня займёмся - привязка камеры к положению персонажа.

В функцию load_cb() добавим ещё один вызов: setup_camera().

Вот как выглядит эта функция:

function setup_camera() {
    var camera = m_scs.get_active_camera();
    m_cons.append_semi_soft_cam(camera, _character, CAMERA_OFFSET);
}

Константа CAMERA_OFFSET для отступа камеры назначена в 1,5 метра вверх (ось Y в WebGL) и 4 метра назад (ось Z в WebGL) от положения персонажа.

Эта функция находит на сцене активную камеру и создаёт ограничитель для плавного перемещения камеры за персонажем.

На этом мы пока остановимся. Можно запустить приложение и насладиться проделанной работой!

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

Исходные файлы приложения и сцены находятся в составе бесплатного дистрибутива Blend4Web SDK.

Изменения

[2014-06-06] Изначальная публикация.

[2014-06-09] Заменены иллюстрации в связи с обновлением контента игры.

[2014-06-25] Изменены пути к файлам.

[2014-10-20] Изменения в анимационном API. Арматурная анимация теперь применяется к скелету.

[2015-10-22] Удален модуль app.js. Заменена функция on_resize.