Как стать автором
Обновить

3D игры в Инстаграм на Javascript, или траектория полета колбасы

Время на прочтение 19 мин
Количество просмотров 5.8K


После первой статьи о программировании игр в масках Инстаграм ко мне обратился заказчик с просьбой создать игру в Инстаграм для его пиццерии. Эту игру планировалось использовать для целей продвижения бизнеса. Я, конечно, понимаю, что, судя по количеству просмотров первой статьи, тема Инстаграма сообществу Хабра не особо интересна. По-видимому, этот сервис все еще считается каким-то несерьезным развлечением для блондинок, а статья о нем годится только в качестве пятничного чтива под вечерний коктейль. Впрочем, сегодня, как раз, пятница. Доставайте коктейль. И не забудьте пригласить блондинок. Однако, вполне вероятно, в будущем технологии достигнут таких высот, что мы начнем играть в Инстаграме в GTA-5. Или 10.

В данной статье речь пойдет сразу о двух играх. Одну я сделал на заказ, а вторую потом — для себя. С каждой новой игрой я сталкивался с новыми задачами, поиск путей решения которых заставлял меня знакомиться с новыми нюансами разработки. Возможно, этот рассказ будет полезен и другим разработчикам элементов дополненной реальности. Игры написаны на чистом Javascript без использования каких-либо дополнительных библиотек. Для отображения 3D графики задействованы встроенные средства Инстаграм.

Игра 1. Пицца


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

Когда заказчик увидел рабочий прототип, то он прислал мне в ответ это: « :)) ». Во время тестирования финального результата возникла проблема. Она заключалась в том, что испытуемые не могли доиграть в игру: их просто распирало от смеха — настолько это оказалось весело.

Напомню, разработка масок и игр производится в Spark AR Studio. А затем оттуда можно загрузить свою работу в Инстаграм на всеобщее обозрение. Примечательно то, что веб-технологии стирают для разработчика границы между операционными системами. Вы пишете один код, который затем работает в приложении Инстаграм как для iOS, так и для Android, без каких-либо доработок и изменений. Конечно, платой за это становится принципиально невысокая скорость скрипта.

Графику для игры предоставил заказчик. С 3D моделями возникли некоторые сложности. В частности, при использовании встроенного движка анимации Spark AR Studio выяснилось, что, если движущийся объект масштабирован, то в игре неверно определяются его координаты. Но этот досадный эффект не наблюдается в случае, если масштабировать не весь объект полностью, а каждый его меш в отдельности. Пришлось масштаб пиццы оставить 1:1, а каждому составляющему ее элементу прописать некий коэффициент. С форматом FBX, в который нужно экспортировать модели для Инстаграм-игры, также возникли проблемы. Графический дизайнер прислал модели со встроенными текстурами, которые, к тому же, были размещены по относительному пути. 3D редактор их видел, а Spark AR — нет. Пришлось перепаковать модели так, чтобы файлы текстур лежали отдельно от моделей и по одному с ними пути.

И еще маленькая проблема, с которой я столкнулся, заключалась в том, что объект api Saprk AR, отвечающий за вывод текста на экран, отказывался принимать в качестве значения число — игровой счет, например. Я долго не мог понять, почему ничего не отображается. Что я делаю не так? Оказалось, что нужно предварительно преобразовать числа в строки ( .toString() ). Это само собой разумеется для других языков, но довольно нетипично для javascript, который всегда делал это сам.

Простейшая анимация


Одной из новых вещей для меня в этой игре стало программирование анимации 3D объектов. (В моей предыдущей игре для Инстаграм «Крестики-нолики» анимации не было вообще.) Движок анимации в Spark AR Studio весьма специфичен. Он принимает некоторые входные параметры, а потом реактивным способом связывает изменяемую величину с объектом, в котором требуется что-то менять. Например, так будет изменяться за время t координата y (startValue, endValue — ее начальное и конечное значения) какого-нибудь 3D-объекта:

var driverParameters = {
    durationMilliseconds: t,
    loopCount: Infinity,
    mirror: false
};
var driver = Animation.timeDriver(driverParameters);
var sampler = Animation.samplers.linear(startValue, endValue);
sceneObject.transform.y = Animation.animate(driver, sampler);
driver.start();

