|
Май
21
|
Почему бы не представить в магазине приложений свой собственный пазл — как это сделали мы! В этом уроке я поэтапно расскажу о создании такого приложения. Итоговый результат будет выглядеть примерно так, как на фото. Чашку с кофе — и можно приступать.

Как настоящие программисты, для начала остановимся на том, что такое slider puzzle и как его реализовать. Наверное, все помнят детскую игру “пятнашки”, где фишки с цифрами нужно было выстроить по порядку. В нашем случае это будут разрозненные фрагменты изображения, которые собираются в единое целое (их на один меньше, чтобы кусочки можно было перемещать). Теперь подумаем, что понадобится, чтобы воплотить такой проект в жизнь.
Для начала потребуется изображение, которое мы разделим на фрагменты. Разместим их в беспорядке, чтобы после снова собрать. Правда перед этим нужно как-то запомнить, где должен находиться тот или иной фрагмент. Для этого введем новый класс, который будет содержать как оригинальное, так и текущее положение каждого фрагмента в матрице (под матрицей понимается сетка, на которой формируется рисунок). Так мы сможем определить, собрал пользователь пазл или нет (сравнив для каждого фрагмента текущее положение с исходным). Следующая задача — определить разрешенные перемещения. Для этой цели заменим один из фрагментов пустым. На его место разрешается передвинуть соседний фрагмент. Ну вот, в принципе, и все. Если я что-то упустил, разберемся по ходу дела.
Итак, перечислим все, что необходимо сделать:
- разбить изображение;
- привязать каждую часть изображения к определенному фрагменту пазла (отвечающему за хранение его исходной и текущей позиции);
- перемешать беспорядочно все фрагменты (запускаем n-ный цикл, во время которого случайно выбранный фрагмент перемещается на место пустого);
- фиксируем касание пользователем фрагментов пазла; если перемещение разрешено, меняем местами пустой фрагмент с выбранным и проверяем, вернулось ли изображение к исходному состоянию.
Начнем? Откройте XCode и создайте приложение windows based. (Здесь я буду останавливаться в основном на логике. Детали по настройкам можно получить, загрузив исходный код либо обратившись к предыдущим урокам).
Как обычно, нам понадобится новый контроллер “UIViewController“. Создайте его и присвойте соответствующее имя. Теперь отыщите среди своих файлов подходящее изображение (по размерам чуть меньше представления).
Первая задача — разделить изображение на части. Создаем новый метод “initPuzzle:(NSString *) imagePath” — он разобьёт рисунок на отдельные фрагменты. Параллельно добавьте две константы, определяющие общее число фрагментов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #define NUM_HORIZONTAL_PIECES 3 #define NUM_VERTICAL_PIECES 3 -(void) initPuzzle:(NSString *) imagePath{ UIImage *orgImage = [UIImage imageNamed:imagePath]; if( orgImage == nil ){ return; } tileWidth = orgImage.size.width/NUM_HORIZONTAL_PIECES; tileHeight = orgImage.size.height/NUM_VERTICAL_PIECES; for( int x=0; x<NUM_HORIZONTAL_PIECES; x++ ){ for( int y=0; y<NUM_VERTICAL_PIECES; y++ ){ CGRect frame = CGRectMake(tileWidth*x, tileHeight*y, tileWidth, tileHeight ); CGImageRef tileImageRef = CGImageCreateWithImageInRect( orgImage.CGImage, frame ); UIImage *tileImage = [UIImage imageWithCGImage:tileImageRef]; UIImageView *tileImageView = [[UIImageView alloc] initWithImage:tileImage]; tileImageView.frame = frame; // освобождаем ресурсы [tileImage release]; CGImageRelease( tileImageRef ); // добавляем к представлению [self.view insertSubview:tileImageView atIndex:0]; [tileImageView release]; } } } |
Запускаем приложение — на экране iPhone появляется изображение, уже поделенное на 9 фрагментов. Это сделал метод “GFImageCreateWithImageInRect” (Core Graphics), который принимает ссылку на изображение и прямоугольник, а возвращает ссылку на обрезанное изображение (в данном случае, по форме прямоугольника). Имея ссылку, приступаем к созданию экземпляра “UIImage“.
Как уже упоминалось выше, для каждого фрагмента запоминается исходная позиция (чтобы определить окончание сборки пазла), а также текущее положение по отношению к сетке. Для этой цели расширим класс “UIImageView” и добавим еще два свойства. Дополнительно можно немного раздвинуть фрагменты, чтобы они больше напоминали стандартый пазл, и добавить пустой участок, открыв возможность перемещения.
Для начала внесем в заголовочный файл константы с промежутками вместе с переменными, отвечающими за позиции фрагментов (включая пустой).
В итоге заголовочный файл должен выглядеть примерно так:
1 2 3 4 5 6 7 8 9 10 11 | #define NUM_HORIZONTAL_PIECES 3 #define NUM_VERTICAL_PIECES 3 #define TILE_SPACING 4 @interface SliderController : UIViewController { CGFloat tileWidth; CGFloat tileHeight; NSMutableArray *tiles; CGPoint blankPosition; } @property (nonatomic,retain) NSMutableArray *tiles; @end |
Заполнить пробелы в классе реализации предлагаю самостоятельно.
Теперь у нас есть заполнитель для фрагментов и пустого места — можно переходить к отображению отдельного фрагмента. Расширим класс “UIImageView” (рассмотренным выше способом) и добавим новые свойства.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @interface Tile : UIImageView { CGPoint originalPosition; CGPoint currentPosition; } @property (nonatomic,readwrite) CGPoint originalPosition; @property (nonatomic,readwrite) CGPoint currentPosition; @end @implementation Tile @synthesize originalPosition; @synthesize currentPosition; - (void) dealloc { [self removeFromSuperview]; [super dealloc]; } @end |
В комментариях к данному коду упомяну только, что после освобождения объекта мы удаляем его из родительского уровня. Объясняется это тем, что мы имеем дело с массивом фрагментов. Когда мы его отбрасываем (освобождаем), каждый из фрагментов должен удалить себя из представления.
Вернемся к методу “-(void) initPuzzle:(NSString *) imagePath” и внесем ряд корректировок:
- пропускать “пустой” фрагмент;
- к каждому фрагменту добавлять позицию в сетке;
- увеличить расстояние между фрагментами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | -(void) initPuzzle:(NSString *) imagePath{ UIImage *orgImage = [UIImage imageNamed:imagePath]; if( orgImage == nil ){ return; } [self.tiles removeAllObjects]; tileWidth = orgImage.size.width/NUM_HORIZONTAL_PIECES; tileHeight = orgImage.size.height/NUM_VERTICAL_PIECES; blankPosition = CGPointMake( NUM_HORIZONTAL_PIECES-1, NUM_VERTICAL_PIECES-1 ); for( int x=0; x<NUM_HORIZONTAL_PIECES; x++ ){ for( int y=0; y<NUM_VERTICAL_PIECES; y++ ){ CGPoint orgPosition = CGPointMake(x,y); if( blankPosition.x == orgPosition.x && blankPosition.y == orgPosition.y ){ continue; } CGRect frame = CGRectMake(tileWidth*x, tileHeight*y, tileWidth, tileHeight ); CGImageRef tileImageRef = CGImageCreateWithImageInRect( orgImage.CGImage, frame ); UIImage *tileImage = [UIImage imageWithCGImage:tileImageRef]; CGRect tileFrame = CGRectMake((tileWidth+TILE_SPACING)*x, (tileHeight+TILE_SPACING)*y, tileWidth, tileHeight ); Tile *tileImageView = [[Tile alloc] initWithImage:tileImage]; tileImageView.frame = tileFrame; tileImageView.originalPosition = orgPosition; tileImageView.currentPosition = orgPosition; // освобождаем русурсы [tileImage release]; CGImageRelease( tileImageRef ); [tiles addObject:tileImageView]; // добавляем к представлению [self.view insertSubview:tileImageView atIndex:0]; [tileImageView release]; } } } |
Для начала очищаем массив, потом указываем пустую позицию последней в сетке. Для каждого фрагмента создаем описывающую его положение точку, привязывая ее к свойствам “originalPosition” и “currentPosition“. Перед обработкой фрагмента проверяем, соответствует ли его позиция пустому положению. В случае подтверждения пропускаем фрагмент. Чуть не забыл — и добавляем его в массив фрагментов.
Закончив с этим, переходим к следующему этапу проекта. Теперь необходимо беспорядочно разместить фрагменты на экране, чтобы пользователю пришлось поломать голову над тем, как собрать изображение обратно. Запустив n-ное количество циклов, будем случайным образом выбирать один из фрагментов рядом с пустым, меняя их местами. Для этого сначала определим разрешенные перемещения, что легко выполнит приведенный ниже фрагмент кода:
1 2 3 4 5 6 7 8 | #define SHUFFLE_NUMBER 100 typedef enum { NONE = 0, UP = 1, DOWN = 2, LEFT = 3, RIGHT = 4 } ShuffleMove; |
Здесь заданы n (количество случайных перемещения фрагментов) и тип “enum“, с помощью которого будут различаться разрешенные и некорректные ходы.
Первый метод “validMove:(Tile *) tile” принимает фрагмент и возвращает enum “ShuffleMove“, определяя, может ли перемещаться указанный фрагмент и в каком направлении. Для этого проверяется позиция фрагмента по отношению к пустому. Если указанный фрагмент соседствует с пустым, он может встать на его место.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | -(ShuffleMove) validMove:(Tile *) tile{ // пустая точка над текущим фрагментом if( tile.currentPosition.x == blankPosition.x && tile.currentPosition.y == blankPosition.y+1 ){ return UP; } // пустая точка под текущим фрагментом if( tile.currentPosition.x == blankPosition.x && tile.currentPosition.y == blankPosition.y-1 ){ return DOWN; } // пустая точка слева от текущего фрагмента if( tile.currentPosition.x == blankPosition.x+1 && tile.currentPosition.y == blankPosition.y ){ return LEFT; } // пустая точка справа от текущего фрагмента if( tile.currentPosition.x == blankPosition.x-1 && tile.currentPosition.y == blankPosition.y ){ return RIGHT; } return NONE; } |
Внедряем методы, ответственные за перемещение фрагмента. Их будет два: “(movePiece:(Tile *) tile withAnimation:(BOOL) animate)” определит, в каком направлении может двигаться фрагмент, и передаст задачу собственно перемещения следующему методу — “movePiece:(Tile *) tile inDirectionX:(NSInteger) dx inDirectionY:(NSInteger) dy withAnimation:(BOOL) animate)“. Второй из методов рассчитывает разницу в координатах x и y (в зависимости от того, как именно по отношению к перемещаемому фрагменту расположен пустой) и на основании ее вычисляет новое положение, меняя местами значения “currentPosition” и “blankPosition“. Если “animate” является истиной, заключаем параметры положения в операторы анимации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | -(void) movePiece:(Tile *) tile withAnimation:(BOOL) animate{ switch ( [self validMove:tile] ) { case UP: [self movePiece:tile inDirectionX:0 inDirectionY:-1 withAnimation:animate]; break; case DOWN: [self movePiece:tile inDirectionX:0 inDirectionY:1 withAnimation:animate]; break; case LEFT: [self movePiece:tile inDirectionX:-1 inDirectionY:0 withAnimation:animate]; break; case RIGHT: [self movePiece:tile inDirectionX:1 inDirectionY:0 withAnimation:animate]; break; default: break; } } -(void) movePiece:(Tile *) tile inDirectionX:(NSInteger) dx inDirectionY:(NSInteger) dy withAnimation:(BOOL) animate{ tile.currentPosition = CGPointMake( tile.currentPosition.x+dx, tile.currentPosition.y+dy); blankPosition = CGPointMake( blankPosition.x-dx, blankPosition.y-dy ); int x = tile.currentPosition.x; int y = tile.currentPosition.y; if( animate ){ [UIView beginAnimations:@"frame" context:nil]; } tile.frame = CGRectMake((tileWidth+TILE_SPACING)*x, (tileHeight+TILE_SPACING)*y, tileWidth, tileHeight ); if( animate ){ [UIView commitAnimations]; } } |
Последним шагом создаем метод “shuffle“, который, как уже упоминалось выше, будет выполнять цикл количество раз, соответствующее “SHUFFLE_NUMBER“, хаотично перемещая фрагменты, для которых разрешено движение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | -(void) shuffle{ NSMutableArray *validMoves = [[NSMutableArray alloc] init]; srandom(time(NULL)); for( int i=0; i<SHUFFLE_NUMBER; i++ ){ [validMoves removeAllObjects]; // выясняем, какие фрагменты могут перемещаться for( Tile *t in tiles ){ if( [self validMove:t] != NONE ){ [validMoves addObject:t]; } } // случайным образом выбираем фрагмент для перемещения NSInteger pick = random()%[validMoves count]; //NSLog(@"shuffleRandom using pick: %d from array of size %d", pick, [validMoves count]); [self movePiece Tile *)[validMoves objectAtIndex:pick] withAnimation:NO]; } [validMoves release]; } |
Ничего нового — делаем то, что и намечали. Для выбора разрешенного к перемещению фрагмента циклически перемещаемся между всеми, занося в массив те, что могут двигаться. Рассмотрев все фрагменты, случайным образом выбираем один и сдвигаем.
Осталось только вызвать нужный метод. К нижней части метода “initPuzzle(NSString *) imagePath” добавьте следующую строку:
1 | [self shuffle]; |
ОК. Теперь наши фрагменты отображаются на экране, причем в беспорядке. Осталось добавить интерактивности, чтобы пользователь мог их перемещать. Для этого зафиксируем касание и определим фрагмент, который нажал пользователь. Если фрагмент разрешен к перемещению, двигаем его.
Для начала внедрим вспомогательный метод, который будет возвращать привязанный к касанию пользователя фрагмент.
1 2 3 4 5 6 7 8 9 | -(Tile *) getPieceAtPoint:(CGPoint) point{ CGRect touchRect = CGRectMake(point.x, point.y, 1.0, 1.0); for( Tile *t in tiles ){ if( CGRectIntersectsRect(t.frame, touchRect) ){ return t; } } return nil; } |
Теперь, располагая информацией по касанию, определим, на каком фрагменте щелкнул пользователь. Отменяем метод “touchesEnded” и перемещаем выбранный фрагмент.
1 2 3 4 5 6 7 8 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint currentTouch = [touch locationInView:self.view]; Tile *t = [self getPieceAtPoint:currentTouch]; if( t != nil ){ [self movePiece:t withAnimation:YES]; } } |
Вот и все — перед вами собственный пазл. Само собой, еще нужно определить момент окончания игры. Добавьте к коду приведенный ниже метод и обращайтесь к нему каждый раз, когда метод “touchesEnded” перемещает фрагмент.
1 2 3 4 5 6 7 8 | -(BOOL) puzzleCompleted{ for( Tile *t in tiles ){ if( t.originalPosition.x != t.currentPosition.x || t.originalPosition.y != t.currentPosition.y ){ return NO; } } return YES; } |
Остальное оставляю вам. Те, кому заканчивать лень, могут просто загрузить исходный код.
Спасибо за внимание.


(6 голосов, средний: 3.33 из 5)
Последние комментарии
Подскажите пожалуйста… Вот...
Код не открывает страницы по простой причине -...