Задание 5. Решение. Ощутите «магию» Swift на простейшей игре c Dynamic Animation (часть 2)

Screen Shot 2015-09-11 at 10.50.23 AM
В этом посте представлено продолжение решения обязательных и дополнительных пунктов Задания 5. Первую часть  можно посмотреть здесь.
Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Developing iOS 8 app: Programming: Project 5″Текст Задания  5 на русском  языке размещен в PDF — файле

Задание 5 iOS 8.pdf


Для выполнения Задания 5 необходимо освоить материал Лекции 12 и Лекции 13.
В качестве прототипа кода для Задания 5 можно использовать код приложения «Dropit«, полученный на Лекции 12, который доступен на сайте Stanford для Swift 1.2 и Xcode 6 и здесь для для  Swift 2.0 и Xcode 7.

Я, как и в первой части,  делаю акцент на том, как правильное использовать такие конструкций Swift как Наблюдатели didSet{} , willSet{}, установленные на свойства, включая outlets, когда применять вычисляемые свойства и lazy ( отложенную) инициализацию. Все это в сочетании с методами «жизненного цикла» UIView и UIViewController дает не только очень понятный и компактный код, но и вызывает автоматический многокаскадный запуск вычислений, которые вы только описали и не запускали явно. Это создает действительно «магическое» впечатление. Особенно хочется отметить класс Settings, описанный в этом посте, которой двумя строками кода позволяет работать как с данными, хранящимися в NSUserDefaults, так и с данными, заданными по умолчанию, используя только оператор присвоения = .
Код для Swift 1.2 и Xcode 6 (если вы еще не установили Xcode 7) находится на Github. Код для Swift 2.0 и Xcode 7 находится на Github. В посте представлен код для Swift 2.0 и Xcode 7.

Пункты 1, 2, 3,4 обязательные. «Ракетка»

«Ракетка» не является объектом, управляемым динамическим аниматором animator, она не добавляется ни к какому «поведению». Поэтому «жизнь» «ракетки» протекает полностью в игровом поле, то есть в классе BreakoutView. Однако аниматор постоянно «чувствует» ее как границу для столкновений,  поэтому любое появление или перемещение «ракетки» в классе BreakoutView должно сопровождаться изменением ее границы в аниматоре. Столкновение с этой границей-«ракеткой» не обрабатывается методами делегата UICollisionBehaviorDelegate класса UICollisionBehavior, нам достаточно того, что «мячик» от нее отскакивает.
Но типом идентификатора этой границы-«ракетки»

struct Constants {
.  .  .  .  .  .  .  .  .  .
 static let paddleBoundaryId = «paddleBoundary»
.  .  .  .  .  .  .  .  .  . }

специально выбран String, который отличает по типу границу — «ракетку» от  границ — «кирпичей», идентификаторы которых имеют тип  Int. Это позволит при столкновении очень быстро отделить интересующие нас столкновения от других.

В первой части мы уже говорили о том, что «ракетка» paddle, также как и аниматор animator инициируется lazy ( отложенно), так как  ее размер зависит от задаваемой пользователем относительной ширины ракетки в % paddleWidthPercentage

Screen Shot 2015-09-10 at 7.01.25 AM
И опять, «ракетка», также, как и аниматор, впервые инициализируются при установке в Наблюдателе Свойства didSet{} моего outlet breakoutView! в  BreakoutViewController
Screen Shot 2015-09-10 at 7.31.30 AM
“Ракетка” поддерживает жест pan, c помощью которого осуществляется только горизонтальное перемещение «ракетки». Поэтому на наш BreakoutViewController добавлен жест pan и его обработка

Screen Shot 2015-09-11 at 9.15.41 AM
Реально перемещение «ракетки» paddle осуществляется в «родном» для нее классе BreakoutView.
Screen Shot 2015-09-11 at 9.27.07 AM
Именно в этом методе «ракетка» добавляется к аниматору свою границу.
Мы можем в любой момент времени переустановить «ракетку» принудительно, например, в начале игры или при выходе за границы игрового поля.
Screen Shot 2015-09-11 at 9.38.42 AM
И опять, мы добавляется «ракетку» к аниматору в виде границы.

Пункты 1, 2, 3,4 обязательные. «Кирпичи»

«Кирпичи», также как и  «ракетка», не являются объектами, управляемыми динамическим аниматором animator, они не добавляются ни к какому «поведению». Поэтому «жизнь» «кирпичей»,
Screen Shot 2015-09-11 at 10.01.09 AM
также, как и «ракетки», в основном протекает  в игровом поле, то есть в классе BreakoutView. Однако аниматор постоянно «чувствует» их как границы для столкновений с «мячиками»,  поэтому любое появление или перемещение «кирпичей» в классе BreakoutView должно сопровождаться изменением их границ в аниматоре.