Для движения отлетающих ингредиентов пиццы в пространстве я решил просто запускать параллельно три таких анимации для каждой координаты. Достаточно было указать начальную координату (startValue) и рассчитанную по углу поворота пиццы конечную координату (endValue), так, чтобы она находилась где-нибудь далеко на случай, если игрок не поймает этот «снаряд» ртом. Если поймал — то движение прекращается. Событие открытия рта я уже описывал в предыдущей статье. Только здесь — игра на двоих и, соответственно, будет уже два лица и два рта:

FaceTracking.face(0).mouth.openness.monitor().subscribe(function(event) {
    if (event.newValue > 0.2) {
        ...
    };
});

FaceTracking.face(1).mouth.openness.monitor().subscribe(function(event) {
    if (event.newValue > 0.2) {
        ...
    };
});

Ключевым моментом в данной игре является ловля ртом летящих ингредиентов, иными словами, поиск попадания летящего объекта в заданную небольшую область вокруг центра рта человека. Сначала у меня это вычисление не хотело производиться правильно, но решение проблемы было найдено после введения скрытого объекта, привязанного к координатам рта, и все заработало. Почему-то напрямую координаты рта не возвращались. Здесь cameraTransform.applyTo — приведение координат точек лица к координатам в 3D мире (метод взят из документации).

move: function() {
    // ход игрока (по событию открытия рта)

    // текущие координаты летящего ингредиента
    var object = Scene.root.find('pizzafly');
    var olast = {
        x: object.transform.x.pinLastValue(),
        y: object.transform.y.pinLastValue(),
        z: object.transform.z.pinLastValue()
    };

    // пустой невидимый объект, привязка его к координатам рта на сцене
    var objectHidden = Scene.root.find('nullObject');
    objectHidden.transform.x = FaceTracking.face(face).cameraTransform.applyTo(FaceTracking.face(face).mouth.center).x;
    objectHidden.transform.y = FaceTracking.face(face).cameraTransform.applyTo(FaceTracking.face(face).mouth.center).y;
    objectHidden.transform.z = FaceTracking.face(face).cameraTransform.applyTo(FaceTracking.face(face).mouth.center).z;

    // текущие координаты рта
    var mouth = {
        x: objectHidden.transform.x.pinLastValue(),
        y: objectHidden.transform.y.pinLastValue(),
        z: objectHidden.transform.z.pinLastValue()
    };

    // расстояние до центра рта
    var d = {
        x: Math.abs(olast.x - mouth.x),
        y: Math.abs(olast.y - mouth.y),
        z: Math.abs(olast.z - mouth.z)
    };

    // условие попадания
    if ((d.x > 0.03) || (d.y > 0.03) || (d.z > 0.03)) {
        // промах
        ...
    } else {
        // попадание
        ...
    };

},

Впоследствии я понял, что следует убрать проверку по глубине (по координате z), так как в данной конкретной игре визуально сложно оценить глубину. То есть, теперь поймать ингредиент стало можно, открыв рот в любой момент полета. Главное — совмещение по x и y.


Наконец, последняя проблема, с которой я столкнулся при написании данной игры, заключалась в ограничении размера финального билда 4 Мб. Причем, по рекомендации Facebook, для того, чтобы игра отображалась на как можно большем количестве мобильных устройств, желательно вписаться даже в 2 Мб. А 3D-моделеры — люди творческие и хотят делать тяжелые модели с плотной сеткой и огромными текстурами, совершенно не заботясь о нас, программистах, а точнее, об итоговой производительности в играх. Текстуры я еще кое-как уменьшил в размере и сжал в jpg (вместо png), а вот саму модель пиццы пришлось отправлять на доработку (ретопологию). В итоге, все же, удалось вписаться в объем 2Мб со всеми моделями, текстурами и скриптом. И игра отправилась на модерацию.

И через какое-то время вернулась с формулировкой, что «эффект содержит слишком много статического текста». Пришлось убрать цифры обратного отсчета времени и вместо них задать анимацию стрелке на секундомере, которая теперь стала отсчитывать время вместо них. После этого игру одобрили.

Игра 2. Про бабочку (ButterFlap)



