Текст Домашнего задания на английском языке доступен на iTunes в пункте “Developing iOS 8 app: Programming: Project 5″. Текст Задания 5 на русском языке размещен в PDF — файле
Для выполнения Задания 5 необходимо освоить материал Лекции 12 и Лекции 13.
В качестве прототипа кода для Задания 5 можно использовать код приложения «Dropit«, полученный на Лекции 12, который доступен на сайте Stanford для Swift 1.2 и Xcode 6 и здесь для для Swift 2.0 и Xcode 7.
В этом посте подробно описывается выполнение некоторых обязательных пунктов Задания 5. Вторая часть решения Задания 5 находится здесь.
И я хочу сделать акцент на том, как правильное использование таких конструкций Swift как Наблюдатели didSet{} , willSet{}, установленные на свойства, включая outlets, как вычисляемые свойства и lazy ( отложенная) инициализация в сочетании с методами «жизненного цикла» UIView и UIViewController могут не только давать очень понятный и компактный код, но и вызывать автоматический многоходовый запуск вычислений, которые вы только описали и не запускали явно. Это создает действительно «магическое» впечатление.
Код для Swift 1.2 и Xcode 6 (если вы еще не установили Xcode 7) находится на Github. Код для Swift 2.0 и Xcode 7 находится на Github. В посте представлен код для Swift 2.0 и Xcode 7.
Пункты 1, 2, 3,4 обязательные
1. Создайте простую Breakout игру, используя Dynamic Animator. Bаш UI не должен выглядеть в точности, как в разделе «Экраны приложения». Креативность приветствуется.
Я не сильна в дизайне и поэтому использовала понравившийся мне дизайн игры из работы Jeroen Schoenberg на Github. Он представлен на рисунке сверху. Наше приложение будет построено на двух MVC: одно MVC будет соответствовать экранному фрагменту непосредственно самой игры Breakout, а другое MVC — настройкам. Начнем с MVC игры Breakout. Вот какие требования выдвигают обязательные пункты Задания 5
2. Когда “кирпич” ударяется мячиком, то должна происходить какая-то анимация. Например, “кирпич” может переворачиваться или вспыхивать другим цветом, прежде чем исчезнет и т.д. Покажите нам, что вы знаете как анимировать изменения в UIView.
3. В дополнение к поддержке жеста pan для перемещения “ракетки”, вы должны поддерживать жест tap, который “толкает” ударный мячик в случайном направлении с определенной силой (то есть заметной, но не разрушающей игру силой!).
4. Когда все “кирпичи” уничтожены (или игра закончилась по другой причине), дайте сигнал (alert) пользователю об окончании игры и переустановите “кирпичи” для следующей игры.
Из всего этого мы понимаем, что основными участниками нашего игрового MVC являются:
- «Удаляющий Мячик» ( или «Мячики») — структура данных balls,
- «Кирпичи», статично сконфигурированные определенным образом на экране — структура данных bricks,
- «Ракетка», перемещающаяся горизонтально и позволяющая направлять «Удаляющий Мячик» на «кирпичи» — структура данных paddle.
Все они действуют в игровом поле Dynamic Animator, причем «кирпичи» и «ракетка» являются пассивными ( хотя и движущимися как «ракетка») границами для аниматора, с которыми сталкивается активный «Удаляющий Мячик» («Мячики»).
Помимо игрового поля, в нашем MVC есть информационные метки со счетом и с оставшимися в игре «Удаляющими Мячиками». Эти метки не участвуют в динамической анимации, хотя формируются по результатам анимации в игровом поле.
Давайте посмотрим, как можно реализовать основное MVC игры Breakout. Для игровое поля мы будем использовать отдельное view класса BreakoutView, расположенное на топовом view нашего View Controller
Вот как это будет выглядеть на storyboard : на пустой View Controller добавлено BreakoutView, метки BALLS, POINTS и метки с соответствующими им значениями ⦁⦁⦁ и 0 для количества мячей в игре и счета. Расположение всех меток выполнено с использованием механизма Autolayout.
Для таким образом сформированного на storyboard View Controller создаем класс BreakoutViewController, в котором определяем outlets для всех меток и BreakoutView. (пока не обращайте внимание на код, об этом будет рассказано ниже).
Наш класс BreakoutViewController должен подсчитывать количество оставшихся в игре мячиков ballsUsed и счет score и управлять содержимым меток ballsLeftLabel и scoreLabel на основе того, что происходит в игровом поле breakoutView. Для того, чтобы понять, в какой форме к BreakoutViewController будет возвращаться информация об ударах мячиком «кирпичей» или о «вылете» мячика за границы игрового поля, подробно рассмотрим класс BreakoutView для нашего игрового поля.
Класс BreakoutView для игрового поля
Здесь мы видим всех действующих лиц игры Breakout:
аниматора animator: UIDynamicAnimator
поведение behavior = BreakoutBehavior(), которое создает физическое поле, действующее на все активные объекты, добавленные в игровое поле (в нашем случае — это «ударяющие мячики»)
«ударяющие мячики« balls : [BallView]
«кирпичи» bricks: [Int : BrickView], расположенные определенным образом на экране,
«ракетка» paddleView, перемещающаяся только горизонтально и позволяющая направлять «удаляющий мячик» («мячики») на «кирпичи»
а также параметры настройки игры, которыми мы сможем управлять:
уровень игры level = [[Int]], задающий расположение кирпичиков на экране в виде двухмерного массива целых чисел
ширина «ракетки» в процентах paddleWidthPercentage :Int
модификатор скорости запуска «ударяющего мячика» launchSpeedModifier :Float изменяющийся в диапазоне от 0 до 1
Для динамического аниматора animator используется lazy (отложенная) инициализация.
Это одна из «магических» фишек Swift (и Objective-C тоже) и я хочу остановится на ней более подробно.
Отложенная (lazy) инициализация динамического аниматора.
Инициализатор UIDynamicAnimator требует указать referenceView в качестве области действия анимации. В нашем случае это breakoutView, то есть self. Если мы это сделаем напрямую, то есть напишем код
var animator: UIDynamicAnimator = UIDynamicAnimator (referenceView: self)
то получим ошибку о недоступности self. В действительности ошибка информирует нас о том, что мы находимся в середине процесса инициализации breakoutView, мы инициализируем составную его часть — переменную animator, а в процессе инициализации мы не имеете доступ к своим собственным свойствам и методам до тех пор, пока инициализация полностью не завершится. Поэтому при инициализации animator у нас нет доступа к self, то есть BreakoutView. И в этом проблема.
Как нам разрешить эту головоломку? Ведь UIDynamicAnimator необходим BreakoutView в качестве параметра referenceView?
Можно сделать animator Optional, и установить его в nil по умолчанию, а затем инициализировать в viewDidLoad, когда вся инициализация уже закончилась. В viewDidLoad можно напрямую создавать animator с BreakoutView в качестве параметра referenceView.
Другой путь состоит в использовании lazy (отложенных) переменных.
Я объявляю переменную animator как lazy и в качестве начального значения использую выполняемое замыкания (с круглыми скобками в конце).
Переменная аниматора animator не будет инициализироваться до тех пор, пока кто-то не запросит этот animator. Но как только его запросят, то будет выполняться это замыкание с круглыми скобками в конце. У него нет аргументов, так что оно вызывается просто с круглыми скобками. Это замыкание возвращает экземпляр класса UIDynamicAnimator.
Но будьте внимательны: лучше не вызывать animator прежде, чем установлен self, то есть BreakoutView. В этом случае инициализируется animator с nil в качестве referenceView. Такой аниматор работать не будет.
Я впервые запрошу animator в Наблюдателе Свойства didSet{} моего outlet breakoutView! в моем BreakoutViewController (он как раз только что установлен по прибытию со storyboard)
. . . . . . . . . . . . . . . . . . . . .
Название «didSet» (уже установлен) говорит само за себя, и мы смело можем использовать Наблюдателя didSet{} для outlet breakoutView не только для установки поведения behavior, но и для косвенной инициализации animator. Это еще одна «магическая» фишка Swift (и Objective-C). Но об этом позже.
Вернемся к нашему игровому полю — классу BreakoutView
Продолжим рассмотрение переменных, связанных с участниками игры, в классе BreakoutView и их инициализацию.
Переменная поведение behavior = BreakoutBehavior() является экземпляром класса BreakoutBehavior, который определяет комплексное поведение «ударяющих мячиков», очень похожее на поведение DropItBehavior в демонстрационном примере Лекции 12 для падающих «красных квадратиков». складывающееся из: ударений collider, гравитации gravity, и “поведения” самого «ударяющего мячика» ballBehavior c усиленным отскоком, без трения и сопротивления. Именно это комплексное поведение behavior было добавлено к аниматору animator в Наблюдателе didSet{} свойства outlet breakoutView! в моем BreakoutViewController. В результате этого сформировалось поле динамической анимации для «ударяющего мячика». Поле динамической анимации дополняется границами для столкновений, которые добавляются и удаляются, а также обнаруживаются во время столкновений по идентификатору. В классе BreakoutBehavior есть соответствующие методы для добавления и удаления границ по идентификатору.
Следующая переменная «удаляющие мячики« balls : [BallView] представляет собой просто массив мячиков, то есть по существу массив UIViews. Мы сделаем его вычисляемой переменной read-only, так как поведение behavior в любой момент точно может сказать, сколько «мячиков» сейчас в игре, поэтому нет необходимости отслеживать появление и удаление мячиков в этом массиве напрямую.
Переменная «кирпичи» bricks: [Int : BrickView] представлена словарем, в котором ключом является порядковый номер «кирпича» в конфигурации уровня игры level, Порядковый номер необходим для идентификации «кирпича» как границы в аниматоре. Значением в словаре является сам «кирпич» BrickView, то есть по существу UIView. Появление и удаление «кирпича» связано с появлением и удалением границ в поведении behavior, поэтому поведение «кирпича» должно быть строго синхронизировано с границами. Расположение «кирпичей» на экране и их размеры на экране определяются при инициализации уровня игры level. Вы видите, что как только устанавливается новое значение level, так сейчас же выполняется метод Наблюдателя didSet{} свойства level.
Сама переменная level. инициализируется все в том же Наблюдателе Свойства didSet{} моего outlet breakoutView! в BreakoutViewController
Таким образом, прослеживается целая цепочка автоматически запускаемых действий:
загрузка со storyboard outlet breakoutView! в BreakoutViewController -> установка переменной level -> создание «кирпичей»
Переменная «ракетка» paddleView, также как и аниматор animator инициируется lazy ( отложенно), так как ее размер зависит от задаваемой пользователем относительной ширины ракетки в %
И опять, «ракетка», также, как и аниматор, впервые инициализируются при установке в Наблюдателе Свойства didSet{} моего outlet breakoutView! в BreakoutViewController
И опять, автоматически запускается цепочка действий.
Поведение «Ударяющих мячиков» и класс BreakoutBehavior
Поведение «ударяющих мячиков» определяется классом BreakoutBehavior, который наследует от UIDynamicBehavior и является составным
Добавляем collider, управляющий поведением, связанным со столкновениями.
который помимо основных функций отслеживает выход «мячика» за границы игровой области и ограничивает его линейную скорость. Для того, чтобы фиксировать столкновение с границами, подтверждаем протокол UICollisionBehaviorDelegate
и реализуем метод, в котором фиксируем столкновение с «кирпичами»
Коллайдер фиксирует два главных события: выход «мячика» за пределы игровой области и удар по «кирпичу», В первом случае мы должны уменьшить количество оставшихся «мячиков» в игре, и, если их не осталось совсем, то объявить, что игра закончена потерей мячей. Во втором случае мы должны увеличить счет игры и визуально показать исчезновение «кирпича», который получил удар, и, если это последний кирпич, то объявить, что игра закончена победой. Оба решения принимаются в главной «штаб-квартире» — в нашем основном View Controller — BreakoutViewController. Для этого взаимодействия мы организуем собственный протокол
В нем два метода:
- метод ballHitBrick (…) позволяет среагировать на ударение с «кирпичом»
- метод ballLeftPlayingField (…) позволяет среагировать на выход «ударяющего мячика» за пределы игрового поля
Вот как эти методы реализованы в BreakoutViewController, который подтвердил наш протокол
Но вернемся к классу BreakoutBehavior. В него добавлены методы удаления и добавления «мячика» в игру.
Интересной особенностью этих методов является то, что они сами удаляют и добавляют «мячики» в иерархию views, так как каждое «поведение» знает своего аниматора, а следовательно и игровое поле этого аниматора, так что в самом игровом поле, то есть в класса BreakoutView, нет необходимости делать это с «мячиками».
В классе «поведения» BreakoutBehavior, есть возможность «заморозить» «мячик» в фиксированной точки, добавляя текущую линейную скорость с противоположным знаком, и , соответственно, «оттаить», убирая эту добавку. Такая возможность используется, например, при переходе на закладку «Setting» ( установки) и обратно.
В классе BreakoutBehavior есть возможность запустить «мячик» с помощью «мгновенного» поведения push («толчок»)
Поведение push («толчок») не причиняет никакого вреда (кроме того, что напрасно занимает память), если вы оставите его подсоединенным к аниматору после того, как “толчок” выполнен, так что удаление его через его собственное свойство action было бы желательно.
Будьте внимательны, так как в замыкании есть ссылка на self, то возникает необходимость в разрыве циклических ссылок памяти с использование weak ( или unowned) в списка захвата (capture list).
Вы можете управлять такими вещами, как упругость (elasticity), трение (friction), сопротовление (resistance) и вращение (rotation) анимируемых объектов. используя экземпляр класса UIDynamicItemBehavior. В классе BreakoutBehavior присутствует поведение самого»мячика» как объекта с идеальными физическими свойствами для «отскока»:
идеальная упругость — elasticity = 1.0
отсутствие трения — friction = 0.0
отсутствие сопротивления — resistance = 0.0
В классе BreakoutBehavior есть два метода работы с границами, которые можно добавить (или убрать) в зависимости от логики игры.
Эти действия с границами можно осуществить, только по идентификатору. Этот идентификатор должен быть NSCopying ( то есть объектом, который реализует NSCopying протокол). NSString и NSNumber, оба реализуют этот протокол (а вследствии взаимозаменяемости (bridging), это означает, что вы можете передавать String или Int, Double и т.д). В дальнейшем мы будем использовать для идентификации границ нашего игрового поля и «ракетки» идентификаторы типа String, а для «кирпичей» — идентификатор типа Int. Нам не нужно писать дополнительных методов работы с границами для различных типов идентификатора, String и Int, что я видела в некоторых работах на Github. И String, и Int реализуют протокол NSCopying. Но, когда этот NSCopying вернется к вам назад в вашем collisionDelegate, то для того,чтобы его использовать, нам придется выполнить “кастинг” от NSCopying к String или Int с помощью as или as!.
В классе BreakoutBehavior есть переменная, способная дать информацию обо всем «мячиках», находящихся в данный момент в игре
Основная «жизнь» «»ударяющих мячиков» протекает в классе BreakoutBehavior, но рождаются они и запускаются в классе игрового поля BreakoutView :
Методы removeBall и removeBalls в классе игрового поля BreakoutView по существу являются транзитными, то есть они здесь присутствуют для того, чтобы не допустить прямого обращения BreakoutViewController к экземпляру классу BreakoutBehavior.
Очень интересный метод placeBallBack, который нам нужен для возвращение «мячика» в игру, например, в случае автовращения. Мы помещаем «мячик» прямо впереди “ракетки”.
При таком способе расположения, если пользователь будет применять жесты taps для толчков, то преимущественными направлениями будут направления вверх к “кирпичам” или удар непосредственно о “ракетку” и затем быстрое движение вверх ( а если он в середине движения, а вы сделали автовращение, он должен продолжать двигаться к “кирпичам” или границам или удариться изо всех сил о “ракетку” и отскочить). Но если вы у какого-то UIView изменили center или выполнили transform, а аниматор “держит” этот UIView в своем поле зрения, то вы должны сказать аниматору, что вы сделали это. Чтобы сказать ему об этом нужно вызовать метод updateItemUsingCurrentState. В качестве аргумента вы передаете view. Аниматор поймет, что этот элемент переместился, и изменит свое внутреннее состояние, чтобы начать оттуда, куда элемент переместился. Имеет ли это смысл? Конечно, если вы и аниматор начнете “бороться” за то, кому перемещать view, то вы должны, по крайней мере, взаимодействовать друг с другом.
Есть еще одно очень интересное вычисляемое свойство, позволяющее «замораживать» «мячики» при уходе, например, на закладку Settings (установки) и «оттаивать» «мячики» при возвращении в игровое поле
Вот как используется это свойство в нашем BrealoutViewController
Мы продолжим рассматривать «ракетку» и «кирпичи», а также закладку Settings ( установки)
Интересное задание, разбирал ваше решение, вспомнил тригонометрию! Теперь знаю как с помощью артангеса находить недостающий угол и какая разница между Velocity и ее Magnitude 🙂
Я тоже люблю это Задание 6, жаль что его нет ни в курсе iOS 10, ни в курсе iOS 11.