Stanford CS 193P iOS 7 2014 — Задание 4. Графическая игра Set (Objective-C)

Screen Shot 2015-11-09 at 3.50.37 PM

Содержание

Цель задания — создание более приближенных к реальности, улучшенных по внешнему виду версий Set Card и Playing Card игр “на совпадение”. В этом задании вы должны применить ваши новые познания по созданию пользовательских классов UIView и использованию UIDynamicAnimator. Вам придется более плотно познакомиться с механизмом Autolayout, чтобы обеспечить правильное вращение вашего пользовательского интерфейса (UI).

Текст Домашнего задания на английском языке доступен на  iTunes в пункте  “Developing iOS 7 app:Assignment 4″

На русском языке

Задание 4 Set fall 2013.pdf

 Задание выполнялось в Xcode 7 iOS 9. Режимы «Use Auto Layout» и «Use Size classes» в этом Домашнем задании включены.

Код Задания 4 находится на Github. Ниже представлен мой вариант решения Задания 4. У Вас может быть совсем другая логика построения этих игр.

Пункт 1

Требуется, чтобы приложение этой недели играло как в Set, так и в Playing Card карточные игры “на совпадение” (на отдельных закладках). Оно должно показывать счет и “пересдавать” карты, но вы можете убрать UI для показа  результатов последнего выбора, а также MVC History (MVC истории), добавленного на прошлой неделе.

С нашей storyboard убираем History View Controller, а также Navigation Controllers. Подсоединим заново наши игровые View Controllers напрямую к Tab Bar Controller. Наконец убираем метку, которая показывала результат выбора карты. На ее месте будет метка, показывающая сколько карт осталось в колоде, а в случае игры Set и дополнительную информацию о наличии Sets в наборе карт, представленных на игровом столе.
Screen Shot 2015-11-07 at 11.48.54 AM
По мере необходимости будем убирать лишний код. Мы сразу включаем режим «Use Size Classes» и «Use Auto Layout»

Screen Shot 2015-11-07 at 11.44.02 AM
И используем обычные приемы системы Autolayout для размещения меток, кнопок и нашего игрового поля padView.

Пункты 2, 5