«Кирпичи» представлены словарем, в котором ключом является порядковый номер «кирпича» в конфигурации уровня игры level = [[Int]], задающего расположение кирпичиков на экране в виде двухмерного массива целых чисел
Screen Shot 2015-09-08 at 5.09.48 PM

Порядковый номер необходим для идентификации «кирпича» как идентификатор границы в аниматоре. Значением в словаре является сам «кирпич» BrickViewто есть по существу UIView. Появление и удаление «кирпича» связано с появлением и удалением границ в поведении behavior, поэтому изменение положения «кирпича» должно быть строго синхронизировано с изменением границ для аниматора. Расположение «кирпичей» на экране и их размеры на экране определяются при инициализации  уровня игры level. Вы видите, что как только устанавливается новое значение  level, так сейчас же выполняется метод Наблюдателя didSet{} свойства level.
Screen Shot 2015-09-10 at 7.38.56 AM

В отличие от «ракетки», границы-«кирпичи» обрабатываются методами делегата UICollisionBehaviorDelegate класса UICollisionBehavior.  С точки зрения логики игры Brealout нас очень интересуют столкновения «мячика» с «кирпичом». 

Коллайдер collider фиксирует  удар по «кирпичу». В этом случае мы должны увеличить счет игры и визуально показать исчезновение «кирпича», который получил удар, и, если это последний кирпич,  то объявить, что игра закончена победой. Это решение принимается в главной «штаб-квартире» — в нашем основном View Controller — BreakoutViewController. Для организации взаимодействия коллайдера collider со «штаб-квартирой» BreakoutViewController организует собственный протокол 
Screen Shot 2015-09-10 at 10.29.17 AM
В нем два метода:

  • метод  ballHitBrick (…) позволяет среагировать на ударение «мячика» с «кирпичом»
  • метод  ballLeftPlayingField (…) позволяет среагировать на  выход «ударяющего мячика» за пределы игрового поля

Один из этих методов,  ballHitBrick (…), используется коллайдером collider при анализе столкновений
Screen Shot 2015-09-11 at 10.29.27 AM
Вот как этот метод реализован в «штаб-квартире» BreakoutViewController, который подтвердил наш протокол BreakoutCollisionBehaviorDelegate
Screen Shot 2015-09-11 at 10.32.51 AM
Но реально «кирпич» удаляется с анимацией в своем «родном» классе BreakoutView.
Screen Shot 2015-09-11 at 11.03.02 AM
И опять, первое, что мы делаем, — удаляем границу, связанную с «побитым» «кирпичом».
В классе BreakoutView есть метод создания «кирпича»
Screen Shot 2015-09-11 at 11.09.30 AM
а также метод создания конфигурации «кирпичей» по схеме, заданной в уровне игры level = [[Int]],
Screen Shot 2015-09-11 at 11.29.57 AM
и метод переустановки оставшихся в игре «кирпичей» при автовращении
Screen Shot 2015-09-11 at 11.33.56 AM

 

 

Пункты 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, задаем ContentStatic Cells и StyleGrouped для статической Table View и  добавляем метки ( labels), переключатели (switches), ползунки (sliders) и т.д. Не забываем подключить механизм Autolayout и добавляем прекрасные иконки.

Screen Shot 2015-09-11 at 12.18.17 PM
Мы будем настраивать 5 параметров игры Breakout:

  1. уровень игры (1, 2, 3, 4  и т.д.),
  2. ширину «ракетки» в % (small, medium, large),
  3. включение управления ракеткой наклоном прибора (Bool)
  4. максимальное число «мячиков» в игре (Int)
  5. коэффициент увеличения скорости «мячика» (0.0 — 1.0)

Создадим класс SettingsTableViewController для нашего нового View Controller с установками и не забудем его подключить к новому  View Controller на storyboard.  Для каждого регулируемого элемента (метки ( labels), переключатели (switches), ползунки (sliders) и т.д.) создадим outlets

Screen Shot 2015-09-11 at 1.18.26 PM
и Actions
Screen Shot 2015-09-11 at 1.32.43 PM
Мы видим, что Actions связаны с экземпляром класса Settings

Screen Shot 2015-09-11 at 1.36.45 PM

Класс  Settings содержит вычисляемые переменные, взаимодействующими с хранилищем NSUserDefaults
Screen Shot 2015-09-11 at 1.40.38 PM
Все вычисляемые переменные устроены одинаково: get{} извлекает данные из хранилища NSUserDefaults, а set{} их туда записывает. Причем обратите внимание, что запись идет в соответствии с типом записываемого значения:

