Главная > Творчество > +(AppStore *) История про коня

+(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. На первое время, думаю, хватит.

  1. 19 November 2010 в 16:33 | #1

    Полезный пост. Пиши еще! :) и ссылки на опубликованные игры в айтюнс хорошо бы :)

  2. 19 November 2010 в 23:26 | #2

    спасибо, согласен. добавил в пост ссылку.

Подписаться на комментарии по RSS