Сделай это спрайт

Вы когда-нибудь хотели создать свою аркадную игру на мобильной платформе Apple? Тогда присоединяйтесь ко мне в этом путешествии, где вы улучшите свое понимание наиболее часто используемых фреймворков в среде Apple Game Development. Верно, я говорю о SpriteKit и GameplayKit!
В частности, эта статья будет посвящена SpriteKit.

Это Луиджи, и сегодня я собираюсь показать вам, как я сделал Fugu, простую игру, в которой вы будете играть за рыбу-фугу, которая должна избегать прикосновений к плохим морским конькам и уворачиваться от пузырей, которые они бросают в вас. Команды довольно просты, заключаются в перемещении пальца по экрану, чтобы соответствующим образом переместить иглобрюха. Каждый раз, когда вы получаете урон от пузыря, размеры рыбы-фугу будут увеличиваться, а также давать визуальную подсказку о том, какие удары вы еще можете выдержать (три удара и все готово), а контакт с врагом отправит вас прямо в игра за экраном. У вас есть счет, который увеличивается с течением времени, поэтому чем больше вы сможете противостоять атакам врага, тем лучше будет ваш счет. Вот как выглядит игра:

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

Обязательно загляните в его репозиторий GitHub, чтобы вы могли ознакомиться со всем проектом, пока я буду его объяснять.



Также есть ссылка External TestFlight для опробования игры перед ее выпуском в App Store (что произойдет очень скоро). Обязательно откройте эту ссылку с устройства iOS!



Так как о SpriteKit и о том, как он работает, можно много говорить, и я не хочу, чтобы вы весь день читали скучные вещи, мы будем упрощать только те части кода, которые сложно понять!

Прежде всего, нам нужно понять весь поток GameScene (фактическая часть, в которой происходит игровой процесс) и то, как SpriteKit обрабатывает некоторые события, такие как входные данные от пользователя или заданные нами ограничения.

На самом деле есть некоторые специфичные для GameScene функции, которые мы можем использовать, чтобы волшебство произошло.

Мы говорим о функциях update, touchesBegan/touchesMoved/touchesEnded, didMove(to), didBegin и didEnd.

Некоторые из этих функций зависят от физической части SpriteKit, и мы рассмотрим их позже.

Начнем с самых важных на данный момент.

Функция didMove(to)

Функция didMove(to) GameScene в основном происходит, когда вы переходите к игровой сцене.

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

Здесь вы также можете определить некоторые свойства самой GameScene, такие как отображение статистики отладки (FPS, текущие визуализированные узлы), настройка того, будут ли отображаться физические тела и многое другое.

В этом проекте мы собираемся использовать didMove(to) для создания нашего главного героя Фугу, супер-морковки, которая время от времени будет появляться, текста счета и некоторых скрытых вещей, которые нам понадобятся позже.

Начнем с добавления Fugu, который будет объявлен в области видимости GameScene как экземпляр класса Puffer.

Вы можете проверить код в проекте, если хотите получить более подробную информацию, но класс Puffer в основном содержит методы, которые создают различные анимации Fugu в зависимости от его состояния, и логическое свойство с именем damageable, которое мы используем, чтобы понять, когда Фугу уязвим для вражеских атак.

Просто зайдите под GameScene и добавьте это:

var puffer = Puffer()

Теперь мы можем вернуться к функции didMove(to) и создать нашу прекрасную рыбку-фугу.

Как видите, линий только на маленькую рыбку фугу очень много.

Мы только что настроили некоторые свойства экземпляра puffer, такие как его положение на экране (точно посередине), некоторые физические свойства, которые мы собираемся обсудить позже, его положение по оси Z, поэтому это перед экраном (потому что наш фон будет в более низкой позиции Z) и его масштаб (размер), поскольку реальное разрешение актива выше, чем размер, который мы хотим на экране, поэтому мы собираемся установить его на 1/4 своего первоначального размера.

Однако самым важным шагом во всем этом является добавление рыбы-фугу в GameScene. Это делается командой addChild(puffer) .

Это в основном добавляет спрайт в качестве дочернего элемента GameScene (который является родителем, который будет содержать все наши элементы), поэтому мы будем использовать эту команду каждый раз, когда мы хотим добавить что-то на экран.

Мы также можем использовать это, чтобы добавить фоновое изображение и баннер, но мы поместим их позже через редактор, чтобы справиться с размером сцены.

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

Просто напишите это перед предыдущим фрагментом кода:

