2011年11月10日星期四

Cocos2d Game Tutorial – Building a Slide Image Game

Cocos2d Game Tutorial – Building a Slide Image Game:
It’s been a week since our last tutorial and in this new tutorial, we’re going to actually tackle a game – A slider game… You know the type of game I’m talking about, the players job is to put a game that is jumbled up, back together again. The great thing about games of this type is that we can use this as our first foray into the world of tile based games.

So what do we need to create this wonderful type of game? Below are a list of steps to make your own tile based slider game:

  1. Create a “Tile” class that has a sprite, a position and a value
  2. Create a manager type class that will create all the tiles and keep track of them
  3. Add touch components that will allow the user to swap tile locations
  4. Add additional image randomizations so there is more variety in the game

That’s it… Sounds simple when written out, huh? Well, we’re going to go through it and show you that it can be simple.

Source Code after the break!



UPDATE: Added a Part 2 for an Image Sliding Solution for those that have asked for it in the comments. Enjoy!

Source code to this tutorials can be downloaded here: Slide Image Game Tutorial

Updated code for XCode 3.2.5 and iOS 4.2 here: Slide Image Game Tutorial UPDATED

So what am I talking about when I say slide image game? Well it’s important for everyone to see the final product…

Here is a complete image…



Here is the mixed up image…



Seriously, this basic example of a game, can take you everywhere… I’ll even show some of the other games you can make with this type of game as a basis in another upcoming tutorial.

Anyway, lets jump right into it then! To accomplish this momentous task, we’ll need a few extra classes, based on the bullet points I said above – Those classes will be: Tile.h, Tile.m, Box.h, Box.m

If you’ve read any of the other tutorials, you should already know about the “SceneManager” and “PlayLayer” classes, which will also make an appearance.

First up is the Tile class I’ve already talked about -

Tile.h:

#import "cocos2d.h"

#import "Constants.h";

@interface Tile : NSObject {
int x, y, value;
CCSprite *sprite;
}

-(id) initWithX: (int) posX Y: (int) posY;
@property (nonatomic, readonly) int x, y;
@property (nonatomic) int value;
@property (nonatomic, retain) CCSprite *sprite;
-(BOOL) nearTile: (Tile *)othertile;
-(void) trade:(Tile *)otherTile;
-(CGPoint) pixPosition;
@end

The main point of the tile class is to act as a small portion of a larger whole. It will contain it’s own sprite as well as have an x and y coordinate (this will be different from the sprite x and y) and a value. The value could be anything such as the order the tile was placed in (maybe to verify the player has correctly reassembled the image after each move), in the current version we’re going to leave it alone.

Now we’ll look at how the class is put together…

Tile.m:

#import "Tile.h"

@implementation Tile
@synthesize x, y, value, sprite;

-(id) initWithX: (int) posX Y: (int) posY{
self = [super init];
x = posX;
y = posY;
return self;
}

-(BOOL) nearTile: (Tile *)othertile{
return
(x == othertile.x && abs(y - othertile.y)==1)
||
(y == othertile.y && abs(x - othertile.x)==1);
}

-(void) trade: (Tile *)otherTile{
CCSprite *tempSprite = [sprite retain];
int tempValue = value;
self.sprite = otherTile.sprite;
self.value = otherTile.value;
otherTile.sprite = tempSprite;
otherTile.value = tempValue;
[tempSprite release];
}

-(CGPoint) pixPosition{
return ccp(kStartX + x * kTileSize +kTileSize/2.0f,kStartY + y * kTileSize +kTileSize/2.0f);
}

@end

Most of this is straight forward – We’re using four class methods, “initWithX”, “nearTile”, “trade” and “pixPosition”.

the method “initWithX” is exactly what it sounds like – It’s the initializer fot the Tile class that gets called when the Tile class first get instantiated. It takes in an X and Y value that related not to the world space, but to the space within the the Box contains it. So for example a Tile might get called with initWithX:3 Y:4 and it would put it at position X = 3 and Y = 4 (out of a grid ox 7 x 7). We’ll use this X and Y location later when we attach the sprite and give the tile sprite a pixel location on the screen.

The method “nearTile” takes in a Tile as a parameter and returns whether the Tile passed in is adjacent and if it is, returns “YES” otherwise “NO”

The method “trade” works to swap the sprite and the value of from one Tile to another. We create a temporary sprite and store the current sprite into that temporary variable. We then copy the other Tile’s sprite into our tile and finally copy the temporary sprite into the other sprite.

Finally, “pixPosition” returns the correct “window” coordinate in pixels for a given tile – you’ll see this used specifically when we’re animating the swap between tiles.

The main focus of the Box class is to handle the creation of the all the individual tiles, loading up the sprites and positioning them on the screen.

Box.h:

#import "cocos2d.h"

#import "Constants.h"
#import "Tile.h"

@interface Box : NSObject {
CGSize size;
NSMutableArray *content;
NSMutableSet *readyToRemoveTiles;
CCLayer *layer;
Tile *OutBorderTile;

NSInteger imgValue;
}
@property(nonatomic, retain) CCLayer *layer;
@property(nonatomic, readonly) CGSize size;

-(id) initWithSize: (CGSize) size factor: (int) facotr;
-(Tile *) objectAtX: (int) posX Y: (int) posY;
-(BOOL) check;

@end

Some of these you may have guessed before we break this down… size relates to the size of the grid we’re creating (3×3. 4×5, 5×3, 7×7, etc).

The two main powerhouses for the Box class are the “content” and the “readyToRemove” variables:

The “content” variable is actually a multi-dimensional array or at least one way of making it. It’s made from creating an NSMutableArray that represents the column and then for each row we add another NSMutableArray. We can then find the correct Tile by saying “return [[content objectAtIndex: y] objectAtIndex: x];”.

The “readyToRemove” variable is useful only initially in this particular example, but I will show you another game later on where we’ll use it all the time – In this example however we’ll use this to force the loading of all new sprites once the board is initially created.

So the good stuff is just below – Lets look at the main Box class and see what is going on:

Box.m:

#import "Box.h"

#import "Box.h"

@implementation Box
@synthesize layer;
@synthesize size;
@synthesize lock;

-(id) initWithSize: (CGSize) aSize imgValue: (int) aImgValue{
self = [super init];
imgValue = aImgValue;
size = aSize;
OutBorderTile = [[Tile alloc] initWithX:-1 Y:-1];
content = [NSMutableArray arrayWithCapacity: size.height];

readyToRemoveTiles = [NSMutableSet setWithCapacity:50];

for (int y=0; y < size.height; y++) {

NSMutableArray *rowContent = [NSMutableArray arrayWithCapacity:size.width];
for (int x=0; x < size.width; x++) {
Tile *tile = [[Tile alloc] initWithX:x Y:y];
[rowContent addObject:tile];
[readyToRemoveTiles addObject:tile];
[tile release];
}
[content addObject:rowContent];
[content retain];
}

[readyToRemoveTiles retain];

return self;
}

-(Tile *) objectAtX: (int) x Y: (int) y{
if (x < 0 || x >= kBoxWidth || y < 0 || y >= kBoxHeight) {
return OutBorderTile;
}
return [[content objectAtIndex: y] objectAtIndex: x];
}