2. Карты должны иметь “стандартный” вид (например, для игры Set они должны содержать 1, 2 или 3 волны, ромба или овала и быть либо зелеными, красными или фиолетовыми ; для Playing Card должна быть “лицевая” сторона карты со всеми привычными атрибутами. Вы должны все это нарисовать, используя UIBezierPath и Core Graphics функции. Вам не следует использовать изображения или строки с атрибутами для Set карт. Нарисованное на карте должно масштабироваться согласно свойству bounds карты. Вы можете взять PlayingCardView из демонстрационного примера для рисования игральной карты в Playing Card карточной игре “на совпадение”.

5. Используйте flip transition для анимации выбранной карты в Playing Card игре (чтобы получался “переворот” карты при ее выборе) .

… продолжаем очищать storyboard от UI элементов прежней игры: убираем все кнопки, а также соответствующее им свойство

[objc]

@property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *cardButtons;

[/objc]

и все ссылки на него в коде, потому что теперь карты будут рисоваться как пользовательские UIViews. Убираем все методы для модификации заголовков кнопок и фоновых  изображений на них.
На storyboard размещаем новое UIView для обоих игровых View Controllers и соединяем их с общим outlet padView в абстрактном класса СardGameViewController:
Screen Shot 2015-11-06 at 9.33.14 PM
… потому что эти новые UIViews будут содержать сетку карт. Они имеют соответствующий общий outlet padView, который расположен в базовом абстрактном класса CardGameViewController:

[js]
// CardGameViewController.m
@property (weak, nonatomic) IBOutlet PadView *padView;
[/js]

На каждом игровом View Controller для нового padView сразу же необходимо выставить ограничения системы Autolayout. Вот как может выглядеть эта система ограничений для padView в экранном фрагменте Playing Card :
Screen Shot 2015-11-07 at 12.12.32 PM
По возможности, как всегда, стараемся использовать как можно меньше «магических чисел».
Для запоминания карт в виде UIViews будем использовать новый массив cardsView . Этот массив используется lazy instantiation (отложенное получение экземпляра класса), о котором мы поговорим немного позже:

[js]
// CardGameViewController.m
@property (strong,nonatomic) NSMutableArray *cardsView; //of UIViews
[/js]

Для расчета размеров и позиций карт на  padView используется сетка — grid:

[js]
// CardGameViewController.m
@property (strong,nonatomic) Grid *grid;
[/js]

Она является экземпляром класса Grid, поставляемого Stanford, и для нее также выполняется lazy instantiation (отложенное получение экземпляра класса):

Screen Shot 2015-11-07 at 3.08.00 PM

Мы видим, что размер сетки grid.size эквивалентен размеру padView, а минимальное число ячеек сетки — количеству карт numberViews, которое при старте равно первоначальному количеству карт при «раздаче» startingCardCount, а по мере продвижения игры — количеству карт на игровом столе

Screen Shot 2015-11-07 at 3.39.11 PM

Для расчета соотношения сторон (aspect ratio) карт, необходимого для настройки сетки grid, в API базового класса CardGameViewController добавлено свойство maxCardSize, определяющее максимально возможный размер карты. Это свойство задается в дочерних классах:

[js]
// CardGameViewController.h

@property (nonatomic) CGSize maxCardSize;
………………….
// PlayingCardGameViewController.m

— (void)viewDidLoad
{

self.maxCardSize = CGSizeMake(80.0, 120.0);

}
// SetCardGameViewController.m
— (void)viewDidLoad
{

self.maxCardSize = CGSizeMake(120.0, 120.0);

}
[/js]

Количество карт, которое раздается при старте игры, startingCardCount, определяется с помощью абстрактного метода API базового класса , который затем переопределяется (override) в дочерних классах:

[js]
// CardGameViewController.h

— (NSUInteger) startingCardCount; //abstract

// CardGameViewController.m

— (NSUInteger) startingCardCount //abstract
{
return 0;
}

………………….
// PlayingCardGameViewController.m

— (NSUInteger) startingCardCount
{
return 24;
}

// SetCardGameViewController.m
— (NSUInteger) startingCardCount
{
return 12;
}
[/js]

Теперь мы можем вернуться к lazy instantiation массива карт cardsView и разместить их на padView с помощью сетки grid, но нам необходимо знать, какое количество карт мы должны размещать на padView, ведь мы в процессе игры можем добавлять карты и убирать их (только с экрана). Поэтому нам необходимо знать, сколько карт находится в игре, то есть сколько карт раздали, и проще всего спросить это у Модели игры CardMatchingGame

[js]
// CardMatchingGame.h
. . . . . . .
@property (nonatomic) NSUInteger cardsInPlay;
. . . . . . .
// CardMatchingGame.m
. . . . . . .
— (NSUInteger)cardsInPlay
{
return [self.cards count];
}
. . . . . . .
[/js]

Теперь давайте вернемся к отложенному получению cardsView. Мы организуем цикл по cardsInPlay и будем получать содержимое карты с помощью игры game, а размеры карты с помощью сетки grid,
Screen Shot 2015-11-07 at 8.24.16 PM
Изображение карты будет будет рисоваться на основе содержимого карты с помощью абстрактного метода:

[js]
// CardGameViewController.m
. . . . . . .
— (UIView *)cellViewForCard:(Card *)card inRect:(CGRect)rect //abstract
{
return nil;
}
. . . . . . .

[/js]

, который находится в API базового класса CardGameViewController и будет переопределен в дочерних классах.
Когда мы с помощью жеста tap посылаем индекс выбранной карты в Модель игры game, то после определения совпадения или несовпадения выбранных карт, статус карт может поменяться (карта может оказаться «выбранной»), и нам требуется обновление пользовательского интерфейса с помощью метода updateUI ( ):
Screen Shot 2015-11-07 at 8.37.32 PM
, а также удаление карт с экрана (если задан соответствующий режим) с помощью метода deleteCardFromGrid. Обновление пользовательского интерфейса выполняется с помощью метода updateUI ( ):
Screen Shot 2015-11-07 at 8.50.45 PM
Стержнем метода updateUI ( ) является вызов абстрактного метода, который рисует изображение карты в зависимости от содержимого карты и ее статуса (выбрана — isMatched == true, или не выбрана —  isMatched == false)

[js]
// CardGameViewController.h
. . . . . . .
— (void) updateCell:(UIView *)cell usingCard:(Card *)card animate:(BOOL)animate; //abstract
. . . . . . . . . .

// CardGameViewController.m
. . . . . . .
— (void) updateCell:(UIView *)cell usingCard:(Card *)card animate:(BOOL)animate // abstract
{

}
. . . . . . .
[/js]

Этот метод находится в API базового класса CardGameViewController и будет переопределен в дочерних классах.
Пора взглянуть на API базового класса CardGameViewController в целом:
Screen Shot 2015-11-07 at 9.27.04 PM
Практически все public свойства и абстрактные методы нам уже знакомы. Для двух последних методов требуется изображение как игральной карты Playing Card, так и Set Card для игры Set.
Добавим PlayingCardView класс из демонстрационного примера с лекции и используем его для реализации абстрактных методов по создание и модификации изображения игральной карты в дочернем классе PlayingCardGameViewController:
Screen Shot 2015-11-07 at 9.41.29 PM
. . . . . . . . . . .
Screen Shot 2015-11-07 at 9.47.47 PM
В последнем методе используется flip transition для анимации выбранной карты в Playing Card игре (чтобы получался “переворот” карты при ее выборе) .

Реализованы еще абстрактные методы создания колоды игральных карт и getter для количества первоначально сдаваемых карт startingCardCount — их будет 24 в нашей Playing Card игре:
Screen Shot 2015-11-07 at 9.53.28 PM
При загрузке Playing Card игры, устанавливаются свойства абстрактного класса CardGameViewController:
Screen Shot 2015-11-07 at 9.56.31 PM
Таким образом, у нас будет Playing Card игра с игральными картами максимального размера 80 х 120, что задает и соотношение сторон (aspect ratio) карты. После удаления «совпавших» карт не происходит автоматического добавления карт, хотя вы можете в любой момент это сделать с помощью кнопки «+3 карты». Исследуются две карты на предмет совпадения и начисления очков.
Дочерний класс SetCardViewController переопределяет (overrides) те же самые методы, но использует при этом SetCard класс и новый класс SetCardView класс, которого еще не существует, для изображения  Set карты:
Screen Shot 2015-11-07 at 10.06.53 PM
. . . . . . . . . . . . . . . . . . . . . .

Screen Shot 2015-11-07 at 10.09.35 PM
В последнем методе также используется flip transition для анимации выбранной карты в Set игре (чтобы получался “переворот” карты при ее выборе) .
Реализованы еще абстрактные методы создания колоды Set карт и задание количество первоначально сдаваемых карт startingCardCount — их будет 12:
Screen Shot 2015-11-07 at 10.12.51 PM
При загрузке Set игры, устанавливаются свойства абстрактного класса:
Screen Shot 2015-11-07 at 10.14.26 PM
Таким образом, у нас будет Set игра с квадратными картами максимального размера 120 х 120, что задает и соотношение сторон (aspect ratio) карты. После удаления «совпавших» карт происходит автоматическое добавление карт, но вы можете это сделать и сами в любой момент с помощью кнопки «+3 карты». Исследуются три карты на предмет совпадения и начисления очков.
API нового класса SetCardView обеспечивает доступ к свойствам Set карты:
Screen Shot 2015-11-07 at 10.42.47 PM
Как только любое из этих свойств устанавливается, UIView нуждается в перерисовке:
Screen Shot 2015-11-07 at 10.52.34 PM
Основные установки точно такие же, как и для изображения игральной карты PlayingCardView:
Screen Shot 2015-11-07 at 10.56.42 PM
Вначале рисуем прямоугольник с закругленными углами, регулируем его границы и выбираем цвет в зависимости от того, выбрана карта или нет, а затем рисуем символы игры Set:
Screen Shot 2015-11-08 at 12.30.42 PM
Рисуем «волну» (squiggle), ромб (diamond) или овал (oval) в зависимости от модели карты:
Screen Shot 2015-11-08 at 12.50.46 PM
«Волну» (squiggles) рисуем как комбинацию кубических и квадратичных кривых Безье:
Screen Shot 2015-11-08 at 6.37.43 PM
Screen Shot 2015-11-08 at 12.57.34 PM
«Ромб» (diamond) рисуем с помощью обычных линий:
Screen Shot 2015-11-08 at 6.50.24 PM
Screen Shot 2015-11-08 at 1.02.19 PM
«Овал» (oval) рисуем как комбинацию кубических и квадратичных кривых Безье:
Screen Shot 2015-11-08 at 6.58.10 PM

Screen Shot 2015-11-08 at 1.03.49 PM
После создания path c соответствующей фигурой, мы обращаемся к методу drawAttributesFor:, который анализирует другие свойства Set карты — color, rankshading,  — и выбирает для рисования карты нужный цвет, количество символов и способ заполнения замкнутой фигуры (не закрашен, закрашен полностью, закрашен полосками):
Screen Shot 2015-11-08 at 3.32.23 PM
Рисование двух или трех фигур выполняется с применением аффинного преобразования типа translate (перемещение). В методе drawPath: фигура в зависимости от свойства shading остается либо незакрашенной (только обходится линией определенного цвета и толщины — метод stroke), либо закрашивается полностью (методы stroke + fill ), либо закрашивается полосками (методы stroke + fill + stripes ):
Screen Shot 2015-11-08 at 4.04.10 PM
Самым тяжелым элементом этого Задания оказалось закрашивание фигуры полосками с помощью Core Graphics. В подсказках к Заданию особо оговаривается, что третий тип “закрашивания” Set карт — это заполнение “полосками”, а не “затенение”.
На настоящий момент мне известно три варианта закрашивания полосками:

  1. Рисование горизонтальных или вертикальных линий в пределах фигуры.
  2. Закрашивание фигуры с использование полосатого цветного паттерна CGContextSetFillPattern(context, pattern, &alpha);
  3. С использованием пунктирной линии, ширина которой во всю высоту символа CGContextSetLineDash(context, 0.0, dashes, 2);

Самый естественный и быстродействующий, но и самый запутанный из них — это 2- ой способ с использованием паттерна, самый тривиальный и затратный — это первый способ. Но самый остроумный и легкий — способ с пунктирной линией (одной линией в отличие от первого способа, когда рисуется множество линий). Я выбрала 2-ой способ с паттерном, потому что мне хотелось освоить эту технологию.
Screen Shot 2015-11-08 at 6.29.29 PM
Я приведу код и для остальных способов, но он не подставляется напрямую в мое приложения, так как эти способы взяты из других приложений. Я привожу здесь этот код с целью дать вам информацию к реализации вашего способа рисования полосок.
Вот остроумный способ рисования полосок с помощью одной пунктирной линии:
Screen Shot 2015-11-08 at 7.10.39 PM
Вот первый способ рисования полосок с помощью обычных вертикальных или горизонтальных линий, то есть обычная штриховка:
Screen Shot 2015-11-08 at 7.14.56 PM
Screen Shot 2015-11-08 at 7.16.31 PM

Пункты 3, 6

3. Когда произошло “совпадение” в игре Set, “совпавшие” карты должны быть убраны из игры (не просто подсвечены серым или сделаны пустыми, а должны быть полностью убраны с UI).

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

Возвращаемся в наш базовый абстрактный класс CardGameViewController. Удаление «совпавших» карт с экрана будем выполнять с анимацией, но воспользуемся подсказкой № 7:

“Совпадения” сейчас приводят к устранению карт. Будет намного проще при реализации, если вы примите архитектурное решение, при котором “устранение” карт будет носить чисто “визуальный” эффект, а не часть самой игры. Другими словами мы настойчиво рекомендуем не иметь API в Model по реальному удалению карт. “Устранять” карты с экрана проще путем “не показывания” isMatched карт (но при этом выполнять Обязательное задание 6: вы не можете оставлять “дыры” в расположении карт на экране).

Поэтому удаление карт с экрана будет происходить с некоторой задержкой, чтобы пользователь смог увидеть «совпавшие» карты. Затем изображения «совпавших» карт будут просто скрываться с помощью UIView свойства hidden:
Screen Shot 2015-11-08 at 7.55.20 PM
После «скрытия» карт мы должны посчитать число карт, оставшихся на столе с помощью метода cardsOnTable, который показывает невыбранные (!isMatched) карты в игре.  Этот метод расширяет API класса CardMatchingGame Модели игры «на совпадение»:

[js]
// CardMatchingGame.h
. . . . . . .
@property (nonatomic,strong) NSArray *cardsOnTable; // of Card not matched
. . . . . . . . . .

// CardMatchingGame.m
. . . . . . .
-(NSArray *)cardsOnTable
{
NSMutableArray *remCards =[[NSMutableArray alloc] init];
for (Card *card in self.cards) {
if (!card.isMatched) {
[remCards addObject:card];
}
}
return [remCards copy];
}
. . . . . . .
[/js]

Изменившееся число карт на игровом столе должно подготовить сетку grid к показу меньшего числа карт:

[js]
self.grid.minimumNumberOfCells =[[self.game cardsOnTable] count];
[/js]

а затем и передвинуть оставшиеся на игровом столе карты, чтобы не осталось пустых мест:

[js]
[self reDrawViewsWithAnimationForView:self.padView];
[/js]

При перерисовке карт на игровом столе, учитывается свойство . hidden изображений карт cardsView:
Screen Shot 2015-11-09 at 9.44.09 PM

Пункт 4

Как и в реальной игре Set, пользователь должен стартовать с 12 карт, и затем иметь возможность запросить 3 дополнительные карты в любой момент времени, если он или она поймет, что не удается выбрать Set. Сообщите пользователю, если при запросе карт в колоде не окажется.

Добавляем новую кнопку на storyboard для добавления 3-х карт на игровой стол для Playing Card игры:
Screen Shot 2015-11-08 at 9.08.07 PM
Добавляем новую кнопку на storyboard для добавления 3-х карт на игровой стол для Set Card игры:
Screen Shot 2015-11-08 at 9.08.19 PM
С помощью CTRL— перетягивания получаем Action addCards в базовом абстрактном классе CardGameViewController:
Screen Shot 2015-11-08 at 9.17.00 PM
Вставляемые карты будут анимировать:
Screen Shot 2015-11-08 at 9.23.40 PM
В API базовом абстрактном классе CardGameViewController есть свойство addCardsAfterDelete:

[js]
@property (nonatomic)BOOL addCardsAfterDelete;
[/js]

Если установить это свойство в YES, то карты будут добавляться в игру автоматически после удаления с экрана «совпавших». Именно этот режим установлен для игры Set.
При добавлении карт анализируется число оставшихся в колоде карт, и, если в колоде карт недостаточно, то пользователь получает уведомление:
Screen Shot 2015-11-09 at 9.56.51 PM
а на экране мы видим следующее:

Screen Shot 2015-11-09 at 10.04.22 PM

Пункт 7

Анимируйте “пересдачу” карт.

Анимируем перемещение старых карт в левый нижний угол и раздачу новых карт:
Screen Shot 2015-11-08 at 9.50.38 PM
Screen Shot 2015-11-08 at 9.55.32 PM

Пункт 8, 9

8. Игра должна работать правильно (и выглядеть хорошо) как в ландшафтном режиме, так и в портретном режиме,  как на iPhone 4, так и на iPhone 5. Используйте механизм Autolayout насколько возможно, чтобы выполнить эту работу. Самостоятельное позиционирование карт потребует некоторой дополнительной работы (хотя вы возможно уже делали эту работу для некоторых Обязательных заданий ранних домашних работ). Код не должен настраиваться на определенную высоту или ширину экрана или его ориентацию (например, не должно быть кода типа “if landscape then” или “if width/height… then”). Суть этого задания в том, чтобы View вашего MVC выглядело хорошо в bounds (границах) любого разумного по размеру прямоугольника.

9. Перемещение элементов внутри вашего пользовательского интерфейса (UI), которое проявляется в ответ на вращение должно анимироваться (Autolayout уже анимирует все изменения, которые оно делает для вас, но другие изменения расположения элементов, которые вы делаете в коде, вы должны анимировать сами).

Так как мы задали все ограничения системы Autolayout при размещении меток, кнопок и нашего padView, то они будут автоматически анимировать при переходе из портретного режима в ландшафтный и наоборот. Это также поможет получить правильный интерфейс для iOS приборов различных размеров: iPhone 5s, iPhone 6s, iPad Air и т.д.. Но сетку grid нам придется подстраивать под размер padView и перерисовывать карты при вращении прибора. Для этого используем метод viewDidLayoutSubviews «жизненного цикла» View Controller:
Screen Shot 2015-11-08 at 10.14.33 PM
В результате вращения прибора игральные карты будут перестраиваться :
Screen Shot 2015-11-08 at 10.18.47 PM
В результате вращения прибора карты Set будут перестраиваться:
Screen Shot 2015-11-08 at 10.25.31 PM

Пункт 10

Используйте UIDynamicAnimator для того, чтобы в любой игре позволить картам собираться вместе в “кучку” по pinch жесту. Как только они собрались, эта “кучка” должна быть способна целиком двигаться по экрану (отслеживая жест pan). Если мы тапнем (жест tap) по “кучке”, то карты должны вернуться “невредимыми” на свои нормальные позиции, и при этом естественно анимировать.

Начинаем с добавления жестов pan и pinch к области padView, в которой располагается сетка с картами, в методе «жизненного цикла» в базовом абстрактном классе CardGameViewController:
// CardGameViewController.m
Screen Shot 2015-11-09 at 12.51.22 PM
Обработчик жестов pinch и pan размещаются в классе PadView. Кроме них в API класса присутствует логическая переменная pinchedViews, которая показывает, находятся ли карты на рабочем столе ввиде «кучки», собранной жестом pinch:
// PadView.h
Screen Shot 2015-11-09 at 1.35.48 PM
В области padView действует динамический аниматор animator, и определен массив объектов, обладающих поведением Attachment (подсоединение):
// PadView.m
Screen Shot 2015-11-09 at 1.42.24 PM
Есть специальный метод attachCardsViewToPoint, который наделяет все subviews, находящиеся в области padView, поведением Attachment (подсоединение):
// PadView.m
Screen Shot 2015-11-09 at 1.49.07 PM
Жест pinch управляет подсоединением всех  subviews, находящихся в области padView, к поведению Attachment, и регулирует длину «поводка» поведения Attachment. Кроме того, выставляется булевская переменная pinchedViews, говорящая о том, что subviews собраны в «кучку»:
// PadView.m
Screen Shot 2015-11-09 at 1.57.07 PM
Screen Shot 2015-11-09 at 2.25.44 PM
Жест pan будет работать только в том случае, если  subviews собраны в «кучку» и булевская переменная pinchedViews равна YES. Этот жест будет подстраивать «якорную точку» subviews к координатам жеста pan. Как только жест pan закончится, все  subviews освобождаются от поведения Attachment:
Screen Shot 2015-11-09 at 2.09.10 PM
Screen Shot 2015-11-09 at 2.28.28 PM
«Кучка» карт «рассыпается по жесту tap, но жест tap у нас уже задействован для выбора карты на игровом столе. Он размещен на игровых View Controllers на storyboard:
Screen Shot 2015-11-09 at 2.36.17 PM
Обрабатывает этот жест Action flipCard и, если карты собраны в «кучку», то сработает метод, восстанавливающий местоположение карт на игральном столе и «гасящий» режим «кучки».
Screen Shot 2015-11-09 at 2.46.14 PM

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

Зная как найти Sets в оставшихся картах, вы могли бы разрешить пользователю “мошенничать”. Имейте кнопку, которая показывала бы Set (если он существует). Вы сами должны решить как его показывать, но может быть маленький индикатор (“звездочка” или что-то еще) на всех 3-х картах?

Добавляем новую кнопку на storyboard для игры Set, которая будет давать подсказки относительно  Sets в этом наборе карт
Screen Shot 2015-11-09 at 2.53.48 PM
Если вы нажимаете ее первый раз, то вам дается «мягкая» подсказка о том, сколько Sets содержится в наборе карт на игральном столе. Если вы хотите, чтобы вам точно указали , какие карты составляют Set, то продолжайте нажимать кнопку подсказки ??, и она вам последовательно покажет все Sets, а потом вернется в первоначальное состояние :
Screen Shot 2015-11-09 at 3.41.02 PM
С помощью кнопки ?? вам циклически покажут все Sets.
Screen Shot 2015-11-09 at 3.45.20 PM