Мы только что создали два значения SKRange для позиций X и Y Фугу, чтобы он не мог выйти за эти пределы. Мы используем значения self.frame.width и self.frame.height, чтобы использовать кадр сцены (просто края экрана) в качестве ограничения.

Затем мы применяем это ограничение к экземпляру puffer с модификатором .constraints.

Теперь нам нужно применить некоторые свойства к самой сцене.

Помимо всех физических свойств, которые мы собираемся обсудить позже, мы можем заметить некоторые интересные модификаторы для самой GameScene, такие как .showsFPS,.showsNodeCountи.showsPhysics, , который можно соответственно использовать для отображения счетчика кадров в секунду, количества визуализированных узлов на экране и хит-боксов физики узлов во время выполнения.

Как видите, мы также добавили дочерний элемент worldNode в GameScene. Это значение SKNode, которое действует как контейнер для других SKSpriteNodes, и мы будем использовать его для приостановки всех других движущихся элементов на экране, когда игра приостанавливается.

Пришло время добавить на сцену метку партитуры. При этом используется значение gameScore, которое будет использоваться для отслеживания оценки пользователя с течением времени.

Добавьте это под ранее написанным фрагментом кода:

При этом мы добавили метку в верхнем углу экрана.

Функция обновления

Теперь поговорим о функции update.

Это одна из основных функций, о которой вам нужно знать, так как это функция, которую ваш GameScene вызывает в каждом кадре (когда ваша сцена не поставлена на паузу). Это очень много звонков, уверяю вас!

Если, например, ваша игра будет работать со скоростью 60 FPS (кадров в секунду), что обычно и происходит, функция update будет вызываться 60 раз в течение одной секунды.

Это изображение объясняет, что именно происходит для каждого кадра в приложении SpriteKit.

Как видите, первое, что происходит, — это запуск всего, что находится внутри функции обновления.

После функции обновления GameScene (SKScene) оценивает любое действие (движение спрайтов, переходы и т. д.) или управляющий оператор, который вы могли определить, затем приступает к моделированию своей физики (например, столкновения между спрайтами), применяя ограничения (например, у вас может быть персонаж в фиксированном положении, независимо от того, какая сила приложена к нему) и, наконец, визуализирует каждый элемент на экране.

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

Однако в нашем случае мы можем использовать эту функцию, чтобы со временем увеличить наш счет.

Давайте сначала объявим переменную gameScore внутри области видимости GameScene:

Теперь, когда у нас установлен счет на 0 при запуске игры, его просто нужно увеличить.

Вот как мы это сделали:

То, что произойдет во время нашего игрового процесса, — это добавление 1/3/6 очков к нашему счету в начале каждого кадра, пока игра не поставлена на паузу. Количество очков, даваемых в каждом кадре, зависит от переменной pufferState, а это означает, что чем сильнее мы повреждены, тем больше очков мы получим, поскольку нам будет труднее избегать пузырей, учитывая увеличенный размер Фугу.

Функции touchesBegan/touchesMoved/touchesEnded

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

touchesBegan():

Здесь мы обработаем начальную часть взаимодействия, когда мы начинаем нажимать на экран.

Это входит в область действия GameScene:

Что мы делаем здесь, так это устанавливаем логическое значение в true, когда мы управляем Fugu (мы будем использовать это в следующей части), а также используем startTouch, которое является значением CGPoint, для закрепления части экрана, которую мы только что коснулись. . Затем мы сохраняем в nodePosition фактическое положение Фугу, чтобы мы могли переместить его позже.

touchesMoved():

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

То, что здесь происходит, довольно просто. Все начинается с того, что мы controlling Фугу, что означает, что мы начали касаться экрана.

Мы просто меняем позицию Фугу с помощью SKAction.move на вычисленные значения X и Y, которые просто берут исходную позицию из nodePosition (с ее значением, установленным в функции touchesBegan) и добавляют к ней X и Y location, временное значение, используемое в эта функция позволяет отслеживать, куда движется палец, удерживая его на экране. Вот и все!

touchesEnded():

Как мы уже говорили, touchesEnded() обрабатывает то, что происходит в тот момент, когда мы убираем палец с экрана.

Мы не используем эту функцию по какой-либо конкретной причине, кроме установки логического значения controlling в false, чтобы мы знали, что больше не контролируем Fugu.

Давайте займемся физическими упражнениями

Теперь мы собираемся поговорить о той части, которая делает игру живой, а именно о физике.

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

Способ, которым SpriteKit обрабатывает физику, довольно прост. Мы можем присвоить каждой сущности (в нашем случае SKSpriteNodes) определенные .physicsBody значения. Начнем с анализа свойств Fugu.

