Pull to refresh

Создаем пазл для iPhone

Reading time 9 min
Views 5.7K
Original author: Joshua Newnham
imageПочему бы не представить в магазине приложений свой собственный пазл — как это сделали мы! В этом уроке я поэтапно расскажу о создании такого приложения. Итоговый результат будет выглядеть примерно так, как на фото. Чашку с кофе — и можно приступать.

Как настоящие программисты, для начала остановимся на том, что такое slider puzzle и как его реализовать. Наверное, все помнят детскую игру «пятнашки», где фишки с цифрами нужно было выстроить по порядку. В нашем случае это будут разрозненные фрагменты изображения, которые собираются в единое целое (их на один меньше, чтобы кусочки можно было перемещать). Теперь подумаем, что понадобится, чтобы воплотить такой проект в жизнь.

Для начала потребуется изображение, которое мы разделим на фрагменты. Разместим их в беспорядке, чтобы после снова собрать. Правда перед этим нужно как-то запомнить, где должен находиться тот или иной фрагмент. Для этого введем новый класс, который будет содержать как оригинальное, так и текущее положение каждого фрагмента в матрице (под матрицей понимается сетка, на которой формируется рисунок). Так мы сможем определить, собрал пользователь пазл или нет (сравнив для каждого фрагмента текущее положение с исходным). Следующая задача — определить разрешенные перемещения. Для этой цели заменим один из фрагментов пустым. На его место разрешается передвинуть соседний фрагмент. Ну вот, в принципе, и все. Если я что-то упустил, разберемся по ходу дела.
Итак, перечислим все, что необходимо сделать:

  • разбить изображение;
  • привязать каждую часть изображения к определенному фрагменту пазла (отвечающему за хранение его исходной и текущей позиции);
  • перемешать беспорядочно все фрагменты (запускаем n-ный цикл, во время которого случайно выбранный фрагмент перемещается на место пустого);
  • фиксируем касание пользователем фрагментов пазла; если перемещение разрешено, меняем местами пустой фрагмент с выбранным и проверяем, вернулось ли изображение к исходному состоянию.

Начнем? Откройте XCode и создайте приложение windows based. (Здесь я буду останавливаться в основном на логике. Детали по настройкам можно получить, загрузив исходный код либо обратившись к предыдущим урокам).

Как обычно, нам понадобится новый контроллер "UIViewController". Создайте его и присвойте соответствующее имя. Теперь отыщите среди своих файлов подходящее изображение (по размерам чуть меньше представления).

Первая задача — разделить изображение на части. Создаем новый метод "initPuzzle:(NSString *) imagePath" — он разобьёт рисунок на отдельные фрагменты. Параллельно добавьте две константы, определяющие общее число фрагментов:

#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];
}
}
}


* This source code was highlighted with Source Code Highlighter.


Запускаем приложение — на экране iPhone появляется изображение, уже поделенное на 9 фрагментов. Это сделал метод "GFImageCreateWithImageInRect" (Core Graphics), который принимает ссылку на изображение и прямоугольник, а возвращает ссылку на обрезанное изображение (в данном случае, по форме прямоугольника). Имея ссылку, приступаем к созданию экземпляра "UIImage".

Как уже упоминалось выше, для каждого фрагмента запоминается исходная позиция (чтобы определить окончание сборки пазла), а также текущее положение по отношению к сетке. Для этой цели расширим класс "UIImageView" и добавим еще два свойства. Дополнительно можно немного раздвинуть фрагменты, чтобы они больше напоминали стандартый пазл, и добавить пустой участок, открыв возможность перемещения.

Для начала внесем в заголовочный файл константы с промежутками вместе с переменными, отвечающими за позиции фрагментов (включая пустой).

В итоге заголовочный файл должен выглядеть примерно так:

#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


* This source code was highlighted with Source Code Highlighter.


Заполнить пробелы в классе реализации предлагаю самостоятельно.

Теперь у нас есть заполнитель для фрагментов и пустого места — можно переходить к отображению отдельного фрагмента. Расширим класс "UIImageView" (рассмотренным выше способом) и добавим новые свойства.

@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


* This source code was highlighted with Source Code Highlighter.


В комментариях к данному коду упомяну только, что после освобождения объекта мы удаляем его из родительского уровня. Объясняется это тем, что мы имеем дело с массивом фрагментов. Когда мы его отбрасываем (освобождаем), каждый из фрагментов должен удалить себя из представления.

Вернемся к методу "-(void) initPuzzle:(NSString *) imagePath" и внесем ряд корректировок:
  • пропускать «пустой» фрагмент;
  • к каждому фрагменту добавлять позицию в сетке;
  • увеличить расстояние между фрагментами.


