Blend4WebСтатьи

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

Автор:

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

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

Изображение

Геймплей


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


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

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

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

Откроем файл character_model.blend и настроим персонажа. Сделаем это следующим образом. Перейдем в режим Blender Game и выделим объект 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) Аддон для движка app.js

3) Физический движок uranium.js


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

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

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <script type="text/javascript" src="b4w.min.js"></script>
    <script type="text/javascript" src="app.js"></script>
    <script type="text/javascript" src="game_example.js"></script>

    <style>
        body {
            margin: 0;
            padding: 0;
        }
    </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_body;

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

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

function init_cb(canvas_elem, success) {

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

    m_app.enable_controls(canvas_elem);

    window.onresize = on_resize;
    on_resize();
    load();
}

function on_resize() {
    var w = window.innerWidth;
    var h = window.innerHeight;
    m_main.resize(w, h);
};

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

function load_cb(root) {

}

});

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

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

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

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

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

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

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

function load_cb(root) {
    _character = m_scs.get_first_character();
    _character_body = m_scs.get_object_by_empty_name("character",
                                                     "character_body");

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

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

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

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

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

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

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

Примечание

Обратите внимание, что применять скелетную анимацию нужно именно на объекте персонажа, на котором в Blender'е установлен модификатор Armature.

Перед тем как определить функции 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_body, "character_run");
                break;
            case "BACKWARD":
                var move_dir = -1;
                m_anim.apply(_character_body, "character_run");
                break;
            }
        } else {
            var move_dir = 0;
            m_anim.apply(_character_body, "character_idle_01");
        }

        m_phy.set_character_move_dir(obj, move_dir, 0);

        m_anim.play(_character_body);
        m_anim.set_behavior(_character_body, 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.

#Blend4Web, #game, #WebGL, #You Won't Fry Me

15 августа 2014 (Обновление: 5 сен 2014)