Разрабатывая игру, приходится идти на всевозможные ухищрения для повышения производительности. В самом деле, если сцена будет притормаживать при каждом выстреле или генерации врагов, то пользователь может махнуть рукой и найти более шуструю игру (благо рынок ими сейчас перегружен). «Узких мест» в игровом приложении немало, но я хотел бы рассказать про оптимизацию именно со стороны кода.
Тестируя одну из своих мобильных игр, я был неприятно поражен тормозами системы при выстрелах или взрывах. Понятное дело, что на стационарном компьютере эта же игра работала без проблем. Путем определенных ухищрений был найдет виновник — скрипт, ответственный за генерацию взрывов. Тогда-то и пришлось найти пул-менеджер на C#, который в дальнейшем перекочёвывал из проекта в проект. Однако для JavaScript такового у меня не было.
А что это за зверь?
Многие игры динамичны — что-то взрывается, кто-то стреляет, где-то бегают толпы воинов. Я не раскрою секрета, если скажу, что некоторые игровые объекты используются многократно. Вопрос только в том, как реализован механизм управления клонами.
Возьмем хрестоматийный пример — стрельбу из оружия. Понятное дело, что пули являются одним и тем же объектом. Самый простой способ — это динамичное создание клона пули, так называемый инстансинг. Просто, дешево и сердито, но есть некоторые ограничения. Обычно функция клонирования достаточно времязатратна: движку нужно выделить место в памяти под новый объект, провести инициализацию и т.д. Если объект простой, то копировать можно. Но представим, что вместо пули герой стреляет ракетами и каждая из них со своей логикой, обвешана системами частиц, текстурами. Вот теперь процедура клонирования будет отнимать гораздо больше времени. Не стоит забывать и о том факте, что после использования клона его нужно удалять из памяти. Все это негативно скажется на производительности в браузерных и мобильных играх.
Менеджер пула работает по другому принципу. Представьте, что в память компьютера заранее загружается нужный объект. При этом он практически не виден для игрового процесса, т.е. отключен. В момент выстрела пуля “пробуждается”, перекидывается в нужные координаты и выполняет свои функции. А вот когда она отработала, например попала в цель, то не уничтожается, а опять переводится в спящий режим. И так по кругу: инициализация, действие, спячка, инициализация, действие, спячка…
В этом случае, программа не тратит время на загрузку объекта в сцену, ведь он уже там есть, только отключен. Правда возникает вопрос, а что происходит, если стрельба идет очередями и первая пуля просто-напросто не успевает отработать? Вот здесь и проявляются управленческие качества менеджера пула. Для этого заранее создается нужное количество копий объекта в памяти машины. При запросе новой пули, менеджер просматривает свой список клонов в поисках свободного и, если он есть, выдает его для работы. В то же время отработавшие объекты возвращаются обратно в пул. Работа происходит по принципу стека — первым зашел, последним вышел.
Такой подход значительно разгружает систему, позволяет быстро подготовить объект, но тратится оперативная память. В принципе, дизайнеру уровня необходимо правильно определиться с количеством клонов, для баланса между расходом памяти и требованиям задачи.
В реальных условиях
Пул-менеджер мне был необходим для браузерного проекта с использованием движка Blend4Web. Поэтому все, что дальше написано, относится к этому фреймворку. Впрочем, основной код очень простой и вполне может быть приспособлен для любого движка на основе JavaScript.
Когда появилась необходимость в пуле объектов, я первым делом бросился прочесывать сеть Интернет. Каково было мое удивление, когда выяснилось, что подходящего кода в природе нет. Наверное, это простая задача и никто не заморачивался с выкладыванием кода в общий доступ. Лишь после долгого поиска, мне удалось найти на одном буржуазном сайте примитивную заготовку, которую после переделки и подгонки под Blend4Web, я предлагаю вам для использования.
Основные требования к менеджеру были поставлены следующие:
- Динамичное создание “неограниченного” количества пулов для разных объектов.
- Вызов клона в определенных координатах.
- Автоматический возврат объекта в пул, после завершения задачи.
В итоге, работа с менеджером строится буквально на двух командах:
//генерация нового пула с названием "New pool" для объекта object размером size
poolmanager.create ("New pool", size, object);
//вызов свободного клона из пула с названием "New pool" в координатах coord
poolmanager.spawn ("New pool",coord);
По традиции, я стараюсь не смешивать всё в одном скрипте. Моя концепция пул-менеджера состоит из:
- Скрипта менеджера.
- Скриптов объектов. Для работы с менеджером необходимо правильно настроить программную часть объекта. В этом же скрипте могут храниться и дополнительные функции, например перемещение, просчет коллизий и т.д.
- Основной скрипт. В нем выполняются инициализация пула, вызов клонов, т.е. непосредственно решаются игровые задачи.
Сначала рассмотрим настройку объекта. Имеется специальный шаблон, который необходим для правильного функционирования менеджера. Именно там по команде из менеджера выполняется первоначальная инициализация объекта, его вызов и “уничтожение”.
exports.my_object = function() {
this.inUse = false;
//код начальной инициализации
this.init = function() {
};
//активация объекта
this.spawn = function (coord) {
this.inUse = true;
}
//завершение работы и сброс параметров по умолчанию
this.despawn = function() {
this.inUse = false;
};
}
Итак, ключевым моментом является переменная inUse, которая отвечает за флаг активности объекта. По умолчанию, клон выключен и спокойно “почивает” в укромном уголке сцены. Но стоит менеджеру обратить внимание на “соню” (свободные объекты определяются при положении inUse = false), как последует его пробуждение с помощью внутренней функции this.spawn. В нее передаются координаты для переноса объекта в сцене. Здесь же может находиться служебный код, например, активация движения.
После завершения работы уже сам клон вызывает внутреннюю функцию this.despawn. В ней выполняются все необходимые действия для обратного перехода объекта в спящий режим. Как видите, заготовка очень простая и теперь заполним ее дополнительными переменными.
...
this.fileName = "bullet1.json";
this.name = "bullet1";
this.id;
this.init = function(data_id) {
this.id = data_id;
};
…
Шаблон объекта должен хранить уникальную информацию о самом себе, чтобы менеджер мог с ним работать, ведь именно в менеджере происходит физическая загрузка ресурсов, вызов функций инициализации и спауна.
- fileName — название файла с данными модели.
- name — имя объекта под которым он хранится в сцене.
- id — номер сцены.
Из-за особенностей системы загрузки Blend4Web в памяти могут находится объекты с одинаковыми именами, но только в том случае, если они расположены в разных слоях сцен. Подробнее о файловой системе b4w вы можете узнать из этой статьи.
Функция this.spawn вызывается менеджером для активации клона и переноса его в нужное место сцены. И, обратите внимание, что именно здесь происходит включение рендерера объекта с помощью функции show_object(). С этого момента клон становится полноценным участником игрового процесса.
this.spawn = function (coord) {
this.inUse = true;
var obj = m_scene.get_object_by_name(this.name,this.id);
m_trans.set_translation_v(obj, coord);
m_scene.show_object(obj);
console.log("Spawn!");
}
Последняя важная функция this.despawn. В этом примере я просто отключаю рендерер объекта с помощью функции hide_object().
this.despawn = function() {
this.inUse = false;
var obj = m_scene.get_object_by_name(this.name,this.id);
m_scene.hide_object(obj);
console.log("Despawn!");
};
Теперь рассмотрим собственно пул-менеджер. Всё закручено на использовании массивов. Всего в программе имеется два глобальных массива. Первый _pools [] — это ассоциативный список имен созданных пользователем пулов и ссылок на массивы с клонами. Второй _pools_size[] отвечает за хранение размеров локальных массивов.
//глобальные ссылки на пулы
var _pools = {};
var _pools_size = {};
В данный момент менеджер имеет две внешние функции: create() и spawn(). По названиям понятно, какая и за что отвечает. Обратите внимание, что после загрузки трехмерный объект выключается и становится невидимым. Последним этапом загрузки является вызов функции инициализации нового клона. Всё, после этой операции в памяти находится “новенький” пул заполненный объектами.
//---------------------------------------------------
//создание нового пула объекта с уникальным именем
//name - имя
//size - количество
//obj - прототип объекта
exports.create = function(name, size, obj) {
var temp_pool = new Array();
_pools[name]=temp_pool;
_pools_size [name] = size;
for (var i = 0; i < size; i++) {
var clone_obj = new obj();
var data_id = m_data.load(APP_ASSETS_PATH + clone_obj.fileName,null,null,false,true);
temp_pool[i] = clone_obj;
clone_obj.init (data_id);
}
Вторая функция отвечает за активацию первого свободного объекта в сцене. Таковой находится по состоянию переменной inUse. Если сброшен, то клон свободен. После вызова функции spawn (), объект активируется.
//---------------------------------------------------
//вызов объекта из пула
//pool_name - имя пула
//coord - вектор для переноса объекта
exports.spawn = function(pool_name,coord) {
var pool = _pools [pool_name];
var size = _pools_size [pool_name];
if(!pool[size - 1].inUse) {
pool[size - 1].spawn(coord);
pool.unshift(pool.pop());
}
//проверяем освободившиеся объекты и заполняем пул
for (var i = 0; i < size; i++) {
if (!pool[i].inUse) {
pool.push((pool.splice(i,1))[0]);
}
}
};
Что в итоге
Уже традиционно в конце урока я высказываю свои мысли о работе с Blend4Web. Фреймворк развивается стремительно и то, что я раннее находил неудобным, многое исправлено в последующих версиях. Отрадно, когда разработчики движка прислушиваются к мнению пользователей и быстро корректируют узкие места. Разумеется, если это действительно необходимо.
Наверное, вы заметили, что при инициализации клонов я использую не копирование, а обычную загрузку объектов командой load(). В API Blend4Web имеется функция копирования copy(). Однако в данном случае, предпочтительнее использовать именно load. Инстансинг не всегда корректно работает со сложными объектами и не учитывает связи при клонировании.
Учтите, что подгрузка новых ресурсов требует определенного времени. Поэтому инициализацию объектов лучше скрыть межуровневым экраном. Также желательно заранее провести загрузку одного объекта, чтобы дальнейшие брались уже из кэша браузера.
Итог. Пул работает, снаряды вылетают и поражают корабли противника, тормозов никаких. Все объекты, что неоднократно используются в игре, теперь вызываются через пул-менеджер. По крайне мере, эта часть кода уже оптимизирована.
Полные листинги готовые к использованию с b4w.
“Pool Manager”/g_pool_manager.js
"use strict"
b4w.register("g_pool_manager", function(exports, require) {
// import modules used by the app
var m_app = require("app");
var m_cfg = require("config");
var m_data = require("data");
// automatically detect assets path
var APP_ASSETS_PATH = m_cfg.get_std_assets_path() + "Danger Space/";
//глобальные ссылки на пулы
var _pools = {};
var _pools_size = {};
//---------------------------------------------------
//создание нового пула объекта с уникальным именем
//name - имя
//size - количество
//obj - прототип объекта
exports.create = function(name, size, obj) {
var temp_pool = new Array();
_pools[name]=temp_pool;
_pools_size [name] = size;
for (var i = 0; i < size; i++) {
var clone_obj = new obj();
var data_id = m_data.load(APP_ASSETS_PATH + clone_obj.fileName,null,null,false,true);
temp_pool[i] = clone_obj;
clone_obj.init (data_id);
}
//---------------------------------------------------
//вызов объекта из пула
//pool_name - имя пула
//coord - вектор для переноса объекта
exports.spawn = function(pool_name,coord) {
var pool = _pools [pool_name];
var size = _pools_size [pool_name];
if(!pool[size - 1].inUse) {
pool[size - 1].spawn(coord);
pool.unshift(pool.pop());
}
//проверяем освободившиеся объекты и заполняем пул
for (var i = 0; i < size; i++) {
if (!pool[i].inUse) {
pool.push((pool.splice(i,1))[0]);
}
}
};
//---------------------------------------------------
//возврат объекта в пул
//pool_name - имя пула
//obj - объект
exports.despawn = function(pool_name, obj) {
};
}
});
“Объект”/g_bullet.js
"use strict"
b4w.register("g_bullet", function(exports, require) {
// import modules used by the app
var m_cfg = require("config");
var m_data = require("data");
var m_ctl = b4w.require("controls");
var m_trans = b4w.require("transform");
var m_scene = b4w.require("scenes");
var m_vec3 = require("vec3");
var m_obj = b4w.require("objects");
var m_poolmanager = b4w.require("g_pool_manager");
var m_phy = require("physics");
var m_vars = require("g_vars");
//------------------------------------------------------------------------------
exports.Bullet = function() {
this.inUse = false;
this.fileName = "bullet1.json";
this.name = "bullet1";
this.id;
this.init = function(data_id) {
this.id = data_id;
};
this.object = function(data_id) {
return m_scene.get_object_by_name(this.name,this.id);
};
this.spawn = function (coord) {
this.inUse = true;
var obj = m_scene.get_object_by_name(this.name,this.id);
m_trans.set_translation_v(obj, coord);
m_scene.show_object(obj);
console.log("Spawn!");
//активация таймера для автоматического отключения объекта (2 сек)
var timer_bulet1 = m_ctl.create_timer_sensor(2,false);
m_ctl.create_sensor_manifold(this, "TIMER_B", m_ctl.CT_SHOT, [timer_bulet1], null, timerB_cb);
}
//завершение работы и сброс параметров по умолчанию
this.despawn = function() {
this.inUse = false;
var obj = m_scene.get_object_by_name(this.name,this.id);
m_scene.hide_object(obj);
console.log("Despawn!");
};
}
function timerB_cb (obj, id) {
obj.despawn();
}
});
Пример использования
"use strict"
b4w.register("g_player", function(exports, require) {
// import modules used by the app
var m_trans = b4w.require("transform");
var m_poolmanager = b4w.require("g_pool_manager");
var m_bullet1 = b4w.require("g_bullet");
exports.init = function() {
//инициализация нового пула с названием "Bullet1", 10 объектов m_bullet.Bullet
m_poolmanager.create ("Bullet1", 10, m_bullet.Bullet);
//спаун объекта в указанных координатах
var gun1_coord = m_trans.get_translation(_gun1);
m_poolmanager.spawn ("Bullet1",gun1_coord);
//инициализация второго пула с названием "Bullet2", 20 объектов m_bullet.Bullet
m_poolmanager.create ("Bullet2", 20, m_bullet.Bullet);
//спаун объекта в указанных координатах
var gun1_coord = m_trans.get_translation(_gun1);
m_poolmanager.spawn ("Bullet1",gun1_coord);
m_poolmanager.spawn ("Bullet1",gun1_coord);
m_poolmanager.spawn ("Bullet1",gun1_coord);
}
}
});
Автор: Prand