Вы можете видеть, что у нас есть много модификаторов для .physicsBody, но не расстраивайтесь, понимая, что эта работа не так сложна, как кажется.

Прежде чем говорить о назначенных ему модификаторах, мы должны понять, какое физическое тело имеет наш спрайт. Есть 3 возможных физических тела: квадрат, круг и альфа-маска.

В нашем случае это круглое тело с радиусом, равным исходной высоте спрайта, назначенного puffer (то есть это не считая масштаба 0,25, который мы установили в функции didMoveTo), что означает, что если вы установите view.showsPhysics от didMove(to) до true, вы увидите что-то вроде этого вокруг тела Фугу:

Это настоящее физическое тело, которое мы ему присвоили, и оно имеет форму круга. Достаточно просто.

Два других атрибута, которые говорят сами за себя, это .affectedByGravity и isDynamic.

По сути, установив для этих двух значений значение false, мы не применяем никакой силы, вызванной гравитацией всей сцены (которая в любом случае установлена на 0), и мы также уверены, что независимо от того, что происходит, тело Фугу не столкнется при контакте с другими телами. .

Теперь самое интересное.

В физике SpriteKit мы используем битовые маски категорий, чтобы понять, какая категория тела соприкасается с другой. Мы бы не смогли понять, например, когда Фугу касается вражеского снаряда, самого врага или просто края экрана.

Давайте объявим битмаски нашей категории вне области видимости GameScene:

Битовые маски категорий — это не что иное, как двоичные целые числа, поскольку вы можете увидеть их фактическое значение в комментариях.

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

Вернуться к puffer.

Вы можете видеть, что иглобрюх идентифицируется (по его .categoryBitMask) как PhysicsCategory.Ally, единственная категория тел, с которыми он будет сталкиваться (что приводит к остановке самого себя или перемещению другого тела с достаточным импульсом), - это PhysicsCategory.Wall тела, и тела, которые мы хотим знать. о том, когда в контакте (да, это .contactTestBitMask) находятся Bullet, Enemy, Carrot, Letter и Wall. Это означает, что если мы не поместим эти категории под .contactTestBitMask, мы не сможем контролировать, когда они будут сталкиваться.

Потратьте немного времени, чтобы лучше усвоить эти концепции, поскольку вся логика физики SpriteKit основана на этих трех модификаторах!

Теперь давайте поговорим о последних двух функциях GameScene, которые относятся к физике SpriteKit: didBegin() и didEnd(). Это две функции, которые принимают участие, когда столкновение между двумя телами начинается и заканчивается (как следует из их названия), но для того, чтобы эти две функции работали, нам нужно иметь SKPhysicsContactDelegate, короче говоря, сцену, которая имеет право смотреть ищу контакты физиков. Мы уже присвоили его нашей GameScene при объявлении!

class GameScene: SKScene, SKPhysicsContactDelegate {

Нам просто нужно добавить эту строку кода в функцию didMove(to):

physicsWorld.contactDelegate = self

При этом GameScene официально назначается контактным делегатом!

Перейдем к двум его функциям.

сделалНачало():

Эта функция вызывается нашей игрой всякий раз, когда происходит контакт между двумя SKPhysics телами.

Здесь у нас есть константа collision, равная контакту между bodyA и bodyB.

Поскольку битовые маски категорий являются двоичными значениями, SpriteKit считает контакт между двумя с помощью оператора | (логическое ИЛИ).

Исходя из того, что Фугу может получать урон (поскольку могут быть фазы, когда он неуязвим), мы будем вызывать разные функции в зависимости от того, какие тела сталкиваются друг с другом.

Важно отметить, что порядок тел имеет значение, поэтому у нас есть два случая, которые вызывают функцию enemyHit(), которая возникает, когда Фугу касается или касается врага.

Поскольку пули всегда будут лететь с фиксированной скоростью, они всегда будут первыми, кто вступит в контакт с Фугу, поэтому нам нужно только одно условие для получения урона от пуль.

То же самое относится и к супер-морковке, поскольку ее положение всегда будет фиксированным, и первым телом, которое столкнется с ней, будет Фугу, несмотря ни на что.

сделалКонец():

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

И это все на данный момент!

Если вы хотите узнать больше о создании игр для iOS с использованием фреймворков Apple, вам может пригодиться эта статья, написанная Орестом Леоне. Здесь Орест расскажет, как работает GameplayKit! Я настоятельно рекомендую вам это сделать, так как GameplayKit — это потрясающая среда, которая поможет вам сделать ваши игры еще более структурированными.