
Why not present 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 start.
As real programmers, first we will focus on what a
slider puzzle is and how to implement it. Probably everyone remembers the children's game "tag", where the chips with numbers needed to be built in order. In our case, these will be scattered fragments of an image that are assembled into a single whole (they are one less so that the pieces can be moved). Now we will think what it takes to bring such a project to life.
First you need an image that we divide into fragments. We will place them in disarray in order to reassemble them. The truth before that you need 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 (by the matrix is meant the grid on which the pattern is formed). This way we will be able to determine whether the user has assembled the puzzle or not (comparing the current position with the source for each fragment). The next task is to determine the allowed displacements. For this purpose, replace one of the fragments with an empty one. In its place is allowed to move the adjacent fragment. Well, in principle, that's all. If I missed something, let's figure it out.
So, we list everything that needs to be done:
')
- split image;
- bind 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 space);
- fix the user touching the puzzle pieces; 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 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 it an appropriate name. Now look for a suitable image among your files (slightly smaller than the view).
The first task is to divide the image into parts. Create a new method "
initPuzzle: (NSString *) imagePath " - it will break the drawing into separate fragments. In parallel, add two constants that define 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<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 .
We start the application - an image already divided into 9 fragments appears on the iPhone screen. This was done by the "
GFImageCreateWithImageInRect " method (Core Graphics), which takes a link to an image and a rectangle, and returns a link to the cropped image (in this case, the shape of the rectangle). Having a link, proceed to the creation of an instance of "
UIImage ".
As mentioned above, for each piece, the initial position is remembered (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. Additionally, you can slightly push the fragments so that they look more like a standard puzzle, and add an empty area, opening the possibility of moving.
To begin with, we will add constants in the header file at intervals along with variables responsible for the position of the fragments (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 blanks in the implementation class myself.
Now we have a placeholder for fragments and empty space - we can proceed to the display of a separate fragment. Expand the "
UIImageView " class (as described above) and add new properties.
@ 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 to this code I will only mention that after the object is released, we remove it from the parent level. This is explained by the fact that we are dealing with an array of fragments. When we drop it (release), each of the fragments must remove itself from the view.
Let's go back to the "
- (void) initPuzzle: (NSString *) imagePath "
method and make a number of 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<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 .
To begin, clear the array, then specify the empty position of the last in the grid. For each fragment, we 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. If confirmed, skip the snippet. I almost forgot - and add it to the array of fragments.
Having finished with this, we proceed to the next stage of the project. Now you need to randomly place the fragments on the screen, so that the user had to break his head over how to assemble the image back. Having started the
n- th number of cycles, we will randomly select one of the fragments next to the empty one, changing their places. To do this, we first define the allowed displacements, which the following code snippet can easily do:
#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 given, with which the allowed and incorrect moves will be distinguished.
The first method "
validMove: (Tile *) tile " takes a fragment and returns an
enum "
ShuffleMove ", determining whether the specified fragment can move and in what direction. To do this, check the position of the fragment with respect to the empty one. If the specified fragment is adjacent to empty, it can take 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 implement the methods responsible for moving the fragment. There will be two: "
(movePiece: (Tile *) tile withAnimation: (BOOL) animate) " will determine in which direction the fragment can move, and pass the task of the actual movement to the following method - "
movePiece: (Tile *) tile inDirectionX: (NSInteger ) dx inDirectionY: (NSInteger) dy withAnimation: (BOOL) animate) ". The second method calculates the difference in
x and
y coordinates (depending on how empty it is with respect to the moving fragment) and, based on it, calculates the new position, interchanging the values of "
currentPosition " and "
blankPosition ". If "
animate " is true,
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 final step is to create the "
shuffle " method, which, as mentioned above, will perform the cycle the number of times corresponding to "
SHUFFLE_NUMBER ", randomly moving the fragments for which movement is allowed.
-( 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 .
Nothing new - we do what we have planned. To select the fragment allowed to move, we cyclically move between everyone, putting those that can move into the array. Having considered all the fragments, we randomly select one and shift it.
It remains only to call the desired method. At the bottom of the "
initPuzzle (NSString *) imagePath "
method , add the following line:
[self shuffle];
* This source code was highlighted with Source Code Highlighter .
OK. Now our fragments are displayed on the screen, and in disarray. It remains to add interactivity so that the user can move them. To do this, we fix the touch and define the fragment that the user clicked. If the fragment is allowed to move, move it.
To begin with, we will implement a helper method that will return a fragment bound to a 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 touch, we will determine which fragment the user clicked.
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 - before you own puzzle. Of course, you still need to determine the end of the game. Add the following method to the code and access it each 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 finish can simply download the source code. :) Thanks for attention.
The source code for the lesson can be downloaded
here .