Теперь расскажу о создании второй игры. Я не могу не делать игры, это мое хобби. За несколько дней до 8 Марта я решил создать к этому празднику игру в Инстаграм. Что-нибудь про цветы, бабочек и конфеты. Но я никак не мог придумать, в чем бы могла заключаться суть игры. Мысль вертелась в голове. Может быть, бабочка будет собирать конфеты и складывать их в корзину? А, может быть, бабочки будут поднимать конфеты в воздух и бросать их, а игроку нужно будет ловить их ртом? В общем, я промаялся пару дней и, так и не найдя решения, понял, что к 8 марта моя игра все равно уже не успеет пройти модерацию, так как последняя занимает 3-4 дня. Я уже хотел было оставить эту затею, как вдруг идея пришла сама, внезапно. Я больше не привязывал игру к Женскому Дню, теперь это была просто игра.

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

Да, все это звучит хорошо. Мне, прямо, самому захотелось поиграть. Но я вспомнил, что я вообще не художник. Однако, я способен сделать 3D модели, и это проще, чем рисовать. Итак, решено, пусть будет 3D-скроллер.

Графика


Я нашел в Интернете роялти-фри модели, которые мне пришлось доработать. На те, что были без текстур, натянул текстуры, предварительно разместив последние в текстурном атласе: на нетекстурированных моделях видны артефакты в виде «лесенок», а текстурам в игре можно задать сглаживание, что делает вид объектов более презентабельным и не таким плоским. Те модели, что содержали текстуры, тоже имели свои огрехи, которые пришлось исправлять. Во-первых, размер текстур был не кратен двойке в степени. То есть, например, он мог быть равен 1200x1200. Пришлось сжимать до 1024x1024. Видеопроцессоры могут либо масштабировать, либо заполнять пустым пространством неподходящие текстуры, то есть, те, размер которых не 1024x1024, 512x512, 256x256 и т.д. В любом случае, подобные действия — это лишняя нагрузка во время работы игры и бессмысленный расход памяти, поэтому лучше подготовить правильные изображения предварительно вручную. Спасало ситуацию еще то, что я распределял все текстуры по текстурным атласам, поэтому, если, например, оригинальная текстура была размера 400x200 пикселей, то я мог просто поместить ее в атлас 1024x1024, как есть, рядом с другими подобными. После этого надо, естественно, масштабировать и UV-развертку, но это делается за пару секунд. Еще попадались варианты моделей, где текстуры почему-то были привязаны по абсолютному пути типа «C:\Work\Vasya\Map.jpg». Ну нет на моем компьютере такой папки! Пришлось указывать пути текстурам вручную… Да с чего вы вообще взяли, что я должен хранить все свои проекты на диске «C:»!? Ох, уж эти моделеры, свободные художники… Кстати, таким образом, по именам папок в случайно оставленных путях, можно ненароком узнать больше о личности моделера, например, его имя. Приятно познакомиться!

Сложнее всего оказалось найти подходящую модель бабочки с анимацией. В Spark AR используется анимация, которую можно экспортировать из любого 3D редактора в формат FBX. Крылья у пары скачанных мной моделей бабочек были как-то странно отзеркалены — было смоделировано одно крыло, а второе неким непонятным мне способом отражено, и, таким образом, их получалось два. При таком подходе к моделированию, в итоге, в игре одно из крыльев (скопированное) не хотело принимать свет от источника и оставалось всегда тусклым. Я не рисковал кардинально менять модель, потому что тогда бы слетела анимация. А в анимации я еще больший нуб, чем в 3D-моделировании. Возможно, проблема была в чем-то другом: я пробовал, например, разворачивать нормали, но это не помогло. Короче, бесплатные 3D модели — это боль. В итоге, промаявшись целый вечер, методом проб и ошибок я нашел подходящую модель, которая после некоторой доводки напильником стала выглядеть удовлетворительно. Кое-что мне в ней не нравилось, но это были мелочи: я переделал текстуру, изменил в некоторых местах геометрию, подправил параметры материалов. Наконец, бабочка взлетела. Ура. Но теперь выдохся я. Поэтому я решил пойти спать и продолжить на следующий день. Да, всего на создание игры я потратил дней 7-8. Но это была такая, довольно неспешная, работа по вечерам, с чтением документации, статей и поиском ответов на вопросы.


