В этом посте представлено продолжение решения обязательных и дополнительных пунктов Задания 5. Первую часть можно посмотреть здесь.
Текст Домашнего задания на английском языке доступен на 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.
Я, как и в первой части, делаю акцент на том, как правильное использовать такие конструкций 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
И опять, «ракетка», также, как и аниматор, впервые инициализируются при установке в Наблюдателе Свойства didSet{} моего outlet breakoutView! в BreakoutViewController
“Ракетка” поддерживает жест pan, c помощью которого осуществляется только горизонтальное перемещение «ракетки». Поэтому на наш BreakoutViewController добавлен жест pan и его обработка
Реально перемещение «ракетки» paddle осуществляется в «родном» для нее классе BreakoutView.
Именно в этом методе «ракетка» добавляется к аниматору свою границу.
Мы можем в любой момент времени переустановить «ракетку» принудительно, например, в начале игры или при выходе за границы игрового поля.
И опять, мы добавляется «ракетку» к аниматору в виде границы.
Пункты 1, 2, 3,4 обязательные. «Кирпичи»
«Кирпичи», также как и «ракетка», не являются объектами, управляемыми динамическим аниматором animator, они не добавляются ни к какому «поведению». Поэтому «жизнь» «кирпичей»,
также, как и «ракетки», в основном протекает в игровом поле, то есть в классе BreakoutView. Однако аниматор постоянно «чувствует» их как границы для столкновений с «мячиками», поэтому любое появление или перемещение «кирпичей» в классе BreakoutView должно сопровождаться изменением их границ в аниматоре.
«Кирпичи» представлены словарем, в котором ключом является порядковый номер «кирпича» в конфигурации уровня игры level = [[Int]], задающего расположение кирпичиков на экране в виде двухмерного массива целых чисел
Порядковый номер необходим для идентификации «кирпича» как идентификатор границы в аниматоре. Значением в словаре является сам «кирпич» BrickView, то есть по существу UIView. Появление и удаление «кирпича» связано с появлением и удалением границ в поведении behavior, поэтому изменение положения «кирпича» должно быть строго синхронизировано с изменением границ для аниматора. Расположение «кирпичей» на экране и их размеры на экране определяются при инициализации уровня игры level. Вы видите, что как только устанавливается новое значение level, так сейчас же выполняется метод Наблюдателя didSet{} свойства level.
В отличие от «ракетки», границы-«кирпичи» обрабатываются методами делегата UICollisionBehaviorDelegate класса UICollisionBehavior. С точки зрения логики игры Brealout нас очень интересуют столкновения «мячика» с «кирпичом».
Коллайдер collider фиксирует удар по «кирпичу». В этом случае мы должны увеличить счет игры и визуально показать исчезновение «кирпича», который получил удар, и, если это последний кирпич, то объявить, что игра закончена победой. Это решение принимается в главной «штаб-квартире» — в нашем основном View Controller — BreakoutViewController. Для организации взаимодействия коллайдера collider со «штаб-квартирой» BreakoutViewController организует собственный протокол
В нем два метода:
- метод ballHitBrick (…) позволяет среагировать на ударение «мячика» с «кирпичом»
- метод ballLeftPlayingField (…) позволяет среагировать на выход «ударяющего мячика» за пределы игрового поля
Один из этих методов, ballHitBrick (…), используется коллайдером collider при анализе столкновений
Вот как этот метод реализован в «штаб-квартире» BreakoutViewController, который подтвердил наш протокол BreakoutCollisionBehaviorDelegate
Но реально «кирпич» удаляется с анимацией в своем «родном» классе BreakoutView.
И опять, первое, что мы делаем, — удаляем границу, связанную с «побитым» «кирпичом».
В классе 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),
- включение управления ракеткой наклоном прибора (Bool)
- максимальное число «мячиков» в игре (Int)
- коэффициент увеличения скорости «мячика» (0.0 — 1.0)
Создадим класс SettingsTableViewController для нашего нового View Controller с установками и не забудем его подключить к новому View Controller на storyboard. Для каждого регулируемого элемента (метки ( labels), переключатели (switches), ползунки (sliders) и т.д.) создадим outlets
и Actions
Мы видим, что Actions связаны с экземпляром класса Settings
Класс Settings содержит вычисляемые переменные, взаимодействующими с хранилищем NSUserDefaults
Все вычисляемые переменные устроены одинаково: get{} извлекает данные из хранилища NSUserDefaults, а set{} их туда записывает. Причем обратите внимание, что запись идет в соответствии с типом записываемого значения:
userDefaults.setInteger(newValue, forKey: Keys.MaxBalls)
. . . . .
userDefaults.setFloat(newValue, forKey: Keys.BallSpeedModifier)
а при извлечении данных из NSUserDefaults всегда используется objectForKey(…), но с последующим «кастингом» as?. Это дает возможность подключить значения, заданные по умолчанию Defaults.MaxBalls, Defaults.Level и т.д. с помощью оператора ??.
return userDefaults.objectForKey(Keys.MaxBalls) as? Int ?? Defaults.MaxBalls
Получился фантастически простой и очень конструктивный класс работы с NSUserDefaults.
Возвращаемся к классу SettingsTableViewController и организуем извлечение данных из NSUserDefaults в методе «жизненного цикла» viewWillAppear класса SettingsTableViewController.
Все. Наш MVC Settings для установок готов.
Объединяем MVC игры Breakout с MVC Settings с помощью Tab Bar Controller.
Осталось добавить считывание параметров игры из NSUserDefaults в нашей «штаб-квартире» BreakoutViewController, и мы будем это делать с помощью специального метода loadSettings, вызов которого поместим в методе «жизненного цикла» viewWillAppear класса BreakoutViewController.
Все. Обязательные пункты выполнены. И круг замкнулся: установки автоматически читаются из NSUserDefaults или становятся равными значениям по умолчанию, «кирпичи» размещаются на экране, «ракетка» нужной ширины также находится на экране. Выполняем жест tap, «мячик» полетел вверх, к «кирпичам» и игра началась. Действительно все происходит магически.
Дополнительный пункт 6 .
Интегрируйте акселерометр куда-нибудь в ваше приложение (может быть реальное гравитационное воздействие на полет прыгающего мячика?).
Я решила интегрировать акселерометр в управление «ракеткой», то есть наклон прибора будет передвигать «ракетку» в сторону наклона.
Первое, что нам необходимо — это менеджер движения, то есть экземпляр класса СMMotionManager. Я говорил вам, что это должна быть глобальная вещь для моего приложения. Я размещу менеджера движения в глобальном месте, которым является мой файл AppDelegate. Я удаляю почти весь текст в AppDelegate и размещаю там структуру, в которой создаю экземпляр менеджера движения Manager.
Затем в специальном методе loadSettings, с которым мы уже знакомы и вызов которого размещен в методе «жизненного цикла» viewWillAppear класса BreakoutViewController, я проверю, а доступен ли акселерометр, и если акселерометр доступен, то начнем обновлять данные с акселерометра с помощью метода startAccelerometerUpdatesToQueue.
Я должна определить очередь, в которой я хочу, чтобы происходило обновление данных и задаю main queue с помощью NSOperationQueue.mainQueue().
Далее я пользуюсь способностью Swift выносить последний аргумент за круглые скобки, если этот аргумент — замыкание. Я переименую аргументы замыкания в data и error. Аргумент data — это данные акселерометра. Что касается error, то я хочу игнорировать эту ошибку. В моем случае, если я получу ошибку, то я не буду использовать данные с акселерометра для перемещения «ракетки».
Теперь внутри замыкания я должна задать движение «ракетки» на основании показаний акселерометра, которыми являются data и которые измеряются в g. Я устанавливаю данные с акселерометра только по оси x с некоторым коэффициентом Const.maxPaddleSpeed.
Мы начали получать данные с акселерометра, но мы должны остановить их получение в методе “жизненного цикла” viewWillDisappear.
Все. Выполнение Задания 5 закончено. Код для Swift 1.2 и Xcode 6 (если вы еще не установили Xcode 7) находится на Github. Код для Swift 2.0 и Xcode 7 находится на Github. В посте представлен код для Swift 2.0 и Xcode 7.
Great tutorial, brilliant code.
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.
Много интересных деталей и результат действительно впечатляет! Дебютная игра на Swift! Ура!! 🙂
Действительно Ура!! Это мое любимое Задание.