-(BOOL) check{
NSArray *objects = [[readyToRemoveTiles objectEnumerator] allObjects];
if ([objects count] == 0) {
return NO;
}
int countTile = [objects count];
for (int i=0; i < countTile; i++) {
Tile *tile = [objects objectAtIndex:i];
tile.value = 0;
if (tile.sprite) {
[layer removeChild: tile.sprite cleanup:YES];
}
}
[readyToRemoveTiles removeAllObjects];

NSString *name = [NSString stringWithFormat:@"%d.png",imgValue];
CCTexture2D * texture = [[CCTextureCache sharedTextureCache] addImage:name];
NSMutableArray *imgFrames = [NSMutableArray array];
[imgFrames removeAllObjects];

for (int i = 0; i < 7; i++) {
for (int j = 6; j >= 0; j--) {
CCSpriteFrame *imgFrame = [CCSpriteFrame frameWithTexture:texture rect:CGRectMake(i*40, j*40, 40, 40) offset:CGPointZero];
[imgFrames addObject:imgFrame];
}
}
for (int x=0; x< size.width; x++) {
int extension = 0;
for (int y=0; y < size.height; y++) {
Tile *tile = [self objectAtX:x Y:y];
if(tile.value == 0){
extension++;
}else if (extension == 0) {
}
}
for (int i=0; i < extension; i++) {
Tile *destTile = [self objectAtX:x Y:kBoxHeight-extension+i];
CCSpriteFrame * img = [imgFrames objectAtIndex:0];
CCSprite *sprite = [CCSprite spriteWithSpriteFrame:img];
[imgFrames removeObjectIdenticalTo:img];
sprite.position = ccp(kStartX + x * kTileSize + kTileSize/2, kStartY + (kBoxHeight + i) * kTileSize + kTileSize/2 - kTileSize * extension);
[layer addChild: sprite];
destTile.value = imgValue;
destTile.sprite = sprite;
}
}

return YES;
}

@end

So what's going on in this class - I think the "initWithSize" and "getObjectAt" are fairly self explanatory... One created the box using the multi-dimensional array and stores the newly created Tiles into it. It also populated all the objects into the other array "readyToRemoveAllTiles" so that we can clean out any sprites (say for a new level in part 2 *wink* *wink* *nudge* *nudge*) and then returns the instance of itself. The other just returns a valid Tile given the passed in X and Y parameters.... It's pretty cut and dry...

So the good class method - "check" - The first thing we do is determine if there are any Tiles in the "readyToRemoveTiles" array... In our case, all the items are in it... great! We'll then proceed to loop through all the items in the array and remove the sprites one by one and then empty the entire array after we're done... Again, you can image that this would be the start of a good cleanup job for a game when you wanted multiple levels...

Next we load up a texture from one of the resources (aptly named) 1.png, 2.png, 3.png, etc and store them into a CCTexture2D that we will build sprite frames from. We're going to be creating 49 sprite frames to account for all of our Tiles... Since we made all our images 280x280 this makes for wonderful math (each CCSpriteFrame is 40 x 40). We do a double "for" loop to capture each of the Tiles sprite images and store them into an "imgFrames" array and then we hit the final leg of this race...