Весь вечер следующего дня я работал над графикой. Специфика масок-игр для Инстаграм, как я уже упомянул, состоит в том, что желательно не выходить за объем 2Мб на всю игру (максимум 4 Мб). За скрипт я не беспокоился: его размер едва ли превысит 50 Кб. А вот над 3D моделями растений пришлось изрядно поколдовать. Например, та часть подсолнуха, где расположены семечки, была сделана геометрией. Сотни полигонов… Заменяем ее на фрагмент сферы из пары десятков треугольников и натягиваем скачанную текстуру, собственно, этой части. Количество листиков тоже можно поубавить. Траву внизу сцены делаем плоскостью из двух треугольников с наложенной текстурой с альфа-каналом. Объем достигаем тенями и копированием фрагментов картинки внутри самой текстуры.

Вообще, количество текстур желательно минимизировать, равно как и их размер в байтах. Именно текстуры создают основной объем игры. Я привык текстуры, где нужна прозрачность, размещать в одном текстурном атласе — в png 32 bit, а текстуры, которые не используют альфа-канал, упаковывать в другой атлас, сохраняя его в jpg. Jpg можно ужать сильнее, чем png. Трава и элементы интерфейса, которым требуется прозрачность, отправились в png-атлас, а все остальное — в jpg.

Итог по объему. Всего у меня получилось 4 вида растений, скала, бабочка, которой будет управлять игрок, и туча. Текстуры всех этих моделей в сжатом виде заняли 2 атласа 1024x1024 jpg и png общим объемом 500 Кб. Сами модели заняли примерно 200 Кб. Плюс скрипт. Звуки — 31 Кб. Итого: порядка 1 Мб. Зеленым цветом, как раз, отображается размер билда (это то, что должно вписаться в 2 Мб).


Внезапно я столкнулся с совершенно неожиданной проблемой. Я удалил несколько неиспользуемых моделей, но не через Spark AR Studio, а из файловой системы. Впоследствии при сборке билда я обнаружил, что со сцены Spark AR Studio они исчезли, но в сборку все равно попали. И очистить проект от неиспользуемых ресурсов никак нельзя. Сылки на них из «Asset Summary» никуда не ведут. Видимо, это недоработка Spark AR Studio. Мне пришлось пересоздать проект заново, добавив туда все необходимые ресурсы с самого начала.

Сцена


Я долго думал над тем, как реализовать скроллинг всех объектов на экране. Из инструментов для этого имеется только javascript и простейший встроенный движок анимации в Spark AR Studio, который может просто менять координаты объекта от начального значения до конечного за заданное время.

Да, перед этим еще возникла дилемма: создать ли полностью весь уровень игры, этакую сцену в 3D редакторе, продублировав повторяющиеся растения необходимое количество раз и расставив их в нужных позициях, чтобы в игре прокручивать всю сцену целиком, либо загружать только по одному экземпляру каждого 3D объекта и подставлять их по ходу движения игрока чуть за рамками экрана. Ответ был очевиден. Наш выбор — второй вариант. Иначе в 2 Мб точно не вписаться: скорее всего, сцена по первому варианту будет тяжеловата. Но тогда нужна схема расстановки объектов. И я, не долго думая, решил использовать 3D редактор в качестве редактора уровней. Ага, получается, что я сделал и то, и другое. Растения для игры я сохранил каждое в одном экземпляре. А от редактора мне нужны были только координаты. Завершив работу, я выписал координаты всех объектов и составил массив с данными для игры.

lv:[
    {n:'flower1', x:8.0, y:-6.5},
    {n:'cloud', x:10.0, y:0.1},
    {n:'rain', x:10.0, y:6.6},
    {n:'flower1', x:14, y:-2.5},
    {n:'diamond_red', x:20, y:2.0},
	...
],

И еще — отдельный ассоциативный массив, по типам объектов, где хранятся размеры коллайдеров и их смещения относительно центров 3D объектов, а также, флаг (col), определяющий, является ли объект препятствием (иначе игрок проходит сквозь него). Для некоторых типов объектов задается флаг (destroy), определяющий, нужно ли скрывать объект после взаимодействия, а также, параметр (v), определяющий некую степень взаимодействия, например, количество очков, которое получает или теряет игрок.