userDefaults.setInteger(newValue, forKey: Keys.MaxBalls)
.  .  .  .  .
userDefaults.setFloat(newValue, forKey: Keys.BallSpeedModifier)

а при извлечении данных из NSUserDefaults всегда используется objectForKey(…), но с последующим «кастингом» as?. Это дает возможность подключить значения, заданные по умолчанию Defaults.MaxBallsDefaults.Level и т.д. с помощью оператора ??.
return userDefaults.objectForKey(Keys.MaxBalls) as? Int ?? Defaults.MaxBalls
Получился фантастически простой и очень конструктивный класс работы с NSUserDefaults.

Возвращаемся к классу SettingsTableViewController и организуем извлечение данных из NSUserDefaults в методе «жизненного цикла» viewWillAppear класса SettingsTableViewController.

Screen Shot 2015-09-11 at 2.08.03 PM
Все. Наш MVC Settings для установок готов.
Объединяем MVC игры Breakout с MVC Settings с помощью Tab Bar Controller.

Screen Shot 2015-09-11 at 2.18.00 PM
Осталось добавить считывание параметров игры из NSUserDefaults в нашей «штаб-квартире» BreakoutViewController, и мы будем это делать с помощью специального метода loadSettings, вызов которого поместим в методе «жизненного цикла» viewWillAppear класса  BreakoutViewController.
Screen Shot 2015-09-11 at 2.26.00 PM
Screen Shot 2015-09-11 at 2.27.56 PMВсе. Обязательные пункты выполнены. И круг замкнулся: установки автоматически читаются из  NSUserDefaults или становятся равными значениям по умолчанию, «кирпичи» размещаются на экране, «ракетка» нужной ширины также находится на экране. Выполняем жест tap, «мячик» полетел вверх, к «кирпичам» и игра началась. Действительно все происходит магически.

Дополнительный пункт 6 .

 Интегрируйте акселерометр куда-нибудь в ваше приложение (может быть реальное гравитационное воздействие на полет прыгающего мячика?).

 

Я решила интегрировать акселерометр в управление «ракеткой», то есть наклон прибора будет передвигать «ракетку» в сторону наклона.

Первое, что нам необходимо — это менеджер движения, то есть экземпляр класса СMMotionManager. Я говорил вам, что это должна быть глобальная вещь для моего приложения. Я размещу менеджера движения в глобальном месте, которым является мой файл AppDelegate. Я удаляю почти весь текст в AppDelegate и размещаю там структуру, в которой создаю экземпляр менеджера движения Manager.

Screen Shot 2015-09-11 at 3.11.31 PM
Затем в специальном методе loadSettings, с которым мы уже знакомы и вызов которого размещен в методе «жизненного цикла» viewWillAppear класса BreakoutViewController, я проверю, а доступен ли акселерометр, и если акселерометр доступен, то начнем обновлять данные с акселерометра с помощью метода startAccelerometerUpdatesToQueue.

Screen Shot 2015-09-11 at 3.20.21 PM
Я должна определить очередь, в которой я хочу, чтобы происходило обновление данных и задаю main queue с помощью NSOperationQueue.mainQueue().
Далее я пользуюсь способностью Swift выносить последний аргумент за круглые скобки, если этот аргумент — замыкание. Я переименую аргументы замыкания в data и error. Аргумент data — это данные акселерометра. Что касается error, то я хочу игнорировать эту ошибку. В моем случае, если я получу ошибку, то я не буду использовать данные с акселерометра для перемещения «ракетки».
Теперь внутри замыкания я должна задать движение «ракетки» на основании показаний акселерометра, которыми являются data и которые измеряются в gЯ устанавливаю данные с акселерометра только по оси x с некоторым коэффициентом Const.maxPaddleSpeed.

Мы начали получать данные с акселерометра, но мы должны остановить их получение в методе “жизненного цикла” viewWillDisappear.

Screen Shot 2015-09-11 at 3.29.26 PM
Все. Выполнение Задания 5 закончено. Код для Swift 1.2 и Xcode 6 (если вы еще не установили Xcode 7) находится на Github. Код для Swift 2.0 и Xcode 7 находится на Github. В посте представлен код для Swift 2.0 и Xcode 7.

Задание 5. Решение. Ощутите «магию» Swift на простейшей игре c Dynamic Animation (часть 2): 4 комментария

    • Wow! It’s your idea is brilliant, I only added some details. But I choose your code as the best one in Github. Thank you very much.

  1. Много интересных деталей и результат действительно впечатляет! Дебютная игра на Swift! Ура!! 🙂

    • Действительно Ура!! Это мое любимое Задание.

Обсуждение закрыто.