События

Создаем игру. Часть 4: Мобильные устройства

2014-06-25

Это четвертая часть серии уроков по созданию игры с помощью движка Blend4Web. Сегодня мы добавим поддержку мобильных устройств и запрограммируем управление персонажем с помощью касаний. Перед прочтением этой статьи рекомендуется ознакомиться с первым уроком из данного цикла, в которой рассказывается о реализации управления на базе клавиатуры. Тестирование будем производить на платформах Android и iOS 8.

Определение мобильных устройств

Поскольку мобильные устройства обычно являются менее производительными, чем настольные компьютеры, нам потребуется снизить настройки качества для них. Чтобы проверить, является ли используемое устройство мобильным, воспользуемся функцией detect_mobile():

function detect_mobile() {
    if( navigator.userAgent.match(/Android/i)
     || navigator.userAgent.match(/webOS/i)
     || navigator.userAgent.match(/iPhone/i)
     || navigator.userAgent.match(/iPad/i)
     || navigator.userAgent.match(/iPod/i)
     || navigator.userAgent.match(/BlackBerry/i)
     || navigator.userAgent.match(/Windows Phone/i)) {
        return true;
    } else {
        return false;
    }
}

Функция инициализации примет следующий вид:

exports.init = function() {

    if(detect_mobile())
        var quality = m_cfg.P_LOW;
    else
        var quality = m_cfg.P_HIGH;

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

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

Элементы управления на HTML-страницe

Дополним HTML-файл следующими элементами:

<!DOCTYPE html>
<body>
    <div id="canvas3d"></div>

    <div id="controls">
        <div id ="control_circle"></div>
        <div id ="control_tap"></div>
        <div id ="control_jump"></div>
    </div>
</body>

1) Элемент control_circle будет появляться в точке касания экрана для указания на возможные направления перемещения.

2) Элемент control_tap - небольшой маркер, следующий за пальцем при его перемещении.

3) Элемент control_jump - кнопка, расположенная в правом нижнем углу экрана. При нажатии кнопки будет происходить прыжок.

По умолчанию все добавленные элементы скрыты (свойство visibility). Их обработка будет происходить после загрузки сцены.

Стили, использованные для данных элементов, можно найти в соответствующем файле game_example.css.

Обработка touch-событий

Обратимся к обработчику загрузки. Его код теперь выглядит так:

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

    var right_arrow = m_ctl.create_custom_sensor(0);
    var left_arrow  = m_ctl.create_custom_sensor(0);
    var up_arrow    = m_ctl.create_custom_sensor(0);
    var down_arrow  = m_ctl.create_custom_sensor(0);
    var touch_jump  = m_ctl.create_custom_sensor(0);

    if(detect_mobile()) {
        document.getElementById("control_jump").style.visibility = "visible";
        setup_control_events(right_arrow, up_arrow,
                             left_arrow, down_arrow, touch_jump);
    }

    setup_movement(up_arrow, down_arrow);
    setup_rotation(right_arrow, left_arrow);

    setup_jumping(touch_jump);

    setup_camera();
}

Как видим, появились пять новых сенсоров, создаваемых с помощью метода controls.create_custom_sensor(). Их значение мы будем изменять самостоятельно при возникновении соответствующих touch-событий.

Если функция detect_mobile() возращает true - на экран выводится элемент управления control_jump и вызывается функция setup_control_events(), отвечающая за установку значений новых сенсоров. Эти сенсоры и являются её параметрами. Данная функция довольно объемная, поэтому рассмотрим её содержимое по порядку.

var touch_start_pos = new Float32Array(2);

var move_touch_idx;
var jump_touch_idx;

var tap_elem = document.getElementById("control_tap");
var control_elem = document.getElementById("control_circle");
var tap_elem_offset = tap_elem.clientWidth / 2;
var ctrl_elem_offset = control_elem.clientWidth / 2;

Вначале создаются буферные переменные для хранения точки касания и индексов касаний, отвечающих за перемещение и прыжок. HTML-элементы tap_elem и control_elem понадобятся нам в нескольких обработчиках.

Обработчик touch_start_cb()

В этой функции происходит обработка начала касания.

function touch_start_cb(event) {
    event.preventDefault();

    var h = window.innerHeight;
    var w = window.innerWidth;

    var touches = event.changedTouches;

    for (var i = 0; i < touches.length; i++) {
        var touch = touches[i];
        var x = touch.clientX;
        var y = touch.clientY;

        if (x > w / 2) // right side of the screen
            break;

        touch_start_pos[0] = x;
        touch_start_pos[1] = y;
        move_touch_idx = touch.identifier;

        tap_elem.style.visibility = "visible";
        tap_elem.style.left = x - tap_elem_offset + "px";
        tap_elem.style.top  = y - tap_elem_offset + "px";

        control_elem.style.visibility = "visible";
        control_elem.style.left = x - ctrl_elem_offset + "px";
        control_elem.style.top  = y - ctrl_elem_offset + "px";
    }
}

В цикле происходит перебор всех изменившихся касаний event.changedTouches. Отбросим все касания, произошедшие в правой части экрана:

    if (x > w / 2) // right side of the screen
        break;

Если это условие выполнено, запомним точку начала касания touch_start_pos и индекс этого касания move_touch_idx. После этого в точке касания отобразим два элемента: control_tap и control_circle. На экране мобильного устройства это будет выглядеть следующим образом:

Обработчик touch_jump_cb()

function touch_jump_cb (event) {
    event.preventDefault();

    var touches = event.changedTouches;

    for (var i = 0; i < touches.length; i++) {
        var touch = touches[i];
        m_ctl.set_custom_sensor(jump, 1);
        jump_touch_idx = touch.identifier;
    }
}