colld:{
    'flower1': {dx:0, dy:5, w:2.3, h:1.4, col:true},
    'diamond_green': {dx:0, dy:0, w:1, h:1, col:false, v:1, destroy:true},
    'diamond_red':{dx:0, dy:0, w:1, h:1, col:false, v:-2},

    ...
},

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

Помня о проблеме с масштабом, с которой я столкнулся при разработке первой игры, я сбросил масштаб всех моделей в 3D редакторе. Таким образом, в Saprk AR модели сразу загрузились в нормальных размерах. Правда, в скрипте все равно, не обошлось без «магического числа», глобального коэффициента, вселенского кода, в котором заключена суть мироздания. Виртуального мироздания, конечно же. И я готов открыть вам это число. Пользуйтесь, люди! Мне не жалко! Это число 0,023423. Короче говоря, несмотря на сброс всех масштабов, один метр в 3D редакторе оказался равен вот этому самому числу в Spark AR Studio. Скорее всего, это я, все же, не до конца понимаю всех премудростей работы с 3D графикой, отсюда — и коэффициент. На него домножаются, как вы уже догадались, координаты (но не размер) всех объектов, которые были экспортированы из редактора. Где подстроить масштаб сцены в Spark AR, я не нашел.

Следующая проблема, с которой я столкнулся, заключалась в потере порядка сортировки объектов при экспорте из 3D редактора. Сложные объекты, состоящие из нескольких мешей, могли непредсказуемо отображаться на сцене в игре таким образом, что, например, меш, который находился позади другого, вдруг выскакивал вперед. И, если посмотреть на порядок объектов уже после экспорта в Spark AR Studio, то там, действительно, видно, что данный меш почему-то находится выше в списке, хотя в редакторе он был ниже. Эту проблему я решил, разделив сцену на слои и сохранив их по разным файлам.

Сложная анимация


Еще три вечера я провозился с программированием. Если для анимации взмахов крыльев бабочки я использовал стандартный движок Spark AR Studio, то задача движения фона была не такой простой. Я так и не понял, как привязать к итерациями цикла движения не просто один изменяемый параметр, а полноценную коллбек-функцию. Во всяком случае, у меня не получилось это сделать, текущие параметры анимации не хотели туда передаваться. А такая функция просто необходима, поскольку, если в первой игре (с пиццей) я проверял коллизию по событию открытия рта, подписавшись на него, то здесь такого события уже не было. И надо было просто проверять столкновения с объектами окружения по мере движения персонажа, по его текущим координатам. А для этого координаты и надо сравнивать на каждой итерации. И тут я подумал. Ведь, я уже писал игры на javascript. А почему бы не использовать свой движок анимации, написанный мной ранее для тех игр? Его принцип действия примерно такой же: за заданное время изменяется параметр (или параметры) между заданными начальным и конечным значениями. И текущее значение передается в качестве параметра — вы не поверите — в заданную коллбек-функцию, в которой можно, например, установить 3D объект на сцене в координаты, равные этим текущим значениям, или проверить по ним столкновения. Спасибо, Кэп. Мне пришлось слегка адаптировать свой движок к местной «экосистеме»: убрать оттуда ссылки на объект window, так как его здесь нет, и прочие мелочи.

Да, еще — по поводу скроллинга пейзажа и предметов окружения. Я решил поместить весь мир в один NullObject, то есть, в пустой 3D объект, контейнер, и передвигать его, используя лишь один параметр для анимации — его координату x. Внутри контейнера все модели имеют такие же координаты, как если бы они находились снаружи, только теперь для них система отсчета привязана к этому пустому объекту. Скалы и цветы будут повторяться (причем, на разной высоте от земли, в соответствии со схемой уровня), поэтому можно использовать повторно эти объекты по мере движения, задавая им нужную горизонтальную и вертикальную позицию внутри «контейнера». Я написал систему поиска попадающих в кадр предметов (при текущем смещении контейнера), которая устанавливает предмет, вышедший из кадра, в новую позицию дальше, если он должен там появиться. Вы можете посмотреть, как это работает на примере трех объектов. (В игре их будет больше, поэтому там вы уже не увидите такого эффекта «перестановки» предметов окружения.)



Функция обновления координат объектов выглядит так:

