显示标签为“Tiled Maps”的博文。显示所有博文
显示标签为“Tiled Maps”的博文。显示所有博文

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

2011年10月16日星期日

Cocos2D Game Tutorial – RPG Style Game for the iPhone – Part 2: Tiled Maps

Cocos2D Game Tutorial – RPG Style Game for the iPhone – Part 2: Tiled Maps:
Update: Made some modifications to the source code – changed the title music (Hope you enjoy a remix of Dragon Warrior) and expanded the map to give a better idea of how you can use the space. Also, I added grid lines to the tiles so it didn’t looks so much like a painting. More parts to come!

It’s been a while since part 1 – we know from your comments that it’s not your favorite thing in the world (it’s not ours either) but we’re gonna try to get going again!

That being said, if you haven’t read part 1 of the tutorial, or the intro describing what we’re working towards, now’s your chance to play catch-up before plunging forward into the next piece of unknown – tile based maps using Tiled!

As will be the case for the rest of the tutorial, we’re using the Cocos2D framework for game development, which already includes support for the TMX file format (we’ll get to that later – just know that it makes our lives easier!). To build the map, we’re going to be using Tiled Qt, a fantastic piece of software that makes creating maps a piece of cake!

Source code after the break!



Download the source for this project from here!

I’ve gone ahead and made what I believe to be some of the finest tiles available. Feel free to use these tiles in any way you see fit (but for the sake of artists everywhere please don’t).

Awesome Tiles
One if by land, Two if by sea, Three if by tree, Four if by...mountain

Be consistent in your spacing – whether its 1 pixel, 0 pixels, or 100 pixels. Tiled needs to know how much space is between each tile when reading in your graphic, otherwise when the map is drawn it will look wrong. The same applies to the margin around the sides. In my case, I’ve used a 1px margin and 1px spacing.

Also I’ve made my tiles 44px by 44px, so they’re large enough to see the wonderful detail on them – how big you make your tiles is completely up to you. If you’re programming for iPhone, just remember that you have a smaller space to deal with and you probably don’t want your user to have to constantly be scrolling around – 44×44 gets you about 11 tiles horizontally, and a little more than 7 vertical. For the purposes of this tutorial, I’ve made a really simple map with 15×15 tiles. Let’s get you started on designing your own map first!

Compression Setting First things first – the default compression that Tiled is set to (at least on the Mac version) is incompatible with Cocos2d. So click on the Tiled menu and select “Preferences”. Then in the drop down select “Base 64 (gzip compressed)” – you do NOT want the zlib compressed version. Click on the pic on the left to see the details if necessary.

Next, go to File->New, and then put in the parameters for your new map. You want the orientation set to “Orthogonal”, and the map size and tile size set to whatever works for you. For your map to work with this project, you’ll want to rename the tile layer from “Tile Layer 1″ to “Background”. Once you’re set there, go to Map->New Tileset, give it a name, choose your tile sheet, then set the height and width, and margin and spacing. Once you’ve done all that, you’ll see the Tilesets section of Tiled (generally the bottom right) become populated with your tiles. Now you’re ready to start drawing!

You can use the Stamp Brush tool Stamp Brush to draw individual squares, or use the Bucket Fill tool Bucket Fill Tool to fill a large portion. Either way, you draw by clicking on the tile you want to use in the Tilesets section, and then click to fill a square or a section. Whenever you’re done, save your map as “Level1″ to the Resources directory of your Xcode project, and make sure you copy or move your tile sheet file in there also, and then add them both in the actual project.

Enough Tiled – time for some code!

We only had to add one class for this tutorial, Map.h/m. The Map class simply loads up the TMX file you created and adds it to the screen, and uses the touch detection methods to pan the screen around. Also we made a static variable to use the Singleton design pattern so that it can be accessed from anywhere (this comes in handy later on). Here’s the code where we load up the map:

Map.h

- (void) loadLevel:(int)levelNum
{
NSString * fileName = [NSString stringWithFormat:@"Level%i.tmx", levelNum];
self.anchorPoint = ccp(0,0);
self.position = ccp(0,0);
self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:fileName];
self.background = [self.tileMap layerNamed:@"Background"];

[self addChild:self.tileMap z:-1];
}

As you can see, the tile layer we renamed to “Background” in Tiled is what we are referencing with the “layerNamed” function. Like I mentioned, the rest of the code in the Map class is mainly for moving the map around – the “boundLayerPos” function makes sure we can’t scroll our Map past its edges.

The only other change we make is in PlayLayer. In the header file, we add:

#import "Map.h"

and in PlayLayer.m we update the init function:

- (id) init
{
if ((self = [super init])) {
Map *m = [Map getInstance];
[m loadLevel:1];
[self addChild:m];
}

return self;
}

And that’s it! Now we have our menu system that loads up our tile based map when you start the game. Part 3 to come as soon as we can get it out!