Этот обработчик вызывается при касании кнопки control_jump:

Всё, что он делает - изменяет значение сенсора jump на 1 и сохраняет индекс соответствующего касания.

Обработчик touch_move_cb()

Эта функция во многом похожа на touch_start_cb(). Она обрабатывает перемещение пальца по экрану.

    function touch_move_cb(event) {
        event.preventDefault();

        m_ctl.set_custom_sensor(up_arrow, 0);
        m_ctl.set_custom_sensor(down_arrow, 0);
        m_ctl.set_custom_sensor(left_arrow, 0);
        m_ctl.set_custom_sensor(right_arrow, 0);

        var h = window.innerHeight;
        var w = window.innerWidth;

        var touches = event.changedTouches;

        for (var i=0; i < touches.length; i++) {
            var touch = touches[i];
            var x = touch.clientX;
            var y = touch.clientY;

            if (x > w / 2) // right side of the screen
                break;

            tap_elem.style.left = x - tap_elem_offset + "px";
            tap_elem.style.top  = y - tap_elem_offset + "px";

            var d_x = x - touch_start_pos[0];
            var d_y = y - touch_start_pos[1];

            var r = Math.sqrt(d_x * d_x + d_y * d_y);

            if (r < 16) // don't move if control is too close to the center
                break;

            var cos = d_x / r;
            var sin = -d_y / r;

            if (cos > Math.cos(3 * Math.PI / 8))
                m_ctl.set_custom_sensor(right_arrow, 1);
            else if (cos < -Math.cos(3 * Math.PI / 8))
                m_ctl.set_custom_sensor(left_arrow, 1);

            if (sin > Math.sin(Math.PI / 8))
                m_ctl.set_custom_sensor(up_arrow, 1);
            else if (sin < -Math.sin(Math.PI / 8))
                m_ctl.set_custom_sensor(down_arrow, 1);
        }
    }

d_x и d_y - смещение по двум осям относительно начальной точки касания. По приращениям вычисляется расстояние до этой точки, косинус и синус направления от начальной точки на палец. Эти данные позволяют полностью описать требуемое поведение в зависимости от расположения пальца с помощью несложных тригонометрических преобразований.

Как видим, в итоге окружность разбивается на восемь частей, каждой из которых соответствует свой набор значений сенсоров right_arrow, left_arrow, up_arrow, down_arrow.

Обработчик touch_end_cb()

Обработчик сбрасывает значения сенсоров и сохраненные индексы событий.

    function touch_end_cb(event) {
        event.preventDefault();

        var touches = event.changedTouches;

        for (var i=0; i < touches.length; i++) {

            if (touches[i].identifier == move_touch_idx) {
                m_ctl.set_custom_sensor(up_arrow, 0);
                m_ctl.set_custom_sensor(down_arrow, 0);
                m_ctl.set_custom_sensor(left_arrow, 0);
                m_ctl.set_custom_sensor(right_arrow, 0);
                move_touch_idx = null;
                tap_elem.style.visibility = "hidden";
                control_elem.style.visibility = "hidden";

            } else if (touches[i].identifier == jump_touch_idx) {
                m_ctl.set_custom_sensor(jump, 0);
                jump_touch_idx = null;
            }
        }
    }

Также для события move скрываются требуемые элементы управления:

    tap_elem.style.visibility = "hidden";
    control_elem.style.visibility = "hidden";

Назначение обработчиков для touch-событий

Последнее, что происходит в функции setup_control_events() - назначение обработчиков на соответствующие touch-события:

    document.getElementById("canvas3d").addEventListener("touchstart", touch_start_cb, false);
    document.getElementById("control_jump").addEventListener("touchstart", touch_jump_cb, false);

    document.getElementById("canvas3d").addEventListener("touchmove", touch_move_cb, false);

    document.getElementById("canvas3d").addEventListener("touchend", touch_end_cb, false);
    document.getElementById("controls").addEventListener("touchend", touch_end_cb, false);

Обратите внимание, что событиe touchend определяeтся для двух HTML-элементов. Это связано с тем, что пользователь может отпустить палец как на элементе controls, так и вне этого элемента.

На этом работа с событиями завершена.

Включение touch-сенсоров в модель управления

Осталось добавить созданные сенсоры в существующую модель управления. Рассмотрим изменения на примере функции setup_movement().

function setup_movement(up_arrow, down_arrow) {
    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, up_arrow,
        key_s, key_down, down_arrow
    ];

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

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

        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);

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

Как видим, изменился лишь набор сенсоров в массиве move_array и логические функции forward_logic() и backward_logic(), которые теперь зависят и от touch-сенсоров.

Аналогичным образом изменились и функции setup_rotation() и setup_jumping(). Их листинг приведен ниже:

function setup_rotation(right_arrow, left_arrow) {
    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, left_arrow,
        key_d, key_right, right_arrow,
        elapsed_sensor,
    ];

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

    function rotate_cb(obj, id, pulse) {

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

        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);
}

function setup_jumping(touch_jump) {
    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, touch_jump], function(s){return s[0] || s[1]}, jump_cb);
}

Еще немного о камере

Напоследок вернемся к камере. На основе отзывов сообщества было принято решение добавить возможность регулировки жесткости привязки камеры к персонажу. Теперь вызов этой функции выглядит так:

    m_cons.append_semi_soft_cam(camera, _character, CAM_OFFSET, CAM_SOFTNESS);

Константа CAM_SOFTNESS определена в начале файла, её значение равно 0.2.

Заключение

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

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

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

Изменения

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