oCoordSet: function(v) {
    //итерация движения мира: установка координат видимым объектам
    //положение камеры относительно мира
    var camx = -ap.oWorld.transform.x.pinLastValue();
    //горизонтальные границы экрана
    var x1 = camx - ap.game.scro.w2;
    var x2 = camx + ap.game.scro.w2;
    //перебор массива объектов уровня
    for (var i = 0; i < ap.d.lv.length; i++) {
        //условие вхождения в видимый экран
        if ((ap.d.lv[i].x >= x1) & (ap.d.lv[i].x <= x2)) {
            //поиск объекта по имени
            ap.d.lv[i].o = Scene.root.find(ap.d.lv[i].n);
            //высота объекта - по массиву
            ap.d.lv[i].o.transform.y = ap.d.lv[i].y;
            if ((ap.d.lv[i].n == 'cloud') || (ap.d.lv[i].n == 'rain')) {
                //если это туча или дождь,
                //то горизонтальная позиция
                //домножается на коэффициент 2.3,
                //чтобы этот объект двигался быстрее остальных
                ap.d.lv[i].o.transform.x = ap.d.lv[i].x - (x2 - ap.d.lv[i].x) * 2.3 + 0.2;
            } else {
                //если это обычный объект окружения,
                //то его координата определяется по заготовленному массиву
                ap.d.lv[i].o.transform.x = ap.d.lv[i].x;
            };
        };
    };
    //два объекта травы на переднем плане последовательно перемещаются вперед
    //при достижении критических значений позиции камеры
    if (camx > ap.game.grassPosLast) {
        ap.game.grassPosLast += ap.game.grassd;
        ap.game.grassi = 1 - ap.game.grassi;
        ap[ap.game.grassNm[ap.game.grassi]].transform.x = ap.game.grassPosLast;
    };
},

Главный герой


Главным героем в игре является непотопляемая бабочка, которая смело летит вперед, преодолевая препятствия. Управление я решил сделать посредством установки маркера, или курсора (светлой точки), при помощи поворота и наклона головы. И в направлении этой точки будет медленно лететь бабочка (в самом деле, она же не истребитель, а также, не умеет телепортироваться). Например, если подписаться на событие наклона головы, то реализовать управление по вертикали можно так:

FaceTracking.face(0).cameraTransform.rotationX.monitor().subscribe(function(event) {
    var v = event.newValue;
    //половинная высота экрана
    var scrH2 = ap.game.scr.h2;
    //курсор
    var p = -v * 0.5;
    if (p < -scrH2) {
        p = -scrH2;
    } else if (p > scrH2) {
        p = scrH2;
    };
    //шаг смещения персонажа
    var d = 0.006;
    //текущая позиция персонажа
    var cur = ap.oPers.transform.y.pinLastValue();
    if (p < cur) {
        cur -= d;
        if (cur < p) {cur = p;};
    } else {
        cur += d;
        if (cur > p) {cur = p;};
    };
    //установка новой позиции точки (курсора)
    ap.oPointer1.transform.y = p;
    //установка новой позиции персонажа,
    //а точнее, пока запоминание его смещения
    ap.game.pers.dy + = cur - ap.game.pers.y;
});

Аналогично — для горизонтали. Только там надо подписаться на событие не наклона, а поворота головы (rotationY), а вместо высоты экрана рассматривать его ширину.

Коллайдеры


Все это прекрасно, мир движется, а игровой персонаж может свободно перемещаться по экрану. Но теперь нужен обработчик столкновений, иначе никакой игры не получится. В игре имеется три события, по которым может изменяться позиция персонажа. Это поворот и наклон головы, а также движение мира, при котором автоматически увеличивается горизонтальная координата игрока (x).

Поскольку я не знаю, как в Spark AR работают итерации обработчика лица — вызываются ли они с определенной периодичностью или им задается максимально возможная тактовая частота, а в своем движке анимации я могу управлять этим параметром, то я решил, что буду определять коллизии в своей функции движения мира, которая вызывается с заданной мной частотой (60 кадров в секунду). В событиях обработки лица будем лишь «накапливать» движение.

