Столкновения
Столкновения (на английском collide) необходимы в игре, чтобы игровые объекты чувствовали друг друга. Нам, как программистам необходимо каким-то образом узнавать, что главный герой, например, столкнулся с платформой, и мы тем самым не даём ему провалиться "сквозь" землю. Или, например, в игре могут быть опасные преграды, в виде шипов, колючей проволоки, мины и так далее, напарываясь на которые наш герой погибает. А можно сделать врагов, без интеллекта, которые просто двигаются по заранее определённому маршруту, или с интеллектом, которые всё время охотятся и бегают за нами, как за главным героем, чтобы уничтожить (например "умные" зомби, которые чувствуют Ваше присутствие и всё время направляются на Вас). Чтобы программным путём узнать, что враг на Вас напал, опять на выручку приходят коллайдеры.
В Phaser 3 довольно богатое разнообразие задания коллизий объектам и плиткам для нашей карты из Tiled. Давайте перечислим их:
- setCollision(indexes [, collides] [, recalculateFaces] [, layer] [, updateLayer])
- setCollisionBetween(start, stop [, collides] [, recalculateFaces] [, layer])
- setCollisionByExclusion(indexes [, collides] [, recalculateFaces] [, layer])
- setCollisionByProperty(properties [, collides] [, recalculateFaces] [, layer])
- setCollisionFromCollisionGroup( [collides] [, recalculateFaces] [, layer])
setCollision
Установить (включить) коллизию у данного объекта из Tiled, индекс которого равен index (либо массив индексов). Этот метод справедлив как для статических, так и для динамических слоёв (createStaticLayer и createDynamicLayer). Второй параметр collides булевое значение, если true - коллизии будут доступны, если false , коллизии будут отключены. По умолчанию true. Третий передаваемый параметр recalculateFaces опциональный и имеет булевое значение. Указывает, следует ли пересчитывать грани плитки после обновления (столкновения). Четвёртый параметр layer имя используемого слоя плитки. Если не указан, используется текущий слой. И, наконец, последний параметр updateLayer булевое значение. Если true, обновляет текущие плитки на слое. Установите значение false, если плитки не были размещены для значительного повышения производительности.
setCollisionBetween
Устанавливает коллизию в диапазоне плиток в слое, индекс которого находится между указанным началом и остановкой (включительно). Вызов этого с начальным значением 10 и конечным значением 14 установит коллизию для плиток 10, 11, 12, 13 и 14. Параметр collides управляет включением (true) или отключением коллизии (false).
Третий передаваемый параметр recalculateFaces опциональный и имеет булевое значение. Указывает, следует ли пересчитывать грани плитки после обновления (столкновения). Четвёртый параметр layer имя используемого слоя плитки. Если не указан, используется текущий слой.
setCollisionByExclusion
Устанавливает коллизию для всех плиток в данном слое, кроме плиток, индекс которых указан в данном массиве. Параметр collides определяет, будет ли столкновение включено (true) или отключено (false). Индексы листов, которые в данный момент отсутствуют в слое, не затрагиваются. Если слой не указан, используется текущий слой карты.
Если указан индекс -1, то установить коллизию для всех плиток.
setCollisionByProperty
Устанавливает столкновение плиток внутри слоя, проверяя свойства плитки. Если у плитки есть свойство, которое соответствует заданному объекту свойств, будет установлен его флаг коллизии. Параметр collides определяет, будет ли столкновение включено (true) или отключено (false). Передача {collides: true} обновит флаг столкновения на всех плитках со свойством "collides", имеющим значение true. Любая плитка, для которой не установлено значение «collides», будет игнорироваться. Вы также можете использовать массив значений, например { types: ["stone", "lava", "sand" ] }. Если плитка имеет свойство «types», которое соответствует любому из этих значений, ее флаг столкновения будет обновлен. Если слой не указан, используется текущий слой карты.
Давайте вернёмся к нашим баранам (игре) и на примерах рассмотрим как устанавливать коллизии и использовать их на практике. Если Вы самостоятельно повторяете код нашей демо игры у себя на компьютере, то должны были заметить, что наш главный герой проваливается сквозь землю. Теперь мы знаем как устранить данный недостаток, необходимо установить плиткам из карты коллизии. Но и это не всё. Необходимо добавить коллизии между главным героем и платформой. Дело в том, что рассмотренные выше методы не добавляют коллизии, а только устанавливают так называемые флаги коллизии, чтобы программа знала, какие плитки будут "твёрдые", а какие "сквозные" (т.е. без коллизии).
В Phaser 3 существует два способа добавить коллизии объектам:
- this.physics.add.collider(objectsA, objectsB, collideCallback);
- this.physics.add.overlap(objectsA, objectsB, collideCallback);
Collider
Выполняет проверку столкновений объектов и разделение между двумя заданными физическими объектами. Последним аргументом передаётся функция обратного вызова. Как только указанные объекты сталкиваются, эта функция незамедлительно вызывается. Аргументами callback являются объекты, которые столкнулись:
Overlap
Похож на collider, только если вам не требуется разделение, используйте вместо него overlap.
Кроме функции обратного вызова collideCallback, следующим параметром можно передавать так называемые processCallback, который запускается, когда gameObject1 пересекает gameObject2, параметр необязательный.
Итак, давайте вспомним, что мы когда-то создавали карту из Tiled и вставляли её в нашу программу следующим образом:
Теперь установим флаг коллизии для каждой плитки this.platforms:
И нашему главному герою, если помните мы добавили коллайдер:
Теперь, наконец, наш главный герой не проваливается "сквозь" землю и успешно бегает и прыгает по платформам. Отлично, мы получили замечательный профит.
Создание шипов
Теперь мы вооружившись значительными знаниями без особого труда можем добавлять различного рода опасные преграды и пропасти. Начнём в нашем примере с добавления шипов. Но прежде чем будем добавлять их непосредственно на сцену, пару слов скажу о группах в Phaser и о некоторых полезных методах объекта Map, касающихся объектов в Tiled.
Группы спрайтов в Phaser 3 нужны для коллекционирования спрайтов в едином контейнере для манипулирования сразу всеми спрайтами одновременно. Например чтобы не создавать каждый шип в отдельности и не задавать для каждого коллизию, проще создать одну группу и добавлять спрайты в цикле. Затем одним методом collider добавить коллизии всех шипов с главным героем.
В Phaser 3 существует два типа групп, статические и динамические:
- this.physics.add.staticGroup(children, config)
- this.physics.add.group(children, config);
Объект конфигурации довольно большой и я приведу его просто для справки, без описания каждого:
Объект карты Map имеет ряд полезных методов, чтобы извлечь из нашей карты объекты. Я пока перечислю три из всех, а по мере необходимости в статьях буду описывать другие полезные методы для работы с объектами слоя:
- getObjectLayer( [name])
- filterObjects(objectLayer, callback [, context])
- findObject(objectLayer, callback [, context])
getObjectLayer
Получает ObjectLayer из this.objects с заданным именем или null, если ObjectLayer с таким именем не найден. Например, чтобы извлечь все объекты шипы из нашей карты, необходимо прописать:
А в Tiled требуется соответственно добавить эти шипы , там где нам это надо и задать этому слою имя Spikes:
filterObjects
Для каждого объекта на заданном уровне объекта запустите заданную функцию обратного вызова фильтра. Любые объекты, прошедшие проверку фильтра (т.е. где обратный вызов возвращает true), будут возвращены как новый массив. Подобно Array.prototype.Filter в ванильном JS. Данный метод понадобиться в будущем.
findObject
Находит и возвращает первый объект в данном слое объектов, который удовлетворяет предоставленной функции обратного вызова. Т.е. находит первый объект, для которого обратный вызов возвращает true. Подобно Array.prototype.find в ванильном JS.
Расставив там где Вам нравиться шипы по карте, давайте вставим их в нашу игру, используя теоретические выкладки выше. Предварительно оговорюсь по поводу того, что к сожалению необходимо иметь отдельно спрайт с изображением шипа, так как из нашего Tileset отдельно объекты "не вытащить". Я отдельно в папке assets/images сохранил картинку spike.png и конечно же загрузил её в программе с помощью this.load.image:
Здесь, spikeObjects это массив с найденными объектами шипов. Пробегаемся по этому массиву с помощью forEach и создаём для каждого объекта спрайт (который тут же добавляется в группу и рендерится на экране).
Для создания из объекта Tiled нужного нам спрайта, у групп есть замечательный метод create
- create( [x] [, y] [, key] [, frame] [, visible] [, active])
Создает новый игровой объект и добавляет его в эту группу, если группа не заполнена.
x - координата по оси X, куда вставляется спрайт
y - координата по оси Y, куда вставляется спрайт
key - ключ текстуры, который мы определили в this.load.image
Остальные параметры необязательные. Здесь мы не будем описывать все параметры подробно, так как есть документация, где Вы сможете детально ознакомиться с каждым методом и параметром. Цель этих статей не подробное и исчерпывающее описание всего API Phaser 3, а лишь главные моменты для создания вполне себе живых и полноценных игр на Phaser 3.
Каждый объект из массива spikeObjects имеет такие свойства, как:
- x - координата по оси X в карте Tiled
- y - координата по оси Y в карте Tiled
- width - ширина объекта
- height - высота объекта
- rotation - угол в радианах, на который повёрнут объект в карте (Tiled предоставляет такую возможность, как вращение объектов)
- И другие свойства
Наконец, для вновь созданных и вставляемых спрайтов мы применяем два метода: setSize и setOffset. Это необходимо для того, чтобы уменьшить немного размер спрайта при вставке (попробуйте убрать эти методы и Вы поймёте о чем речь) и соответственно сместить их, так как мы изменяем размеры тела спрайта.
В конце мы добавляем коллизию главного героя с шипами, передав вторым параметром всю группу с шипами. Функция обратного вызова сработает, если мы напоримся на любой шип во время игры. И рассмотрим мы эту функцию в дальнейших статьях, так как сейчас ещё рановато о ней говорить, мы не знаем как работать со сценами и создавать уровни с переходами.
Падение героя в воду
Принцип здесь точно такой же, как мы это делали с шипами. Только с небольшим изменением. Давайте сперва взглянем на код и разберём его "по косточкам":
Если Вы заметили, мы вместо видимого спрайта, вставили просто картинку с прозрачным фоном. Дело в том, что мы уже вставили плитки с водой, но не как Platforms, а как слой под названием Others (как this.map.createStaticLayer('Others', tileset, 0, 200);
). А объекты waterObjects нам нужны только для того, чтобы определить что главный герой их пересек, а это значит, что он погиб (упал в воду). И чтобы наша вода не перекрылась никакими спрайтами, мы просто вставляем прозрачные картинки поверх плиток с водой. Чтобы лучше понять идею ещё раз взгляните на screenshot из Tiled:
Вас наверное смутили странные прибавления координат 200, 150, 250 и так далее. Дело в том, что изначально мы сместили карту на 200 пикселей вниз, так как я извиняюсь, и не сказал Вам сразу, что в Phaser 3 почему то создатели решили сделать начало координат не в верхнем левом углу (как в Canvas), а в центре игрового экрана (поэтому мы применяли если помните метод setOrigin, чтобы сместить начало координат в верхний левый угол), плюс мы сделали наш первый уровень по высоте 7 плиток, а то что выше должно просто заполниться фоном. Вот поэтому мы вынуждены были сместить карту вниз, а соответственно теперь все координаты объектов не совпадают, так как они есть в Tiled, и приходиться немного "костылить", чтобы всё подогнать точно по пикселям. Но не думайте что это так работает во всех играх. Думаю в хорошо продуманных и профессиональных проектах дизайнеры предусмотрели, чтобы не было никаких смещений, а выдают карты программистам "тутелька в тютельку".
P.S. Статья получилась очень информативной, много новых методов и свойств для запоминания. Но, как говорят, тяжело в учении, легко в бою. Потратьте немного времени на изучении и переваривании данного материала и поверьте мне, потом всё окупиться в виде Ваших красивых и насыщенных игр в будущем. А может даже это станет Вашим поприщем всей жизни, кто знает.