for (int x=0; x< size.width; x++) {
int extension = 0;
for (int y=0; y < size.height; y++) {
Tile *tile = [self objectAtX:x Y:y];
if(tile.value == 0){
extension++;
}else if (extension == 0) {
}
}

The final portion is not specifically needed in this way, but it's a way to check for how many of the Tiles need to be replaced with images - This is good for something like a Tile dropping game where you don't want to replace all the tiles at once... In this case we check each Tile to see if there is a value of 0 and keep track of them by incrementing the "extension" variable... For this example since all the Tiles were in the "readyToRemoveTiles" array, the extension variable will always be 7...

Below, we're getting the value of the first Tile object at the Y coordinate (kBoxHeight-extension+i) - Since in our example kBoxHeight = 7 and extension will also always be 7 we can just care about the "i" variable... it will go up 1 by 1 until it hits 6... Again, why did I do it this way, because we'll be using this later on in other games and it's best to be familiar with it now and get it over with :) Next comes the sprite work....

for (int i=0; i < extension; i++) {
Tile *destTile = [self objectAtX:x Y:kBoxHeight-extension+i];
CCSpriteFrame * img = [imgFrames objectAtIndex:0];
CCSprite *sprite = [CCSprite spriteWithSpriteFrame:img];
[imgFrames removeObjectIdenticalTo:img];
sprite.position = ccp(kStartX + x * kTileSize + kTileSize/2, kStartY + (kBoxHeight + i) * kTileSize + kTileSize/2 - kTileSize * extension);
[layer addChild: sprite];
destTile.value = imgValue;
destTile.sprite = sprite;
}

We then create a new CCSprite given the CCSpriteFrame and position it according to it's location in the Box. We also set the same imgValue to all the Tiles and we're done...

Lastly we have PlayLayer a class that should be all too familiar... It will handle our "touches" and "initializing" of our box class. We need to keep track of the first two touches - the "selectedTile" will always be the current tile that the player selected and we'll know when it already has a value that the player may have selected another tile and that we may have to swap the two... We also, obviously, need to keep track of the box itself that contains all the Tiles, so we'll keep track of that here too...

PlayLayer.h:

#import "cocos2d.h"

#import "Box.h"

@interface PlayLayer : CCLayer
{
Box *box;
Tile *selectedTile;

NSInteger value;
}

-(void) changeWithTileA: (Tile *) a TileB: (Tile *) b sel : (SEL) sel;
-(void) check: (id) sender data: (id) data;
@end

So how do this all come together? Well I've listed it out below...

PlayLayer.m:

#import "PlayLayer.h"

@implementation PlayLayer

-(id) init{
self = [super init];

value = (arc4random() % kKindCount+1);
box = [[Box alloc] initWithSize:CGSizeMake(kBoxWidth,kBoxHeight) imgValue:value];
box.layer = self;
box.lock = YES; 

[box check];

self.isTouchEnabled = YES;

return self;
}

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch* touch = [touches anyObject];
CGPoint location = [touch locationInView: touch.view];
location = [[CCDirector sharedDirector] convertToGL: location];

if (location.y < (kStartY) || location.y > (kStartY + (kTileSize * kBoxHeight))) {
return;
}

int x = (location.x - kStartX) / (kTileSize);
int y = (location.y - kStartY) / (kTileSize);

if (selectedTile && selectedTile.x == x && selectedTile.y == y) {
selectedTile = nil;
return;
}

Tile *tile = [box objectAtX:x Y:y];
if (tile.x >= 0 && tile.y >= 0) {
if (selectedTile && [selectedTile nearTile:tile]) {
[box setLock:YES];
[self changeWithTileA: selectedTile TileB: tile sel: @selector(check:data:)];
selectedTile = nil;
}
else {
if (selectedTile) {
if (selectedTile.x == x && selectedTile.y == y) {
selectedTile = nil;
}
}
selectedTile = tile;
}
}
}

-(void) changeWithTileA: (Tile *) a TileB: (Tile *) b sel : (SEL) sel{
CCAction *actionA = [CCSequence actions:
[CCMoveTo actionWithDuration:kMoveTileTime position:[b pixPosition]],
[CCCallFuncND actionWithTarget:self selector:sel data: a],
nil
];

CCAction *actionB = [CCSequence actions:
[CCMoveTo actionWithDuration:kMoveTileTime position:[a pixPosition]],
[CCCallFuncND actionWithTarget:self selector:sel data: b],
nil
];
[a.sprite runAction:actionA];
[b.sprite runAction:actionB];

[a trade:b];
}

-(void) check: (id) sender data: (id) data{

}
@end

There are three main methods to this class - The "init" method where we initialize the box class - The "ccTouchesBegan" where we determine the tile that was selected and if "selectedTile" already has a value then we determine if a new tile has been selected that is next to the tile... If this is the case, we do some fancy animations that show the two swapping in the "changeWithTileA" method using a CCSequence and CCMoveTo... In fact to make this work all we needed was the [a trade b], but what's the fun in that...

Well I hope you enjoyed this one as much as I enjoyed writing it... Until next time... Seeeeeeeeeeeeeeeya!

Source code to this tutorials can be downloaded here: Slide Image Game Tutorial

Updated code for XCode 3.2.5 and iOS 4.2 here: Slide Image Game Tutorial UPDATED

没有评论:

发表评论