Принцип будет такой. В событиях обработки наклона и поворота головы накапливается смещение по осям x и y. Далее в функции скроллинга мира «в копилку» добавляется еще и смещение по горизонтали. И затем проверяется, если прибавить к исходным координатам накопленное смещение, то не произойдет ли столкновение с каким-либо из объектов мира. Если нет — то исходные координаты игрока плюс смещение делаем текущими координатами. А затем исходные обновляем до текущих (сбрасываем) и обнуляем смещения. Если да — то откатываем координаты к исходным. Причем, нам нужно определить, по какой оси было бы столкновение, поскольку нельзя откатывать обе координаты. Иначе игрок просто «прилипнет» к одной точке пространства и больше не сможет никуда сдвинуться. Нужно дать ему свободу перемещения по той оси, по которой столкновение не произойдет. Так можно дать ему шанс облететь препятствие.

setPersPos: function(camx, camdx) {
    //установка новой позиции персонажа
    //с учетом накопленных смещений и смещения мира (camx,camdx)

    //текущие координаты персонажа и накопленные смещения
    var persx = ap.game.pers.x;
    var persy = ap.game.pers.y;
    var dx = ap.game.pers.dx;
    var dy = ap.game.pers.dy;

    //определяем коллизии, передав координаты персонажа и мира
    var col = ap.collisionDetect(
        {x: persx, y: persy, dx: dx, dy: dy},
        {x: camx, dx: camdx, y: 0}
    );

    if (col.f == true) {
        //была коллизия

        if (col.colx == true) {
            //была коллизия по горизонтали
            //устанавливаем координату персонажа по значению,
            //возвращенному функцией определения коллизий
            //(она будет равна предыдущему значению, с учетом сдвига мира)
            ap.game.pers.x = col.x;
        } else {
            //не было коллизии по горизонтали,
            //просто добавляем накопленное смещение
            ap.game.pers.x = persx + dx;
        };

        if (col.coly == true) {
            //аналогично - для вертикали
            ap.game.pers.y = col.y;
        } else {
            ap.game.pers.y = persy + dy;
        };

    } else {
        //коллизии не было, просто добавляем смещения
        ap.game.pers.x = persx + dx;
        ap.game.pers.y = persy + dy;
    };

    //обнуляем смещения
    ap.game.pers.dx = 0;
    ap.game.pers.dy = 0;

    //ставим управляемый объект в новые координаты
    ap.oPers.transform.x = ap.game.pers.x;
    ap.oPers.transform.y = ap.game.pers.y;
},

Сама функция определения коллизий:

collisionDetect: function(opers, ow) {
    //определение коллизий, opers - данные по игроку, ow - по миру

    var res = {f: false, colx: false, coly: false, x: 0, y: 0};

    var ocoll, x, y, w, h, persx, persy, persx0, persy0, od, colx1, coly1, colx2, coly2;
    var collw = false, collh = false;

    //текущая координата игрока
    //(камера остается неподвижной, смещается "полотно" мира)
    persx0 = opers.x + ow.x - ow.dx;
    persy0 = opers.y + ow.y;

    //расчет новой координаты игрока с учетом его смещения
    persx = persx0 + opers.dx;
    persy = persy0 + opers.dy;

    //перебор объектов сцены
    for (var i = 0; i < ap.d.lv.length; i++) {
        od = ap.d.lv[i]; //obj data

        //поиск в отдельном массиве (с инфо по коллайдерам),
        //участвует ли данный объект в коллизиях
        //а также размер и смещение центра коллайдера относительно объекта
        if (typeof ap.d.colld[od.n] !== "undefined") {

            //поиск в этом массиве смещений коллайдера внутри объекта
            ocoll = ap.d.colld[od.n];
            colx1 = od.x + ocoll.x1;
            colx2 = od.x + ocoll.x2;
            coly1 = od.y + ocoll.y1;
            coly2 = od.y + ocoll.y2;

            if ((persx < colx1) || (persx > colx2) || (persy < coly1) || (persy > coly2)) {} else {
                //игрок пересекается с объектом
                res.f = true;

                //если по предыдущим координатам не было коллизии по горизонтали,
                //значит, произошла коллизия по горизонтали
                if ((persx0 < colx1) || (persx0 > colx2)) {
                    collw = true;
                };
                //если по предыдущим координатам не было коллизии по вертикали,
                //значит, произошла коллизия по вертикали
                if ((persy0 < coly1) || (persy0 > coly2)) {
                    collh = true;
                };

            };
        };

    };

    //запись и возврат результата

    //если была коллизия, то откатываем координату к предыдущей,
    //иначе устанавливаем новую
    if (collw == true) {
        res.colx = true;
        res.x = persx0 - ow.x;
    } else {
        res.x = opers.x;
    };

    //аналогично - по вертикали
    if (collh == true) {
        res.coly = true;
        res.y = persy0 + ow.y;
    } else {
        res.y = opers.y;
    };

    return res;
},

