Create a puzzle for iPhone

Original author: Joshua Newnham
  • Transfer
imageWhy not submit your own puzzle in the app store - as we did! In this tutorial, I will step by step talk about creating such an application. The final result will look something like the photo. A cup of coffee - and you can proceed.

As real programmers, for a start we will dwell on what slider puzzle is and how to implement it. Probably everyone remembers the children's game "tag", where the chips with numbers had to be arranged in order. In our case, these will be scattered fragments of the image that are assembled into a single whole (they are one less so that the pieces can be moved). Now let's think about what it will take to bring such a project to life.

First you need an image, which we will divide into fragments. Let's put them in a mess so that they can be reassembled later. The truth before this is to somehow remember where this or that fragment should be located. To do this, we introduce a new class that will contain both the original and the current position of each fragment in the matrix (the matrix is ​​understood as the grid on which the picture is formed). So we can determine whether the user has assembled the puzzle or not (comparing for each fragment the current position with the original). The next task is to determine the allowed movements. For this purpose, replace one of the fragments with an empty one. In its place is allowed to move the neighboring fragment. Well, that’s basically it. If I missed something, we'll figure it out along the way.
So, we list all that needs to be done:

  • split image;
  • attach each part of the image to a specific piece of the puzzle (responsible for storing its original and current position);
  • randomly mix all the fragments (we start the nth cycle, during which a randomly selected fragment moves to the empty place);
  • we fix the user’s touch of the puzzle fragments; if movement is allowed, swap the empty fragment with the selected one and check if the image has returned to its original state.

Let's start Open Xcode and create a windows based application . (Here I will dwell mainly on the logic. Details on the settings can be obtained by downloading the source code or referring to the previous lessons).

As usual, we will need a new controller " UIViewController ". Create it and give the appropriate name. Now find among your files a suitable image (the sizes are slightly smaller than the presentation).

The first task is to divide the image into parts. We create a new method " initPuzzle: (NSString *) imagePath " - it will break the picture into separate fragments. In parallel, add two constants that determine the total number of fragments:

#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 for( int y=0; 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.


Launch the application - an image already divided into 9 fragments appears on the iPhone screen. This was done by the " GFImageCreateWithImageInRect " (Core Graphics) method , which accepts a link to an image and a rectangle, and returns a link to the cropped image (in this case, in the shape of a rectangle). Having the link, we proceed to create an instance of " UIImage ".

As mentioned above, for each fragment, the initial position is stored (to determine the end of the puzzle assembly), as well as the current position relative to the grid. For this purpose, we extend the class " UIImageView"and add two more properties. In addition, you can slightly expand the fragments to make them look more like a standard puzzle, and add an empty section, making it possible to move.

To begin, we will add constants with gaps along with variables responsible for the positions of the fragments in the header file (including the empty one) .

As a result, the header file should look like this:

#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.


I propose to fill in the gaps in the implementation class on my own.

Now we have a placeholder for fragments and empty space - you can proceed to display a single fragment. Extend the class " a UIImageView " (discussed above means) and add new features.

@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.


In the comments on this code, I only mention that after the release of the object, we remove it from the parent level. This is explained by the fact that we are dealing with an array of fragments. When we discard it (release), each of the fragments must remove itself from the view.

Let's go back to the method " - (void) initPuzzle: (NSString *) imagePath " and make some adjustments:
  • skip the "empty" fragment;
  • add a position in the grid to each fragment;
  • increase the distance between the fragments.


-(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 for( int y=0; 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.


First we clear the array, then indicate the empty position of the last in the grid. For each fragment, create a point describing its position, tying it to the " originalPosition " and " currentPosition " properties . Before processing a fragment, we check whether its position corresponds to an empty position. In case of confirmation, skip the fragment. I almost forgot - and add it to the array of fragments.

Having finished with this, we move on to the next stage of the project. Now you need to randomly place the fragments on the screen, so that the user had to puzzle over how to assemble the image back. By running n-th number of cycles, we will randomly select one of the fragments next to the empty one, changing their places. To do this, first determine the allowed movements, which will easily perform the code snippet below:

#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.


Here n (the number of random movements of fragments) and the type " enum " are specified , with the help of which the allowed and incorrect moves will be distinguished.

The first method, " validMove: (Tile *) tile ", accepts the fragment and returns the enum " ShuffleMove ", determining whether the specified fragment can move and in which direction. To do this, check the position of the fragment with respect to the empty one. If the specified fragment is adjacent to the empty one, it can fall into its place.

-(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.


We introduce the methods responsible for moving the fragment. There will be two of them: " (movePiece: (Tile *) tile withAnimation: (BOOL) animate) " will determine in which direction the fragment can move and pass the task of actually moving to the next method - " movePiece: (Tile *) tile inDirectionX: (NSInteger ) dx inDirectionY: (NSInteger) dy withAnimation: (BOOL) animate) ". The second of the methods calculates the difference in the x and y coordinates (depending on how exactly the fragment is located empty) and on the basis of it calculates the new position, interchanging the values ​​of " currentPosition " and " blankPosition ". If " animate"is true, we enclose the position parameters in the animation operators.

-(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.


The last step is to create the " shuffle " method , which, as mentioned above, will loop the number of times corresponding to " SHUFFLE_NUMBER ", randomly moving fragments for which movement is allowed.

-(void) shuffle{
NSMutableArray *validMoves = [[NSMutableArray alloc] init];
srandom(time(NULL));
for( int i=0; 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.


Nothing new - we do what we planned. To select a fragment allowed to move, we cyclically move between all, entering into the array those that can move. Having examined all the fragments, randomly select one and shift.

It remains only to call the desired method. Add the following line to the bottom of the " initPuzzle (NSString *) imagePath " method :

[self shuffle];

* This source code was highlighted with Source Code Highlighter.


OK. Now our fragments are displayed on the screen, and in a mess. It remains to add interactivity so that the user can move them. To do this, fix the touch and define the fragment that the user clicked. If the fragment is allowed to move, move it.

First, we’ll introduce a helper method that will return a fragment attached to the user's touch.

-(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.

Now, having information on the touch, we determine which fragment the user clicked on. We cancel the " touchesEnded " method and move the selected fragment.

- (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.


That's all - you have your own puzzle. Of course, you still need to determine the moment the game ends. Add the method below to the code and refer to it every time the touchesEnded method moves a fragment.

-(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.


I leave the rest to you. Those who are too lazy to complete can simply download the source code. :) Thanks for attention.

The source code for the lesson can be downloaded here .

Also popular now: