Текст Домашнего задания на английском языке доступен на iTunes «Developing iOS 9 Apps with Swift. Programming Project 6: Animation». Текст Задания 6 на русском языке размещен в PDF — файле
Для выполнения Задания 6 необходимо освоить материал Лекции 14.
В качестве прототипа кода для Задания 6 можно использовать код приложения «Dropit L14«, полученный на Лекции 14, который доступен на сайте Stanford для Swift 2 и Xcode 7 ; для Xcode 8 и Swift 2.3 — на Github, для Xcode 8 и Swift 3 — на Github, для Xcode 8, Swift 3 и iOS 10 — на Github.
В этом посте подробно описывается выполнение обязательных и дополнительных пунктов Задания 6. В посте представлен код для Xcode 8, Swift 3 и iOS 10, который находится на Github в разделе Breakout_Assignment_6.
Да, будем выполнять Домашнее Задание 6 сразу для iOS 10 и Swift 3, потому что Задание 6 не связано с данными, получаемыми из сети, не связано с существенно усовершенствованными в iOS 10 фреймворками Core Data и UserNotifications. Приложение Задания 6 будет очень простым в архитектурном плане — там не будет ни SplitViewController, ни специальных segues — Popovers, Unwind Segues, Embedded Segues, Modal Segues. Оно будет представлять собой два слабо связанных между собой MVC, объединенных простейшим множественным MVC — TabBarController. В Задании 6 нам предлагается продемонстрировать умение работать с системой Dynamic Animation, управляя динамическими элементами в виде UIViews. Кроме того, мы можем в таком простейшем приложении показать все возможные механизмы запуска кода — код в iOS приложениях не исполняется линейно, отдельные его фрагменты можно запустить с помощью:
- жестов,
- методов «жизненного цикла» UIViewController ( viewWillAppear, viewDidLoad, viewDidLayoutSubviews и т.д.) и UIView ( layoutSubviews )
- Наблюдателей Свойства didSet{ }, willSet{ },
- lazy ( отложенной) инициализации,
- инициализации в виде выполняемого замыкания ( с круглыми скобками «()» в конце),
- делегирования, которое реализуется в Swift свойствами в виде замыканий,
- вычисляемых переменных с хранением в другом месте (другое свойство, NSUserDefaults, Core Data).
Все эти механизмы мы будем использовать в нашем простейшем приложении и они могут не только давать очень понятный и компактный код, но и вызывать автоматический каскадный запуск вычислений, которые вы только описали и не запускали явно. Это создает действительно «магическое» впечатление.
Пункты 1, 2, 3,4 обязательные
1. Создайте простую Breakout игру, используя Dynamic Animator. Bаш UI не должен выглядеть в точности, как в разделе «Экраны приложения». Креативность приветствуется.
Наше приложение будет построено на двух MVC: одно MVC будет соответствовать экранному фрагменту непосредственно самой игры Breakout, а другое MVC — настройкам. Начнем с MVC игры Breakout. Вот какие требования выдвигают обязательные пункты Задания 6
2. Когда “кирпич” ударяется мячиком, то должна происходить какая-то анимация. Например, “кирпич” может переворачиваться или вспыхивать другим цветом, прежде чем исчезнет и т.д. Покажите нам, что вы знаете как анимировать изменения в UIView.
3. В дополнение к поддержке жеста pan для перемещения “ракетки”, вы должны поддерживать жест tap, который “толкает” ударный мячик в случайном направлении с определенной силой (то есть заметной, но не разрушающей игру силой!).
4. Когда все “кирпичи” уничтожены (или игра закончилась по другой причине), дайте сигнал (alert) пользователю об окончании игры и переустановите “кирпичи” для следующей игры.
Из всего этого мы понимаем, что основными участниками нашего игрового MVC являются:
- «Удаляющий Мячик» ( или «Мячики») — структура данных balls,
- «Кирпичи», статично сконфигурированные определенным образом на экране — структура данных bricks,
- «Ракетка», перемещающаяся горизонтально и позволяющая направлять «Удаляющий Мячик» на «кирпичи» — структура данных paddle.
Все они действуют в игровом поле Dynamic Animator, причем «кирпичи» и «ракетка» являются пассивными ( хотя и движущимися как «ракетка») границами для аниматора, с которыми сталкивается активный «Удаляющий Мячик» («Мячики»).
Помимо игрового поля, в нашем MVC есть информационные метки со счетом и с оставшимися в игре «Удаляющими Мячиками». Эти метки не участвуют в динамической анимации, хотя формируются по результатам анимации в игровом поле.
Давайте посмотрим, как можно реализовать основное MVC игры Breakout. Для игровое поля мы будем использовать отдельное UIView класса BreakoutView, расположенное на топовом view нашего View Controller
Вот как это будет выглядеть на storyboard : на пустой View Controller добавлено BreakoutView, метки BALLS, POINTS и метки с соответствующими им значениями ⦁⦁⦁ и 0 для количества мячей в игре и счета. Расположение всех меток выполнено с использованием механизма Autolayout.
Для таким образом сформированного на storyboard View Controller создаем класс BreakoutViewController, в котором определяем outlets для всех меток и нашего игрового поля Breakout View (пока не обращайте внимание на код, об этом будет рассказано ниже).
Наш класс BreakoutViewController должен подсчитывать количество оставшихся в игре мячиков ballsUsed и счет score и управлять содержимым соответствующих меток ballsLeftLabel и scoreLabel на основе того, что происходит в игровом поле Breakout View, а также отображать интенсивность «гравитации».
Наш MVC с игрой Breakout будут обслуживаться тремя основными классами, мы должны распределить полномочия между Controller, UIView и UIDynamicBehavior. Существует бессчетное количество приемлемых решений, но у вас должен быть план, и он состоит в следующем:
класс BreakoutViewController (наследует от UIViewController) отвечает за взаимодействие с пользователем и отображение информационной части игры: счет, количество оставшихся мячиков, а также за получение параметров настройки от MVC настроек,
класс BreakoutView (наследует от UIView) отвечает за все, происходящее в игровом поле: запускает и останавливает анимацию, создает игровую обстановку: «кирпичи», «ракетку» и «мячики», следит за отработкой жеста pan по перемещению «ракетки», следит за перерисовкой «кирпичей» при автовращении, является транзитом при передачи информации от класса BreakoutViewController к классу BreakoutBehavior, отражающего поведение «ударяющегося мячика» в присутствии границ в виде «кирпичей» и «ракетки»
класс BreakoutBehavior (наследует от UIDynamicBehavior и реализует протокол UICollisionBehaviorDelegate) описывает «поведение» мячика как «динамического объекта» UIDynamicItemBehavior, который можно запускать, тормозить и запускать заново после торможения в условиях столкновения с именованными границами.
Давайте подробно рассмотрим центральную фигуру в этой «троице» — класс BreakoutView для нашего игрового поля.
Класс BreakoutView для игрового поля
Мы видим здесь всех «действующих лиц» игры Breakout:
аниматора animator: UIDynamicAnimator:
поведение behavior = BreakoutBehavior(), которое создает физическое поле, действующее на все активные объекты, добавленные в игровое поле (в нашем случае — это «ударяющие мячики»)
«кирпичи» bricks: [Int : BrickView], расположенные определенным образом на экране,
«ударяющие мячики« balls : [BallView]
«ракетку» paddleView, перемещающуюся только горизонтально и позволяющую направлять «удаляющий мячик» («мячики») на «кирпичи»
а также параметры настройки игры Breakout, которыми может управлять пользователь :
уровень игры levelInt: Int?, целочисленное значение, определяющее массив level = [[Int]], задающий способ расположение «кирпичиков» на экране в виде двухмерного массива целых чисел:
Мы выбираем соответствующее расположение кирпичей на экране из массива Levels.levels, содержащего варианты в виде двухмерного массива целых чисел (если вы хотите разнообразить поведение «кирпичей», то можно задавать целое значение > 1, так как ничего не говорится о том, что “кирпич” должен исчезать при первом же ударе ) :
ширину «ракетки» в процентах paddleWidthPercentage :Int
включение /выключение анимации animating: Bool. Если кто-то установит эту переменную в true, то мы начнем анимацию. Все, что нужно для анимации, — это взять аниматор и добавить к нему “поведение” behavior. Как только вы добавите “поведение” к аниматору animator, то начнется анимация. Подобно этому, если вы хотите выключить анимацию, то нужно сделать противоположное — убрать “поведение” behavior из аниматора. Это остановит анимацию.
модификатор скорости запуска «ударяющего мячика» launchSpeedModifier:Float, изменяющийся в диапазоне от 0 до 1, с его помощью мы сможем регулировать скорость запуска «ударяющего мячика» как часть от диапазона изменения:
интенсивность «гравитации» gravityMagnitudeModifier:Float, изменяющаяся в диапазоне от 0 до 1, с его помощью мы сможем регулировать какая часть ускорения свободного падения g будет действовать на «мячик» («мячики»):
Для динамического аниматора animator используется lazy (отложенная) инициализация в виде выполняемого замыкания.
Это одна из «магических» фишек Swift (и Objective-C тоже) и я хочу остановится на ней более подробно.
Отложенная (lazy) инициализация динамического аниматора.
Инициализатор UIDynamicAnimator требует указать referenceView в качестве области действия анимации. В нашем случае это breakout View, то есть self. Если мы это сделаем напрямую, то есть напишем код
var animator: UIDynamicAnimator = UIDynamicAnimator (referenceView: self)
то получим ошибку о недоступности self. В действительности ошибка информирует нас о том, что мы находимся в середине процесса инициализации breakoutView, мы инициализируем составную его часть — переменную animator, а в процессе инициализации мы не имеете доступ к своим собственным свойствам и методам до тех пор, пока инициализация полностью не завершится. Поэтому при инициализации animator у нас нет доступа к self, то есть Breakout View. И в этом проблема.
Как нам разрешить эту головоломку? Ведь UIDynamicAnimator необходим BreakoutView в качестве параметра referenceView?
Можно сделать animator Optional, и установить его в nil по умолчанию, а затем инициализировать в viewDidLoad, когда вся инициализация уже закончилась. В viewDidLoad можно напрямую создавать animator с BreakoutView в качестве параметра referenceView.
Другой путь состоит в использовании lazy (отложенных) переменных.
Я объявляю переменную animator как lazy и в качестве начального значения использую выполняемое замыкания (с круглыми скобками в конце).
Переменная аниматора animator не будет инициализироваться до тех пор, пока кто-то не запросит этот animator. Но как только его запросят, то будет выполняться это замыкание с круглыми скобками в конце. У него нет аргументов, так что оно вызывается просто с круглыми скобками. Это замыкание возвращает экземпляр класса UIDynamicAnimator.
Поведение «Ударяющих мячиков» и класс BreakoutBehavior
Класс BreakoutBehavior определяет комплексное поведение «ударяющих мячиков» («мячика»), очень похожее на поведение DropItBehavior в демонстрационном примере Лекции 14 для падающих «квадратиков», складывающееся из «поведений»:
- гравитации gravity,
- столкновений collider и
- “поведения” самого «ударяющего мячика» ballBehavior как физического объекта c усиленным отскоком (он абсолютно упругий), без трения и сопротивления.
Комплексное «поведение» BreakoutBehavior инициализируется путем добавления всех трех «поведений» collider, gravity, и ballBehavior как «дочерних»:
В результате этого сформировалось поле динамической анимации для «ударяющих мячиков (мячика)». Поле динамической анимации дополняется границами для столкновений, которые добавляются и удаляются, а также обнаруживаются во время столкновений по идентификатору. В классе BreakoutBehavior есть соответствующие методы для добавления и удаления границ по идентификатору, которые мы рассмотрим ниже.
Давайте рассмотрим все три «поведения» подробно.
«Поведение» gravity (тип его UIGravityBehavior ) управляет «гравитацией»:
Гравитационное поле, воздействующее на «ударяющий мячик» сильно влияет на движение «мячика» — если оно имеет стандартное ускорение g = 1000 points / second², то «мячику» даже трудно добраться до нижнего ряда «кирпичей» (слишком сильно «гравитация» тянет вниз) — поэтому мы дадим пользователю возможность регулировать интенсивность гравитационного поля, тем самым облегчая или усложняя условия игры Breakout, с помощью переменной gravityMagnitudeModifier.
«Поведение» collider (тип его UICollisionBehavior ) управляет столкновениями. Будем инициализировать collider одним замечательным способом, с помощью выполняемого замыкания:
Я создаю внутри замыкания локальную переменную collider — и это “поведение” столкновения UICollisionBehavior. Я не делаю мое игровое поле referenceView для моего аниматора границами, как это было сделал профессор в демонстрационном приложении на Лекции 14, потому что мне нужно, чтобы мой «мячик» не отскакивал от нижней части экрана, а «проваливался» вниз:
collider.translateReferenceBoundsIntoBoundary = false
Поэтому к “поведению” столкновения collider автоматически не добавятся границы нашего referenceView как границы для столкновений. Мы будем определять границы для столкновений в зависимости от размеров самого игрового поля в классе BreakoutView c помощью функции resetLayout (), которая увеличивает границу по «высоте» вдвое:
то есть так:
Но с помощью action нашего сollider мы проверяем, не вышел ли наш «мячик» («мячики») за пределы игрового поля referenceView динамического аниматора self.dynamicAnimator!, которому принадлежит сollider. Если да, то мы вызываем функцию leftPlayingField,
которая определена как переменная leftPlayingField типа замыкание определенного типа в классе BreakoutBehavior:
var leftPlayingField : ((_ ball: BallView)-> ())?
Еще мы в action нашего сollider ограничиваем линейную скорость наших «мячиков» («мячика») с помощью функции limitLinearVelocity, о которой поговорим позже.
И в конце замыкания, определяющего «поведение» столкновений, мы возвращаем collider.
Когда мы создаем переменную с помощью выполняемого замыкания, то мы должны ЯВНО указывать этой тип переменной, в нашем случае UICollisionBehavior, потому что компилятор хочет убедиться, что возвращает значение подходящего типа.
Для того, чтобы фиксировать столкновения с границами, подтверждаем протокол UICollisionBehaviorDelegate в классе BreakoutBehavior:
и реализуем метод collisionBehavior( beganContactFor: withBoundaryIdentifier: at:) делегата UICollisionBehaviorDelegate, в котором установим факт столкновения с «кирпичами»:
И опять, если мы определили, что столкновение «мячика» ball произошло с границей, имеющей целочисленный идентификатор brickIndex, то вызывается функция hitBreak, которая должна обработать эту ситуацию. Функция hitBreak определена как переменная hitBreak типа замыкания определенного типа в классе BreakoutBehavior:
var hitBreak : ((_ behavior: UICollisionBehavior, _ ball: BallView,
_ brickIndex: Int)-> ())?
Таким образом, «поведение» столкновения collider фиксирует два главных события, происходящих в игровом поле: выход «мячика» за пределы игровой области путем вызова переменной-замыкания leftPlayingField, и удар по «кирпичу» путем вызова переменной-замыкания hitBreak. В первом случае мы должны уменьшить количество оставшихся «мячиков» в игре, и, если их не осталось совсем, то объявить, что игра закончена потерей мячей. Во втором случае мы должны увеличить счет игры и визуально показать исчезновение «кирпича», который получил удар, и, если это последний кирпич, то объявить, что игра закончена победой. Оба решения принимаются в главной «штаб-квартире» — в нашем основном View Controller — BreakoutViewController. Для этого взаимодействия мы реализуем в классе BreakoutViewController эти функции:
функция ballHitBrick вызывается при столкновении «мячика» с «кирпичем» и это приводит к анимационному удалению «кирпича» с экрана, а если больше «кирпичей не осталось, то к экстренному уведомлению пользователя о ВЫИГРЫШИ с помощью Alert;
функция ballLeftPlayingField вызывается при покидали «мячиком» игрового поля в процессе столкновений с экрана с «кирпичами». В том случае происходит потеря «мячика», а если это был последний «мячик» , то к экстренному уведомлению пользователя о ПОТЕРЕ ВСЕХ МЯЧЕЙ И ПРОИГРЫШИ с помощью Alert:
Добавим эти функции в качестве замыканий
при установке outlet breakoutView в классе BreakoutViewController:
Мы здесь видим очень компактную реализацию паттерна делегирования в виде замыканий.
Но вернемся к классу BreakoutBehavior.
Нам осталось описать третье поведение — «поведение» самого «мячика» ballBehavior как физического объекта.
Вы можете управлять такими характеристиками «мячика», как упругость (elasticity), трение (friction), сопротовление (resistance) и вращение (rotation), используя экземпляр класса UIDynamicItemBehavior для анимируемых объектов. В классе BreakoutBehavior присутствует поведение самого»мячика» как объекта с идеальными физическими свойствами для «отскока»:
идеальная упругость — elasticity = 1.0
отсутствие трения — friction = 0.0
отсутствие сопротивления — resistance = 0.0
В классе BreakoutBehavior есть переменная balls, способная дать информацию обо всем «мячиках», находящихся в данный момент в игре:
Основная «жизнь» «»ударяющих мячиков» протекает в классе BreakoutBehavior, В этом классе вы можете добавлять «мячики» («мячик») в игру и удалять»мячики» («мячик») из игры:
Интересной особенностью этих методов является то, что они сами удаляют и добавляют «мячики» в иерархию views, так как каждое «поведение» знает своего аниматора, а следовательно и игровое поле этого аниматора, так что в самом игровом поле, то есть в класса BreakoutView, нет необходимости делать это с «мячиками».
В классе «поведения» BreakoutBehavior, есть возможность «заморозить» «мячик» в фиксированной точки, добавляя текущую линейную скорость с противоположным знаком, и , соответственно, «оттаить», убирая эту добавку. Такая возможность используется, например, при переходе на закладку «Setting» ( установки) и обратно.
В классе BreakoutBehavior есть возможность запустить «мячик» с помощью «поведения» pushBehavior («толчок») с определенной силой magnitude, действующей мгновенно:
Угол, под которым действует этот толчок, является случайным и расположен в диапазоне 1.25 * π — 1.25 * π:
Поведение pushBehavior («толчок») не причиняет никакого вреда (кроме того, что напрасно занимает память), если вы оставите его подсоединенным к аниматору после того, как “толчок” выполнен, так что удаление его через его собственное свойство action было бы желательно:
В конце мы добавляем «поведение» pushBehavior как «дочернее»:
В свойстве action нашего «поведения» collider мы ограничили скорость наших «мячиков» с помощью функции limitLinearVelocity:
Эта функция добавлена в расширение extension класса UIDynamicItemBehavior и она не только ограничивает линейную скорость динамического объекта, но и меняет цвет фона (backgroundColor) динамического объекта в зависимости от линейной скорости, если динамический объект — это UIView (в нашем случае (BallView):
Цвет фона меняется от белого до желтого, оранжевого, красного и малинового, имитируя «разогрев» динамического объекта item ( в нашем случае «мячика»).
В классе BreakoutBehavior есть два метода работы с границами, с помощью которых можно добавить (или убрать) границы в зависимости от логики игры.
Эти действия с границами можно осуществить, только по идентификатору. Этот идентификатор должен быть NSCopying ( то есть объектом, который реализует NSCopying протокол). NSString и NSNumber, оба реализуют этот протокол (а вследствии взаимозаменяемости (bridging), это означает, что вы можете передавать String или Int, Double и т.д). В дальнейшем мы будем использовать для идентификации границ нашего игрового поля и «ракетки» идентификаторы типа String, а для «кирпичей» — идентификатор типа Int. Нам не нужно писать дополнительных методов работы с границами для различных типов идентификатора, String и Int, что я видела в некоторых работах на Github. И String, и Int реализуют протокол NSCopying. Но, когда этот NSCopying вернется к вам назад в вашем collisionDelegate, то для того,чтобы его использовать, нам придется выполнить “кастинг” от NSCopying к String или Int с помощью as или as!, как мы делали это в методе делегата UICollisionBehaviorDelegate, когда фиксировали столкновение «мячика» с «кирпичами»:
Таким образом, класс BreakoutBehavior посвящен «поведению» «мячиков» в среде, где действуют силы гравитации, происходят столкновения с границами и сам «мячик» является физическим упругим объектом.
Класс BreakoutBehavior предполагает, что в любой текущий момент времени в этой среде может действовать множество «мячиков» (а как частный случай — один «мячик»). Реально сколько «мячиков» может находится в игре зависит от логики игры, то есть от того, запускается ли новый «мячик» в игру только после того, как «текущий» «мячик» выйдет из игры, или нет.
В нашей игре «мячик» запускается в игру жестом tap в игровой зоне. Для этого мы в нашей штаб-квартире — класса BreakoutViewController — в Наблюдателе Свойства didSet {} в outlet breakoutView добавляем жест tap, оставляя обработку этого жеста launchBall в классе BreakoutViewController :
В нашем случае в игре всегда участвует один «мячик», так как метод addBallToGame() срабатывает только тогда, когда в игровом поле больше нет «мячиков». Если «мячик» движется в игровом поле, то жест tap интерпретируется как дополнительный «толчок» (push) «мячика», который выполняется методом pushBalls().
Но можно построить логику игры и по-другому: при повторном жесте tap не толкать уже движущийся в игровом поле «мячик», а добавлять новый мячик в игровое поле, а для толчка «мячиков» воспользоваться другим жестом, например, двойным tap.
Но мы этого делать не будем, у нас всегда один «мячик» в игре.
Управление «действующими лицами» игры в классе BreakoutView.
«МЯЧИКИ»
Хотя основная «жизнь» «»ударяющих мячиков» протекает в классе BreakoutBehavior, но «рождаются» они, запускаются в игру и «умирают» в классе игрового поля BreakoutView:
Выше мы уже видели использование этих методов в нашей штаб-квартире — классе BreakoutViewController — при старте игры с помощью жеста tap, а также при определении ситуаций ВЫИГРЫША и ПРОИГРЫША в специальных замыканиях поведения столкновений collider. Методы removeBallFromGame и removeBallsFromGame в классе игрового поля BreakoutView по существу являются транзитными, то есть они здесь присутствуют для того, чтобы не обращаться напрямую к методам класса BreakoutBehavior.
Добавление «мячика» в игру производится с помощью метода addBallToGame (), который представлен выше. В этом методе создается BallView, соответствующий «мячику», добавляется к поведению behavior и запускается методом launchBall, в котором в качестве аргумента необходимо указать значение мгновенной силы magnitude, действующей на «мячик» при толчке и определяющий его скорость запуска. При добавлении «мячика» в игру пользователь может регулировать скорость запуска мячика launchSpeed между минимальным и максимальным значением с помощью множителя launchSpeedModifier:
Помимо простого добавления «мячика» в игру, мы можем в любой момент «подтолкнуть» все мячики, находящиеся в игре, с помощью метода pushBalls (), также представленного выше. Для этого метода значение мгновенной силы magnitude, действующей на «мячик» при толчке и определяющий его скорость запуска, определяется константой Constants.pushSpeed.
Очень интересный метод placeBallBack, который нам нужен для возвращение «мячика» в игру, например, в случае автовращения.
Мы помещаем «мячик» прямо впереди “ракетки”.
При таком способе расположения, если пользователь будет применять жесты taps для толчков, то преимущественными направлениями будут направления вверх к “кирпичам” или удар непосредственно о “ракетку” и затем быстрое движение вверх ( а если он в середине движения, а вы сделали автовращение, он должен продолжать двигаться к “кирпичам” или границам или удариться изо всех сил о “ракетку” и отскочить). Но если вы у какого-то UIView изменили center или выполнили transform, а аниматор “держит” этот UIView в своем поле зрения, то вы должны сказать аниматору, что вы сделали это. Чтобы сказать ему об этом нужно вызовать метод updateItemUsingCurrentState. В качестве аргумента вы передаете view. Аниматор поймет, что этот элемент переместился, и изменит свое внутреннее состояние, чтобы начать оттуда, куда элемент переместился. Имеет ли это смысл? Конечно, если вы и аниматор начнете “бороться” за то, кому перемещать view, то вы должны, по крайней мере, взаимодействовать друг с другом.
Есть еще одно очень интересное вычисляемое свойство ballsVelocities, позволяющее «замораживать» «мячики» при уходе игры с экрана, например, на закладку Settings (установки) и «оттаивать» «мячики» при возвращении в игру:
Вот как используется это свойство в нашей штаб-квартире BrealoutViewController в методах «жизненного цикла» viewWillAppear и viewWillDisappear :
«РАКЕТКА»
Пункты 1, 2, 3,4 обязательные. «Ракетка»
«Ракетка» не является динамическим объектом, управляемым динамическим аниматором animator, она не добавляется ни к какому «поведению». Это просто граница, хотя и перемещаемая с помощью жеста pan. Поэтому «жизнь» «ракетки» протекает полностью в игровом поле, то есть в классе BreakoutView. Однако аниматор animator постоянно «чувствует» ее как границу для столкновений, поэтому любое появление или перемещение «ракетки» в классе BreakoutView должно сопровождаться изменением ее границ в аниматоре. Столкновение с этой границей-«ракеткой» не обрабатывается методами делегата UICollisionBehaviorDelegate класса UICollisionBehavior, нам достаточно того, что «мячик» от нее отскакивает.
Но типом идентификатора этой границы-«ракетки»
struct Constants {
. . . . . . . . . .
static let paddleBoundaryId = «paddleBoundary»
. . . . . . . . . . }
специально выбран String, который отличает по типу границу — «ракетку» от границ — «кирпичей», идентификаторы которых имеют тип Int. Это позволит при столкновении очень быстро отделить интересующие нас столкновения от других.
«Ракетка» paddle, также как и аниматор animator инициируется lazy ( отложенно), так как ее размер зависит от задаваемой пользователем относительной ширины ракетки в % paddleWidthPercentage :
“Ракетка” управляется жестом pan, c помощью которого осуществляется только горизонтальное перемещение «ракетки». Для этого мы в нашей штаб-квартире — класса BreakoutViewController — в Наблюдателе Свойства didSet {} в outlet breakoutView добавляем жест pan
, предусматривая и распознавание, и обработку этого жеста (метод panPaddle) в классе BreakoutView:
Именно в последнем методе «ракетка» paddle через поведение behavior добавляется к аниматору как граница. Вы видите, что граница «ракетки» в этом коде сделана BezierPath овалом (хотя сама “ракетка” выглядит на UI прямоугольником). Это заставляет «мячик» отскакивать от “ракетки” более интересно.
Мы можем в любой момент времени переустановить «ракетку» принудительно, например, в начале игры или при выходе за границы игрового поля с помощью двух методов, опирающихся на размеры игрового поля self.bounds:
Эти функции работают в методе layoutSubviews() для игрового поля breakoutView, в методах, вызываемых при автовращении прибора, а также при старте и перезапуске игры:
«КИРПИЧИ»
Пункты 1, 2, 3,4 обязательные. «Кирпичи»
«Кирпичи», также как и «ракетка», не являются динамическими объектами, управляемыми динамическим аниматором animator, они не добавляются ни к какому «поведению». Поэтому «жизнь» «кирпичей»,
также, как и «ракетки», в основном протекает в игровом поле, то есть в классе BreakoutView. Однако аниматор animator постоянно «чувствует» их как границы для столкновений с «мячиками», поэтому любое появление или пропадание «кирпичей» в классе BreakoutView должно сопровождаться изменением их границ в аниматоре.
Ключом в словаре bricks : [Int: BrickView] является порядковый номер «кирпича», он же необходим как идентификатор границы для «кирпича» в аниматоре. Значением в словаре является сам «кирпич» BrickView, то есть по существу UIView. Появление и удаление «кирпича» связано с появлением и удалением границ в поведении behavior, поэтому изменение положения «кирпича» должно быть строго синхронизировано с изменением границ для аниматора.
Расположение «кирпичей» на экране и их размеры определяются при инициализации уровня игры levelInt — , целочисленного значения, определяющего массив level = [[Int]], задающий способ расположение кирпичиков на экране в виде двухмерного массива целых чисел. Мы выбираем соответствующее расположение кирпичей на экране из массива Levels.levels, содержащего варианты в виде двухмерного массива целых чисел:
Вы видите, что как только пользователь устанавливает новое значение levelInt, так сейчас же выбирается соответствующий двумерный массив расположения «кирпичей» из 4-х возможных, представленных выше, и выполняется метод Наблюдателя didSet{} свойства level и срабатывает метод reset(), формирующий «действующих лиц» для начальной фазы игры Breakout, то есть убираются с экрана все старые «кирпичи», все оставшиеся в старой игре «мячики», и генерируются новые «кирпичи» и «ракетка», которая располагается в центре экрана:
В отличие от «ракетки», границы-«кирпичи» обрабатываются методами делегата UICollisionBehaviorDelegate класса UICollisionBehavior. С точки зрения логики игры Brealout нас очень интересуют столкновения «мячика» с «кирпичом».
В методе collisionBehavior( beganContactFor: withBoundaryIdentifier: at:) делегата UICollisionBehaviorDelegate в классе BreakoutBehavior коллайдер collider фиксирует удар по «кирпичу», вызывая переменную-замыкание hitBreak:
В этом случае мы должны увеличить счет игры и визуально показать исчезновение «кирпича», который получил удар, и, если это последний кирпич, то объявить, что игра закончена победой. Это решение принимается в главной «штаб-квартире» — в нашем основном View Controller — BreakoutViewController. Организация взаимодействия коллайдера collider со «штаб-квартирой» BreakoutViewController осуществляется функцией ballHitBrick, в которой происходит удаление «кирпича» с экрана с анимацией, а если больше «кирпичей не осталось, то уведомление пользователя о ВЫИГРЫШИ с помощью Alert:
Но реально «кирпич» удаляется с анимацией в своем «родном» классе BreakoutView.
И опять, первое, что мы делаем, — удаляем границу, связанную с «побитым» «кирпичом» и идентифицируемую индексом «битого кирпича» brickIndex. Затем выполняем анимацию и удаляем «кирпич» из словаря «кирпичей» bricks.
В классе BreakoutView есть все необходимые методы работы с «кирпичами»:
— вы можете создать «кирпич»:
— вы можете создать конфигурацию «кирпичей» по схеме, заданной в уровне игры level = [[Int]] :
— вы можете удалить»кирпич»:
— вы можете удалить все «кирпичи»:
— вы можете переустановить оставшиеся в игре «кирпичи» при автовращении
Пункты 5, 6, 7, 8 обязательные.
5. Ваша игра должна быть сконструирована так, чтобы поддерживать не менее 4-х различных параметров, которые управляют способом, каким ведется игра (например, число “кирпичей”, ударная сила мячика, число ударных мячиков, наличие гравитационного поля, “специальные кирпичи”, которые вызывают интересное поведение и т.д.).
6. Используйте TabBarController, чтобы добавить еще одну закладку к вашему UI, которая содержит статическую таблицу TableView c управляющими элементами, которые позволят пользователю установить этим 4+ различным параметрам игры значения. Ваша игра должна начать использовать их немедленно (как только вы кликните на кнопку возврата к главной закладке игры).
7. MVC для настройки этой игры должен использовать по крайней мере по одному из следующих 3-х iOS классов: UISwitch, UISegmentedControl и UIStepper (вы можете использовать UISlider вместо UIStepper, если считаете, что это больше подходит вашим настройкам).
8. Конфигурация вашей игры должна сохраняться постоянно между запусками приложения.
Создаем MVC для Settings (Установок). На storyboard из Палитры Объектов добавляем новый Table View Controller, задаем Content — Static Cells и Style — Grouped для статической Table View и добавляем метки ( labels), переключатели (switches), ползунки (sliders) и т.д. Не забываем подключить механизм Autolayout и добавляем прекрасные иконки.
Мы будем настраивать 5 параметров игры Breakout:
- уровень игры (1, 2, 3, 4 и т.д.),
- ширину «ракетки» в % (small, medium, large),
- максимальное число «мячиков» в игре (Int)
- коэффициент увеличения скорости «мячика» (0.0 — 1.0)
- включение реальной гравитации (Bool)
- коэффициент интенсивности гравитации (0.0 — 1.0)
Создадим класс SettingsTableViewController для нашего нового View Controller с установками и не забудем его подключить к новому View Controller на storyboard. Для каждого регулируемого элемента (метки ( labels), переключателя (switches), ползунков (sliders) и т.д.) создадим outlets
и Actions
Мы видим, что Actions связаны с экземпляром класса Settings
Класс Settings содержит вычисляемые переменные, взаимодействующими с хранилищем NSUserDefaults
Все вычисляемые переменные в классе Settings устроены одинаково: get{} извлекает данные из хранилища NSUserDefaults, а set{} их туда записывает. Причем обратите внимание, что запись идет в соответствии с типом записываемого значения:
userDefaults.set (newValue, forKey: Keys.MaxBalls)
. . . . .
userDefaults.set (newValue, forKey: Keys.BallSpeedModifier)
а при извлечении данных из NSUserDefaults всегда используется objectForKey(…), но с последующим «кастингом» as?. Это дает возможность подключить значения, заданные по умолчанию Defaults.MaxBalls, Defaults.Level и т.д. с помощью оператора ??.
return userDefaults.object (forKey: Keys.MaxBalls) as? Int ?? Defaults.MaxBalls
Получились фантастически простые и очень конструктивные вычисляемые переменные для работы с NSUserDefaults (рисунок выше): level, ballSpeedModifier, maxBalls, paddleWidth, realGravity, gravityMagnitudeModifier.
Если эти переменные встречаются в коде в правой части выражения, то мы извлекаем (читаем) значения из NSUserDefaults, и если их еще там нет, то мы получаем значение по умолчанию, а если — в левой части выражения, то мы записываем новые значения в NSUserDefaults. Другими словами, эти вычисляемые переменные класса Settings всегда актуальны и мы можем их использовать в MVC игры Breakout в любое время.
Возвращаемся к классу SettingsTableViewController. Как только MVC с «настройками» появляется на экране, мы извлекаем значения из NSUserDefaults в методе «жизненного цикла» viewDidAppear и выставляем согласно им наш UI:
Работа с UI с помощью Action устанавливает новые значения для NSUserDefaults:
Все. Наш MVC Settings c «настройками» готов.
Объединяем MVC игры Breakout с MVC Settings с помощью Tab Bar Controller.
При появлении MVC игры Breakout на экране все настройки считываются и устанавливаются в методе «жизненного цикла» viewWillAppear:
Мы можем оперативно настраивать параметры игры и возвращаться обратно, при этом анимация «замораживается», а при возвращении «оттаивает» и начинает функционировать с новыми параметрами.
Сейчас наша игра предполагает, что в любой момент времени у нас в игре находится один мячик. Но компоновка классов настолько гибкая, что позволяет буквально изменением пары строк кода в методе launchBall класса BreakoutViewController настроить игру на возможность запуска одновременно нескольких «мячиков»:
Код находится на Github в разделе Breakout_Assignment_6.