Дождь


Для анимации дождя я использовал стандартный движок частиц (particles) Spark AR Studio. Здесь ничего особенного нет. Добавляем на сцену объект Emtter, не забыв задать ему Emitter -> Space -> Local (вместо World). Это для того, чтобы капли не отставали динамически от тучи при движении, а падали бы всегда прямо. Такой способ удобнее для более легкого определения момента попадания бабочки под дождь — не надо делать поправку на высоту. Для самих капель я подготовил соответствующую текстуру. Ну и, разумеется, объект дождя будет перемещаться совместно с объектом тучи. Затем я добавил в код обработки коллизий условие попадания под тучу. И, если над игроком в этот момент отсутствует препятствие, то бабочка падает и игра заканчивается.

Модерация


Для успешного прохождения модерации, обязательное видео из игры не должно содержать статического текста, не привязанного к объектам. Иначе робот мгновенно останавливает прохождение модерации. Однако, после этого в течение нескольких дней игру проверяет человек. И ему не понравилось обилие текста перед игрой, а также в конце. Пришлось убавить количество текста. После этого игру одобрили.

Резюме


Не то, чтобы моя игра получилась особо захватывающей. Вероятно, ей не хватает динамики и дополнительных механик. Я выпущу обновление. Но я понял, что делать игры под Инстаграм интересно. Это своего рода вызов. Я получил огромное удовольствие от процесса программирования и решения всевозможных задач в условиях минимализма в плане инструментария и количества отводимой памяти. Помнится, кто-то сказал, что 640 Кб хватит всем. А теперь попробуйте уместить 3D игру в 2 Мб. Я, пожалуй, не стану утверждать, что их всем хватит… Но попробуйте!


В заключение я бы хотел собрать в один список все неочевидные моменты, с которыми я столкнулся. Может быть, это кому-нибудь пригодится в качестве шпаргалки при создании игр для Инстаграм.

  • Масштаб. Подгоняйте масштаб всех 3D моделей в 3D редакторе, чтобы на сцене в игре он сразу был 1:1.
  • Сортировка мешей сложных объектов может вести себя непредсказуемо. Поэтому сложные объекты лучше разделить на слои и экспортировать эти слои в разные файлы. Отсюда также следует, что не стоит вообще создавать одним файлом слишком сложные сцены с глубиной.
  • Текстуры. Экспортировать модели из редактора следует в ту версию формата FBX, которой не предполагает встраивание текстур. На сцену лучше добавлять модели и текстуры по-отдельности. Тогда вы избежите дублирования текстур при использовании их в разных моделях, что сократит размер дистрибутива.
  • Еще про текстуры. Формат JPG экономичнее по объему, что критично для финального билда, поэтому те текстуры, в которых не используется прозрачность по альфа-каналу, лучше сжать в JPG, остальные сохранить в PNG.
  • И последнее про текстуры. Никогда, слышите, никогда не прописывайте текстурам в моделях абсолютные пути. А для Spark AR вообще желательно, чтобы текстуры лежали в одной папке с самой моделью. Впрочем, это, скорее, пожелание для 3D-моделеров.
  • Если вы удаляете 3D объект, то делайте это правильно: сначала удалите его со сцены, а затем из ассетов. Не удаляйте объекты напрямую через файловую систему. Иначе модель все равно останется в проекте и будет раздувать размер финального билда.
  • Объекты, отображающие текст на экране, должны получать в качестве значений строки. Нестрогая типизация javascript здесь не работает.
  • В контексте Spark AR отсутствует объект window и всевозможные специфичные для конкретных браузерных движков объекты типа webkitRequestAnimationFrame, mozRequestAnimationFrame и так далее. Это стоит участь при программировании на javascript анимации.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+11
Комментарии 3
Комментарии Комментарии 3

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн