+(AppStore *) История про коня
Давненько не обновлял блог, нужно бы что-то написать. На самом деле, материала опять скопилось море, не понимаю, как другие блоггеры все успевают. Это ж нужно только сидеть и писать. Ладно, упустим уже лирику. В данный момент я почти заканчиваю свое третье приложение для iPhone, о нем я расскажу в ближайших постах, а пока я решил чиркнуть пару строк о своих первых двух приложениях, и вот для чего. Дело в том, что мой сайт еще пока не реализован в той степени, в которой я его задумывал. Не хватает как минимум двух разделов. Один из них — Products. По-русски пока не придумал, как назвать, но суть, думаю ясна. Буду туда выкладывать результаты своего творчества (вот, кстати, неплохое название — творчество). Сейчас я как раз доделываю серверную функциональность, и в связи с этим нужно будет что-то уже выложить. Пока из готового у меня есть два iPhone-приложения, ну и ранние разработки для других платформ (Windows, Mac OS X). Я долго размышлял на тему, как это все будет выглядеть, изучал примеры своих коллег, и решил следующее. На сайте будет раздел Products, он будет содержать список моих приложений, с кратким описанием, ссылкой на страницу самого приложения, ссылку на обсуждение, ссылку на страницу скачивания. В случае с приложениями ссылка на скачивание будет вести на страницу в iTunes — это я подсмотрел у других разработчиков — распространенная практика. Страница на обсуждение — URL поста в блоге. Каждому приложению будет соответствовать один пост, в котором я буду рассказывать, что интересного я узнал в ходе программирования данного продукта, какие хитрости он содержит и т.д. Плюс — данный подход позволит получить некий feedback через комменты, так как городить для этого отдельную функциональность на сайте как-то лениво. Даже создам по такому случаю новую категорию Products.
И так, поскольку неплохо бы иметь страницу обсуждения для приложения, собственно этот пост будет о моем первом приложении — Knight Move. Вот как оно в итоге получилось:
[nggallery id=6]
и вот как это выглядит в AppStore
На русский язык название можно перевести как Ход конем. Это логическая головоломка, реализующая математическую задачку Конь Эйлера, которая заключается в том, что нужно обойти прямоугольное поле, делая ходы шахматным конем. Эту развлекаловку я знаю с детства. Тогда мы рисовали в тетрадках в клеточку поля, причем только 10х10 (о том, что играть можно на полях других размерностей, я узнал уже позже). Это меня и натолкнуло на мысль оформить игру в виде тетради с рукописными элементами. Мне эта идея показалась оригинальной, но как выяснилось позже — я не один такой умный в AppStore, и явление это весьма распространенное)) Вообще, когда я изучаю новую платформу программирования, я предпочитаю в качестве разминки писать игру Сапер — его реализация позволяет отработать основные приемы разработки GUI (отрисовка, обработка событий и др.). Однако, как делать сапера под айфон я концептуально не смог представить (хотя видел, что кто-то таки написал его под айфон). Поэтому решил писать коня. Функциональность решил немного расширить — сделать выбор размерности поля, таблицу рекордов и режим обучения (отличается от обычного тем, что показываются подсказки). Очень долго мучался, как сделать разумнее выбор размерности. Дело в том, что на полях не всех размерностей возможен обход. Во-первых, размер поля должен быть не меньще 4, во-вторых если одна сторона равна четырем, то вторая может быть не меньше семи, и т.д. (полные правила внутри игры). И все это не укладывалось у меня в голове в плане пользовательского интерфейса. В итоге, решил разнести размерность сторон в две колонки и при выборе в левой колонке (width) в правой колонке перестраивается список height на основе доступных значений для выбранного width.
В своем первом приложении мне довелось узнать, что-же представляет из себя типичное iOS приолжение. Собственно, каркас приложения остается таким же как в одном из моих предыдущих постов
Есть класс-делегат приложения, в котором я инициализирую внутренние контроллеры (ViewController), а также обрабатываю события операционной системы (напр. applictionDidFinishLaunching). ViewController — класс контроллер представления (View) в цепочке Responders он стоит перед делегатом приложения и также получает события системы. Кроме того, он производит какие-то действия с объектом-представления, к которому привязан. Представление (View) — класс для отображения информации. По идее, в этом классе должен находиться код исключительно для отображения данных. Собственно, в этом и заключается концепция MVC. Все графические элементы в iOS являются наследниками UIView. UIView может содержать внутри себя коллекцию других UIView. Добавить новый элемент в UIView можно с помощью метода addSubview, убрать — removeFromSuperview. Причем, находиться конкретный объект в один момент времени может только в одном контейнере UIView, поэтому, когда мы вызваем addSubview и передаем этому методу какой-то UIView, то, у этого объекта сначала вызывается removeFromSuperview, а затем он уже добавляется в тот UIView, в котором вы вызывали addSubview. Эта схема очень практична и позволяет реализовать 99% всех потребностей пользовательского интерфейса.
И так, в моем приложении 5 страниц (Страница с игрой, Страница с главным меню, Справка, Настройки, Таблица рекордов), соответственно имеем 5 контроллеров и 5 же представлений. А также сопуствующие классы: класс, реализущий логику игры, класс для работы с настройками, класс для работы со звуками. У приложения есть главное окно — объект класса UIWindow. Он загружается при инициализации приложения из nib-файла. Добавляя и убирая из него объекты UIView, представляющие собой страницы, мы реализуем классическую схему работы с UIWindow, которая по концепции очень похожа на Mac OS X.
Класс с логикой игры рассматривать пожалуй не интересно, он представляет собой черный ящик для всего приложения, который на входе имеет, образно говоря, два метода: initGame (инициализирует игру в соответствии с пользовательскими предпочтениями), и processHit(x, y), где x, y экранные координаты нажатия — переводит экранные координаты в координаты поля и возвращает какой-то ответ, либо ход не возможен, либо возможен, либо игра окончена (ходить больше некуда).
Класс для работы со звуком в принципе полезная вещь, даже выложу его сюда, может кому пригодится. Нашел в интернете и немного отрихтовал. Представляет собой удобную надстройку над системными функциями по работе со звуком. Пример: есть у нас звуковой файл youwin.wav, и мы хотим его использовать:
//initializing #import <AudioToolbox/AudioToolbox.h>; #import "SoundEffect.h" ... youwinSound = createSysSnd(@"youwin"); //using -(void) playYouwinsound { [youwinSound play]; }
Не забываем подключить к проекту необходимый фреймворк — по дефолту его нет.
Ожидаемым, но приятным открытием стало то, что в iOS для пользовательских настроек существует тот же механизм, что и в Mac OS X — класс NSUserDefaults.
В реализации классов-наследников UIView отработал несколько стандартных iOS-приемов. Ну, во-первых, конечно же рисование на контексте. В большинстве случаев все сводится к тому, чтобы отрисовать объект класса UIImage на текущем графическом контексте. Вот, к примеру, в коне была задачка, отрисовывать произвольные числа. Причем, чтобы выдержать дизайн, это должны быть «рукописные» числа. Кстати, все элементы игры я отрисовывал в тетрадке в клетку, а затем пропускал через сканер. Таким же образом были отсканированы и цифры. В проекте цифры лежат в виде одной строки 0123456789 в png-файле. Для удобства я разбил картинку на 10 элементов, и запихал их в NSMutableArray. Кстати, тоже классическая задачка (разбить сет или кадры анимации в серию отдельных UIImage). Делается это примерно так:
- (NSMutableArray*) getDigitsArray { UIImage * dgt = [UIImage imageNamed:@"digits.png"]; int d_wdth[] = {0, 9, 16, 25, 34, 43, 52, 62, 70, 79, 89, 98, 106, 9 + 106, 16 + 106, 25 + 106, 34 + 106, 43 + 106, 52 + 106, 62 + 106, 70 + 106, 79 + 106, 89 + 106, 98 + 106, 106 + 106}; NSMutableArray* array = [NSMutableArray arrayWithCapacity:((sizeof(d_wdth) / sizeof(int)) - 1)]; array = [NSMutableArray arrayWithCapacity:((sizeof(d_wdth) / sizeof(int)) - 1)]; for (int x = 0; x < (sizeof(d_wdth) / sizeof(int)) - 1; x++) { CGRect rect = CGRectMake(d_wdth[x], 0, d_wdth[x + 1] - d_wdth[x], dgt.size.height); CGImageRef tileImageRef = CGImageCreateWithImageInRect(dgt.CGImage, rect); [array addObject:[UIImage imageWithCGImage:tileImageRef]]; CGImageRelease(tileImageRef); } return array; }
В моем случае шрифт получился не моноширинный, отсюда и наличие d_wdth. В большинстве же случаев ширина и высота элементов одинакова и задачка несколько упрощается.
И так, имеем NSMutableArray с арабскими цифрами. Нужно отрисовать трехзначный таймер:
-(void) drawCounter :(int) stepIndex{ UIGraphicsBeginImageContext(counter.frame.size); UIImage * d[9]; int numWidth = 0; for (int i = 0; i < [moveLogic dgtCnt:stepIndex]; i++) { d[i] = [digits objectAtIndex: [moveLogic dgtAtIdx:stepIndex :i]]; numWidth += d[i].size.width; } CGPoint scrPoint = CGPointMake(0, 15); d[[moveLogic dgtCnt:stepIndex]] = [digits objectAtIndex: 10]; numWidth += d[[moveLogic dgtCnt:stepIndex]].size.width; for (int i = 0; i < moveLogic.totalStepDigitsCount; i++) { d[i + 1 + [moveLogic dgtCnt:stepIndex]] = [digits objectAtIndex: [moveLogic totalStepDigitAtIndex:i]]; numWidth += d[i + 1 + [moveLogic dgtCnt:stepIndex]].size.width; } for (int i = 0; i < [moveLogic dgtCnt:stepIndex] + 1 + moveLogic.totalStepDigitsCount; i++) { float offset = 0; for (int j = 0; j < i; j++) offset += d[j].size.width; [d[i] drawAtPoint:CGPointMake(offset + (counter.frame.size.width - numWidth), scrPoint.y + (CELL_SIZE - d[i].size.height) / 2 )]; } counter.image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); }
Здесь counter — объект класса UIImageView, который содержится во View игровой страницы. На нем и рисуем счетчик. Функция UIGraphicsBeginImageContext создает контекст для рисования. Далее, все функции рисования будут рисовать на созданном контексте. Например, метод drawAtPoint класса UIImage отрисует изображение, которое содержит в координатах x, y переданных ему через параметры. Ну, думаю, тут все ясно. counter.image = UIGraphicsGetImageFromCurrentImageContext(); присваивает объект UIImage, который мы нарисовали свойству image объекта counter, что приводит к изменению его внешнего вида. В этом месте стоит упомянуть об управлении памятью. Как известно, в iOS < 4 отсутствовало такое понятие, как сборщик мусора. За объектами в памяти нужно следить самостоятельно. Это делается через управление ссылками. Если мы создали какой-то объект в памяти, мы должны вызвать у него метод retain, это увеличит некоторую внутреннюю переменную на единицу. Метод release уменьшает эту переменную на единицу. Когда значение достигает нуля, объект убирается из памяти. В случае с системными функциями типа UIGraphicsBeginImageContext все немного по-другому. Мне было сначала непонятно, что происходит со старым объектом, когда мы присваиваем свойству counter.image новый указатель? Оказалось, что в случае с системными функциями это происходит на автомате и нам за этим следить не надо.
Что еще интересного я узнал, разрабатывая коня. Уяснил для себя такие понятия, как transitions и animation. С помощью этих сущностей в iPhone реализованы всякие красивости и эффекты — повороты, перемещения, затемнения и т.д. Вот, к примеру, захотелось мне сделать, чтобы синий указатель текущего хода вокруг клетки не просто мгновенно оказывался в новой клетке, а плавно туда перемещался. Я уже хотел было это реализовывать через таймер, вычислять траекторию, перемещать объект, но все оказалось проще и приятнее. Вот код:
-(void) moveCurrent { CGPoint scrPoint = [self getScreenCoord: [moveLogic getCurrentStepCoord]]; if (current.hidden) { current.frame= CGRectMake(scrPoint.x - 1, scrPoint.y - 1, 32, 32); current.hidden = NO; } else { [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.2]; current.transform = CGAffineTransformMakeTranslation(scrPoint.x - current.frame.origin.x, scrPoint.y - current.frame.origin.y); current.frame= CGRectMake(scrPoint.x - 1, scrPoint.y - 1, 32, 32); [UIView commitAnimations]; } lastStepCoords = scrPoint; }
У класса UIView есть набор статических методов для анимации. Стандартный цикл анимации состоит из вызова beginAnimations, присваивания свойству transform всех потомков UIView, которых мы хотим анимировать за один раз, каких-либо объектов, генерируемых функциями CGAffineTransformXXX,и вызова commitAnimations, которая собственно запускает все назначенные трансформации. Метод setAnimationDuration задает интервал в секундах, за который нужно выполнить анимацию. Кстати, таймер в iOS также представлен классом NSTimer как и в Mac OS X. Работать с ним так же легко и приятно:
moveTimer = [NSTimer scheduledTimerWithTimeInterval: 0.1 target:self selector:@selector(timerEvent:) userInfo:nil repeats: YES]; ... -(void) timerEvent: (NSTimer *) theTimer { //do stuff }
Из красивостей анимации использовал очень кстати подвернувшийся эффект переворачивания страницы, когда пользователь переходит со страницы на страницу:
-(void) switchView :(int) from :(int) to { currentView = to; if (from != to) { [UIView beginAnimations:@"View Curl" context:nil]; [UIView setAnimationDuration:1.0]; [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; [UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:views[from] cache:YES]; [UIView commitAnimations]; [window addSubview:views[from]]; [views[to] retain]; [views[to] removeFromSuperview]; } }
Сделал метод унифицированным, чтобы не дублировать код.
Ну пожалуй и все на сегодня. На последок еще скажу пару слов о звуках, ведь это очень важный аспект в game-development’е. Думаю, у всех начинающих игрописателей возникает вопрос, где взять озвучку для своих игр? Ответ очевиден — гугл)) У меня ушло наверное полдня чтобы собрать коллекцию игровых звуков мегабайт под 200. На первое время, думаю, хватит.
Полезный пост. Пиши еще! :) и ссылки на опубликованные игры в айтюнс хорошо бы :)
спасибо, согласен. добавил в пост ссылку.