Создаем игру. Часть 5: Опасный мир
2014-07-11
Мы продолжаем увлекательный процесс создания мини-игры на движке Blend4Web. На этот раз введем элементы геймплея: появятся раскаленные глыбы, падающие с неба и наносящие урон персонажу. Кроме того, очки жизни будут уменьшаться и при контакте с лавой.
Новые объекты на сцене в Blender
Подготовим новые игровые объекты в файле blend/lava_rock.blend:
1) Три разновидности падающих глыб: rock_01, rock_02, rock_03.
2) Дымовые шлейфы для этих глыб - эмиттеры трех одинаковых систем частиц, привязанные к объектам глыб связью родитель-потомок: smoke_emitter_01, smoke_emitter_02, smoke_emitter_03.
3) Cистемы частиц для эффекта взрыва глыб: burst_emitter_01, burst_emitter_02, burst_emitter_03.
4) Метки, появляющиеся под падающими камнями: mark_01, mark_02, mark_03.
О том, как были созданы эти объекты, мы расскажем в одной из следующих статей.
Для удобства скомпонуем все эти объекты в одну группу lava_rock, и добавим ее по ссылке в основной файл game_example.blend. Увеличим количество всех объектов вдвое - сделаем копию пустого объекта с прикрепленной к нему группой. В итоге на сцене будет присутствовать пул из 6 падающих глыб, доступ к которым мы получим по именам двух пустых объектов - lava_rock и lava_rock.001.
Полоса здоровья
Добавим четыре HTML элемента для отрисовки полосы здоровья.
<div id ="life_bar">
<div id ="life_bar_main"></div>
<div id ="life_bar_green"></div>
<div id ="life_bar_red"></div>
<div id ="life_bar_mid"></div>
</div>
Эти элементы будут перемещаться при получении персонажем урона. В файл game_example.css добавим соответствующие описания стилей.
Константы и переменные
Первое, что мы сделаем - инициализируем несколько новых констант для настройки геймплея, а также глобальную переменную для хранения количества очков жизни персонажа:
var ROCK_SPEED = 2;
var ROCK_DAMAGE = 20;
var ROCK_DAMAGE_RADIUS = 0.75;
var ROCK_RAY_LENGTH = 10;
var ROCK_FALL_DELAY = 0.5;
var LAVA_DAMAGE_INTERVAL = 0.01;
var MAX_CHAR_HP = 100;
var _character_hp;
var _vec3_tmp = new Float32Array(3);
Типизированный массив _vec3_tmp создается для хранения промежуточных результатов вычислений, с целью уменьшения нагрузки на сборщик мусора JavaScript движков.
В функции load_cb() установим переменной _character_hp значение MAX_CHAR_HP - в начале игры персонаж полностью здоров.
_character_hp = MAX_CHAR_HP;
Падение камней - инициализация
Список вызываемых функций теперь выглядит следующим образом:
var elapsed_sensor = m_ctl.create_elapsed_sensor();
setup_movement(up_arrow, down_arrow);
setup_rotation(right_arrow, left_arrow, elapsed_sensor);
setup_jumping(touch_jump);
setup_falling_rocks(elapsed_sensor);
setup_lava(elapsed_sensor);
setup_camera();
С целью улучшения производительности сенсор времени elapsed_sensor инициализируется только один раз и подается в качестве аргумента в нужные функции.
Рассмотрим новую функцию, которая инициализирует процесс падения камней:
function setup_falling_rocks(elapsed_sensor) {
var ROCK_EMPTIES = ["lava_rock","lava_rock.001"];
var ROCK_NAMES = ["rock_01", "rock_02", "rock_03"];
var BURST_EMITTERS_NAMES = ["burst_emitter_01", "burst_emitter_02",
"burst_emitter_03"];
var MARK_NAMES = ["mark_01", "mark_02", "mark_03"];
var falling_time = {};
...
}
Первое, что мы видим - заполнение массивов названиями объектов падающих глыб и сопутствующих объектов. Словарь falling_time служит для отслеживания времени, прошедшего после начала падения каждого из камней.
Падение камней - сенсоры
Инициализируем сенсоры, описывающие поведение каждой из падающих глыб, в двойном цикле:
for (var i = 0; i < ROCK_EMPTIES.length; i++) {
var dupli_name = ROCK_EMPTIES[i];
for (var j = 0; j < ROCK_NAMES.length; j++) {
var rock_name = ROCK_NAMES[j];
var burst_name = BURST_EMITTER_NAMES[j];
var mark_name = MARK_NAMES[j];
var rock = m_scs.get_object_by_dupli_name(dupli_name, rock_name);
var burst = m_scs.get_object_by_dupli_name(dupli_name, burst_name);
var mark = m_scs.get_object_by_dupli_name(dupli_name, mark_name);
var coll_sens_lava = m_ctl.create_collision_sensor(rock, "LAVA", true);
var coll_sens_island = m_ctl.create_collision_sensor(rock, "ISLAND", true);
var ray_sens = m_ctl.create_ray_sensor(rock, [0, 0, 0],
[0, -ROCK_RAY_LENGTH, 0], false, null);
m_ctl.create_sensor_manifold(rock, "ROCK_FALL", m_ctl.CT_CONTINUOUS,
[elapsed_sensor], null, rock_fall_cb);
m_ctl.create_sensor_manifold(rock, "ROCK_CRASH", m_ctl.CT_SHOT,
[coll_sens_island, coll_sens_lava],
function(s){return s[0] || s[1]}, rock_crash_cb, burst);
m_ctl.create_sensor_manifold(rock, "MARK_POS", m_ctl.CT_CONTINUOUS,
[ray_sens], null, mark_pos_cb, mark);
set_random_position(rock);
var rock_name = m_scs.get_object_name(rock);
falling_time[rock_name] = 0;
}
}
Внешний цикл осуществляет перебор dupli-групп (напомню, их всего две). Внутренний цикл обрабатывает объекты камней (rock), системы частиц для взрыва (burst) и меток (mark). Управление системами частиц для дымовых шлейфов не требуется, т.к. они привязаны к падающим глыбам и следуют за ними автоматически.
Сенсоры coll_sens_lava и coll_sens_island служат для детектирования столкновения камней с поверхностью лавы и земли. Третий аргумент функции create_collision_sensor() говорит о нашем намерении получать координаты столкновения в обработчике.
Сенсор ray_sens служит для определения расстояния до объектов под камнем и используется для позиционирования метки под падающими глыбами. Созданный таким образом луч начинается в координате [0,0,0] в пространстве объекта и заканчивается на расстоянии в 10 метров под ним. Последний аргумент, равный null, означает, что столкновения будут определяться с любыми объектами независимо от их collision_id.
Падение камней - сенсорные множества
Далее используется уже знакомая нам по предыдущим статьям сенсорная модель. На основе созданных сенсоров инициализируются три сенсорных множества: ROCK_FALL отвечает за падение глыб, ROCK_CRASH обрабатывает соударение с землей и лавой, MARK_POS размещает метку под глыбой.
Также зададим каждому камню случайное положение на небольшой высоте с помощью функции set_random_position():
function set_random_position(obj) {
var pos = _vec3_tmp;
pos[0] = 8 * Math.random() - 4;
pos[1] = 4 * Math.random() + 2;
pos[2] = 8 * Math.random() - 4;
m_trans.set_translation_v(obj, pos);
}
Последнее, что происходит в цикле - время падения каждого камня в словаре falling_time инициализируется нулем:
falling_time[rock_name] = 0;
В качестве ключей в данном объекте используются имена объектов камней. Эти имена являются уникальными, несмотря на то, что в сцене присутствуют несколько одинаковых объектов. Дело в том, что в Blend4Web результирующее имя объекта, добавленного по ссылке с использованием группы дуплицирования, комбинируется из имени группы и исходного имени объекта, например lava_rock.001*rock_03.
Обработчик для процесса падения
Обработчик rock_fall_cb() приведен ниже:
function rock_fall_cb(obj, id, pulse) {
var elapsed = m_ctl.get_sensor_value(obj, id, 0);
var obj_name = m_scs.get_object_name(obj);
falling_time[obj_name] += elapsed;
if (falling_time[obj_name] <= ROCK_FALL_DELAY)
return;
var rock_pos = _vec3_tmp;
m_trans.get_translation(obj, rock_pos);
rock_pos[1] -= ROCK_SPEED * elapsed;
m_trans.set_translation_v(obj, rock_pos);
}
Время падения увеличивается на величину elapsed, полученную из текущего значения сенсора времени. Перед началом падения камня имеется небольшая задержка (ROCK_FALL_DELAY). Эта задержка позволит физическому движку, работающему асинхронно в независимом Web Worker'е, корректно определить высоту объекта в момент начала его падения. В дальнейшем это поможет нам правильно разместить метку под камнем.
В переменную rock_pos сохраняются текущие координаты камня. Затем координата Y изменяется на величину ROCK_SPEED * elapsed, после чего объект устанавливается в новую позицию. Для упрощения мы использовали линейную модель перемещения (без ускорения свободного падения).
Обработчик для столкновения
При столкновении камня вызывается следующий обработчик:
function rock_crash_cb(obj, id, pulse, burst_emitter) {
var char_pos = _vec3_tmp;
m_trans.get_translation(_character, char_pos);
var sensor_id = m_ctl.get_sensor_value(obj, id, 0)? 0: 1;
var collision_pt = m_ctl.get_sensor_payload(obj, id, sensor_id);
var dist_to_rock = m_vec3.distance(char_pos, collision_pt);
m_trans.set_translation_v(burst_emitter, collision_pt);
m_anim.set_current_frame_float(burst_emitter, 0);
m_anim.play(burst_emitter);
set_random_position(obj);
if (dist_to_rock < ROCK_DAMAGE_RADIUS)
reduce_char_hp(ROCK_DAMAGE);
var obj_name = m_scs.get_object_name(obj);
falling_time[obj_name] = 0;
}
В данной функции последний параметр burst_emitter - это объект системы частиц, переданный нами при регистрации сенсорных множеств.
По значению sensor_id определяется, какой именно сенсор вызвал обработчик. Значение 0 соответствует соударению с поверхностью земли, а 1 - соответственно, с поверхностью лавы. Координаты точки столкновения получим с помощью вызова:
var collision_pt = m_ctl.get_sensor_payload(obj, id, sensor_id);
Эмиттер системы частиц взрыва burst_emitter помещается в точку соударения, после чего на нём запускается анимация.
После этого камень случайным образом позиционируется на высоте, чтобы совершить новое падение.
На основании позиции персонажа char_pos вычисляется расстояние до точки столкновения dist_to_rock. Если персонаж находится достаточно близко к месту столкновения, то у него снизится показатель жизненной энергии:
if (dist_to_rock < ROCK_DAMAGE_RADIUS)
reduce_char_hp(ROCK_DAMAGE);
Саму функцию снижения очков жизни мы рассмотрим позже. В завершение обнулим для данного камня время полета, чтобы обеспечить следующую итерацию падения:
falling_time[obj_name] = 0;
Обработчик для метки под камнем
Чтобы облегчить игроку уклонение от падающих глыб, расположим для них специальную метку на поверхности. Посмотрим на код обработчика:
function mark_pos_cb(obj, id, pulse, mark) {
var mark_pos = _vec3_tmp;
var ray_dist = m_ctl.get_sensor_payload(obj, id, 0);
var obj_name = m_scs.get_object_name(obj);
if (falling_time[obj_name] <= ROCK_FALL_DELAY) {
m_trans.get_translation(obj, mark_pos);
mark_pos[1] -= ray_dist * ROCK_RAY_LENGTH - 0.01;
m_trans.set_translation_v(mark, mark_pos);
}
m_trans.set_scale(mark, 1 - ray_dist);
}
В переменную ray_dist сохраним расстояние до земли. Позиционирование метки осуществляется до того, как камень начал свое движение (задержка ROCK_FALL_DELAY секунд). Обратите внимание, как смещается метка относительно камня:
mark_pos[1] -= ray_dist * ROCK_RAY_LENGTH - 0.01;
Множитель ROCK_RAY_LENGTH необходим, потому что ray_dist - относительная длина отрезка луча (от 0 до 1), в то время как его настоящая длина - 10 метров. Чтобы немного приподнять метку над поверхностью земли, вычитается величина 0.01.
В процессе падения камня метка линейно увеличивается в размерах. За это отвечает последняя строка в обработчике:
m_trans.set_scale(mark, 1 - ray_dist);
В итоге наблюдаем падение раскаленных глыб:
Урон от лавы
При попадании в лаву у персонажа с течением времени будут постепенно сокращаться очки жизни. Функция setup_lava() достаточно компактная, поэтому приведем её код целиком:
function setup_lava(elapsed_sensor) {
var time_in_lava = 0;
function lava_cb(obj, id, pulse, param) {
if (pulse == 1) {
var elapsed = m_ctl.get_sensor_value(obj, id, 1);
time_in_lava += elapsed;
if (time_in_lava >= LAVA_DAMAGE_INTERVAL) {
if (elapsed < LAVA_DAMAGE_INTERVAL)
var damage = 1;
else
var damage = Math.floor(elapsed/LAVA_DAMAGE_INTERVAL);
reduce_char_hp(damage);
time_in_lava = 0;
}
} else {
time_in_lava = 0;
}
}
var lava_ray = m_ctl.create_ray_sensor(_character, [0, 0, 0], [0, -0.25, 0],
false, "LAVA");
m_ctl.create_sensor_manifold(_character, "LAVA_COLLISION",
m_ctl.CT_CONTINUOUS, [lava_ray, elapsed_sensor],
function(s) {return s[0]}, lava_cb);
}
Сенсор lava_ray - луч с длиной 0.25 (чуть больше половины высоты персонажа), направленный из центра персонажа вниз. На его основе и на основе сенсора elapsed_sensor создается сенсорное множество LAVA_COLLISION. По количеству времени, проведенного персонажем в лаве (time_in_lava), мы определяем, нужно ли снижать жизненную энергию. Скорость уменьшения очков жизни регулируется константой LAVA_DAMAGE_INTERVAL. При нахождении персонажа в лаве в течение этого времени снимается 1 пункт жизни. Если же задержка между кадрами превышает заданный интервал повреждений, урон вычисляется следующим образом:
var damage = Math.floor(elapsed/LAVA_DAMAGE_INTERVAL);
Время нахождения в лаве сбрасывается в двух случаях: при получении урона от лавы или при выходе из лавы. Затем оно снова накапливается до достижения значения LAVA_DAMAGE_INTERVAL.
Очки жизни
При обработке камней и лавы мы обращались к функции reduce_char_hp(). Посмотрим, что она собой представляет:
function reduce_char_hp(amount) {
if (_character_hp <= 0)
return;
_character_hp -= amount;
var green_elem = document.getElementById("life_bar_green");
var red_elem = document.getElementById("life_bar_red");
var mid_elem = document.getElementById("life_bar_mid");
var hp_px_ratio = 192 / MAX_CHAR_HP;
var green_width = Math.max(_character_hp * hp_px_ratio, 0);
var red_width = Math.min((MAX_CHAR_HP - _character_hp) * hp_px_ratio, 192);
green_elem.style.width = green_width + "px";
red_elem.style.width = red_width + "px";
mid_elem.style.left = green_width + 19 + "px";
if (_character_hp <= 0)
kill_character();
}
Во-первых, в данной функции происходит уменьшение значения глобальной переменной _character_hp. Во-вторых, осуществляется изменение HTML-элементов полосы здоровья: для элемента life_bar_green ширина уменьшается, для элемента life_bar_red - увеличивается, а элемент life_bar_mid размещается между ними.
В случае, если здоровье достигает нуля, то вызывается функция kill_character():
function kill_character() {
m_anim.apply(_character_body, "character_death");
m_anim.play(_character_body);
m_anim.set_behavior(_character_body, m_anim.AB_FINISH_STOP);
m_phy.set_character_move_dir(_character, 0, 0);
m_ctl.remove_sensor_manifolds(_character);
}
Смерть персонажа сопровождается запуском анимации "character_death" в режиме m_anim.AB_FINISH_STOP - анимация не циклическая. Далее останавливаем персонажа и удаляем все существующие на нём сенсорные множества.
Заключение
У нас уже получается что-то похожее на игру! Если вам хочется увеличить сложность, вы можете добавить на сцену камней или увеличить их скорость и урон.
В одной из следующих статей будут подробно рассмотрены модели и материалы, разработанные для этого урока.
Ссылка на приложение в отдельном окне
Исходные файлы приложения и сцены находятся в составе бесплатного дистрибутива Blend4Web SDK.
Изменения
[2014-07-11] Изначальная публикация.
[2014-07-22] Изменен путь к приложению.