-(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];
}
}
}


* This source code was highlighted with Source Code Highlighter.


Для начала очищаем массив, потом указываем пустую позицию последней в сетке. Для каждого фрагмента создаем описывающую его положение точку, привязывая ее к свойствам "originalPosition" и "currentPosition". Перед обработкой фрагмента проверяем, соответствует ли его позиция пустому положению. В случае подтверждения пропускаем фрагмент. Чуть не забыл — и добавляем его в массив фрагментов.

Закончив с этим, переходим к следующему этапу проекта. Теперь необходимо беспорядочно разместить фрагменты на экране, чтобы пользователю пришлось поломать голову над тем, как собрать изображение обратно. Запустив n-ное количество циклов, будем случайным образом выбирать один из фрагментов рядом с пустым, меняя их местами. Для этого сначала определим разрешенные перемещения, что легко выполнит приведенный ниже фрагмент кода:

#define SHUFFLE_NUMBER  100
typedef enum {
NONE      = 0,
UP      = 1,
DOWN      = 2,
LEFT      = 3,
RIGHT      = 4
} ShuffleMove;


* This source code was highlighted with Source Code Highlighter.


Здесь заданы n (количество случайных перемещения фрагментов) и тип "enum", с помощью которого будут различаться разрешенные и некорректные ходы.

Первый метод "validMove:(Tile *) tile" принимает фрагмент и возвращает enum "ShuffleMove", определяя, может ли перемещаться указанный фрагмент и в каком направлении. Для этого проверяется позиция фрагмента по отношению к пустому. Если указанный фрагмент соседствует с пустым, он может встать на его место.

-(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;
}


* This source code was highlighted with Source Code Highlighter.


Внедряем методы, ответственные за перемещение фрагмента. Их будет два: "(movePiece:(Tile *) tile withAnimation:(BOOL) animate)" определит, в каком направлении может двигаться фрагмент, и передаст задачу собственно перемещения следующему методу — "movePiece:(Tile *) tile inDirectionX:(NSInteger) dx inDirectionY:(NSInteger) dy withAnimation:(BOOL) animate)". Второй из методов рассчитывает разницу в координатах x и y (в зависимости от того, как именно по отношению к перемещаемому фрагменту расположен пустой) и на основании ее вычисляет новое положение, меняя местами значения "currentPosition" и "blankPosition". Если "animate" является истиной, заключаем параметры положения в операторы анимации.

-(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];
}
}


* This source code was highlighted with Source Code Highlighter.


Последним шагом создаем метод "shuffle", который, как уже упоминалось выше, будет выполнять цикл количество раз, соответствующее "SHUFFLE_NUMBER", хаотично перемещая фрагменты, для которых разрешено движение.

-(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];
}


* This source code was highlighted with Source Code Highlighter.


Ничего нового — делаем то, что и намечали. Для выбора разрешенного к перемещению фрагмента циклически перемещаемся между всеми, занося в массив те, что могут двигаться. Рассмотрев все фрагменты, случайным образом выбираем один и сдвигаем.

Осталось только вызвать нужный метод. К нижней части метода "initPuzzle(NSString *) imagePath" добавьте следующую строку:

[self shuffle];

* This source code was highlighted with Source Code Highlighter.


ОК. Теперь наши фрагменты отображаются на экране, причем в беспорядке. Осталось добавить интерактивности, чтобы пользователь мог их перемещать. Для этого зафиксируем касание и определим фрагмент, который нажал пользователь. Если фрагмент разрешен к перемещению, двигаем его.

Для начала внедрим вспомогательный метод, который будет возвращать привязанный к касанию пользователя фрагмент.

-(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;
}


* This source code was highlighted with Source Code Highlighter.

Теперь, располагая информацией по касанию, определим, на каком фрагменте щелкнул пользователь. Отменяем метод "touchesEnded" и перемещаем выбранный фрагмент.

- (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];
}
}


* This source code was highlighted with Source Code Highlighter.


Вот и все — перед вами собственный пазл. Само собой, еще нужно определить момент окончания игры. Добавьте к коду приведенный ниже метод и обращайтесь к нему каждый раз, когда метод "touchesEnded" перемещает фрагмент.

-(BOOL) puzzleCompleted{
for( Tile *t in tiles ){
if( t.originalPosition.x != t.currentPosition.x || t.originalPosition.y != t.currentPosition.y ){
return NO;
}
}
return YES;
}


* This source code was highlighted with Source Code Highlighter.


Остальное оставляю вам. Те, кому заканчивать лень, могут просто загрузить исходный код. :) Спасибо за внимание.

Исходный код к уроку можно скачать здесь.
Tags:
Hubs:
+17
Comments 16
Comments Comments 16

Articles