(译)如何使用cocos2d制作一个滑动图片游戏(Slide Image Game):第一部分

来源:互联网 时间:1970-01-01

原文链接地址:http://www.iphonegametutorials.com/2010/09/23/cocos2d-game-tutorial-building-a-slide-image-game/

教程截图:

这是一张完整的图片:

这是一张打乱了的图片:

PS:为了方便大家更好地交流学习cocos2d,一位热心的网友组建了一个ios游戏开发交流群,群号166128264。以后大家有问题可以去群里面讨论。想加入翻译工作的朋友也可以加群,以后有教程更新和翻译相关内容,都会在群里面讨论。另外,我翻译这些教程的目的,是希望和大家更好地交流学习心得,帮助新手更好地入门。其实游戏开发,我还是推荐大家看E文,因为大部分资料都是E文的,不可能全部有人替你翻译。

已经有一周时间没有出教程了,在这篇新的教程中,我们将征服一个新的游戏---滑动图片游戏。你肯定知道这是一个什么样的游戏,玩家的任务就是先把一张图片打乱,然后再把打乱的图片拼好。(当然,教程结束后,诸位可以改进,用程序把图片打乱,然后再让玩家直接还原图片,最好弄个完整的图片作参考啊!^^)制作这种类型的游戏,最大的好处就是可以为我们接下来学习和理解Tiled Map游戏做铺垫。(不过事实正好相反,我先翻译了Tiled Map的游戏教程,不过也没关系)

那么,为了制作一个这样的游戏,我们需要做哪些事情呢?下面就是制作这个滑动图片游戏的步骤列表:

  1. 创建一个“Tile”类,它包含sprite,position(x,y)和value这些实例变量。
  2. 创建一个管理类,它负责创建所有的Tile,同时可以追踪所有的Tile的状态。
  3. 添加touch组件,这样玩家可以交换两个tile的位置。
  4. 添加一些代码来随机加载图片,这样游戏可以有更多的花样。

就这么多,当我把步骤写出来的时候,是不是觉得很简单?(译者:补充一点,看我教程的朋友,不要仅仅局限于具体的技术细节,要多想想游戏创作的步骤,这个游戏有4步,你的游戏需要几步呢?多思考,这样就不会看着教程能做,没教程就无从下手了)接下来,我会一步步实现所有的功能,你会发现其实真的也很简单。

  这里有第二部分,也就是本教程完整源代码,你们可以先下下来,看看什么效果。

这里是这篇教程的源代码。

讲了那么多,可能你还不知道我们要做一个什么样的游戏,下载完整源代码,可以帮助大家都本系列教程将要完成的产品有一个完整的认识,这很重要。。。

说真的,这个简单的游戏例子,能给你很多启发。。。今后,我将会基于这个系列的教程,给你们介绍更多的这种类型的游戏(译者:比如拼图啦,华容道啦,推箱子啦,连连看啦)

不管怎么说,先让我们实现这个游戏再说!为了完成了这个艰巨的任务,我们需要一些辅助类。这些类是Tile.h, Tile.m, Box.h, Box.m。

如果你看了我博客上其它教程的话,你肯定已经知道“SceneManager”和"PlayLayer“类了,这里就不再啰嗦了。如果还没有的话,请参考《cocos2d菜单教程》和《cocos2d精灵教程》。

首先,先让我们看看Tile类的实现。

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

tile类主要就是代表从一张大图里面提取出来的一小块内容,有X,Y位置(注意,这里的x,y坐标不等于精灵的位置坐标,精灵的位置坐标是sprite.position),有精灵,有value。这里的value可以是任何值 ,比如可以代表每个tile在原图中的位置(这样的话,我们就可以用这些位置来判断玩家是否正确拼成完整的图了),在这个版本中,我们暂时不会使用这个value;

现在,让我们看看其具体实现:

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

大部分内容一看就能明白--我们实现了四个方法 “initWithX”, “nearTile”, “trade” and “pixPosition”.

“initWithX”方法,从名字就可以看出来它是做什么的---它是Tile类的初始化代码,它Tile类被初始化的时候被调用。它接收一个x,y值,这两个值和世界坐标无关,而是与包含它们的Box类有关。举个例子,我们使用 initWithX:3 Y:4,假如我们的box是7*7的话,那么,这个Tile被放置在3,4号位置)我们可以用pixPosition函数来计算每个Tile的精灵在屏幕上的位置。

“nearTile”接收一个Tile类型的参数,判断两个Tile是否是邻居,如果是,就返回YES,否则返回NO。

“trade”就是把两个Tile的精灵交换一下。交换两个变量,相信学过C语言的都会,定义一个临时变量temp,然后temp = a; a=b; b=temp;

最后,“pixPosition”计算得到每个Tile的精灵在屏幕上的正确的坐标位置---你将会在后面看到这个函数的特殊用途。

     Box类的主要功能就是处理所有单个Tile类的创建,加载相应精灵,以及把它们放置在屏幕上的正确位置。

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

单看头文件,有些内容你也可以猜到它的作用了。。。size就是我们将要创建的网格的大小(3*3, 4*5, 5*3,7*7等等)

Box类最主要的两个变量就是“content”和“readyToRemove”。

content变量实际上是一个多维数组,至少也是一维(如果SIZE为1*1的话)。我们将创建一个NSMutableArray代码列,然后会在每一列中再加入一个NSMutableArray作为一行。我们可以使用 “return [[content objectAtIndex: y] objectAtIndex: x];”来得到正确的Tile。

readyToRemove变量,在这个教程中,只是初始化了,但是,今后,我会介绍另一个游戏,在那里面我会大量使用这个变量----在这个教程中,我将使用它加载所有新创建的精灵。

接下来,让我们看看Box类的具体实现:

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

那么,这个类到底做了些什么事呢---我认为"initWithSize" 和 "getObjectAt" 这两个方法已经很清楚了。。这里就不再啰嗦了。

因此,最重要的方法---check。首先,我们判断readyToRemoveTiles数组中是否包含任何Tiles。。在我们这个例子中,所有的Tile都在里面。。。非常好!然后我们遍历这个数组里面所有的元素,把其中的精灵一个个全部清除掉,最后,我们把整个数组里的元素全部清空。现在,你可能知道了,当制作一个游戏的多个关卡的时候,如何做好清理化工作了。

接下来,我们从资源文件中加载纹理(比如1.png, 2.png, 3.png等等),然后把它们存储在一个CCTexture2D对象中,之后我们会从CCTexture2D对象来构建所有的精灵帧。我们将创建49个精灵帧。因为我们的图片大小是280*280,所以每个CCSpriteFrame大小就是40*40。这里面用了一个双重for循环。

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) {
}
}

上面这部分代码,对于本教程来说,其实并不是必须的,但是,它可以用来判断有多少Tile需要替换图片。这种机制非常好,尤其是当你制作一个Tile drop游戏(比如宝石迷阵)的时候,因为你不想一次替换掉所有的Tile。。。在本例中,我们将检测Tile的value属性是否为0,然后用extension变量来追踪有多少个value为0的Tile。在本例中,由于所有的Tile都存在了readyToRemoveTiles中,所以extension变量永远都是7。

下面,我们得到第一个Y坐标为(kBoxHeight-extension+i)的Tile。。因为,在我们的例子中, kBoxHeight = 7,而extension总是为7,所以,我们实际上只需要关心变量i就可以了。i会从0一直递增到6.好了,你可能会问,为什么我要这样?因为,我会在其实游戏中使用到,如果现在我们就熟悉了的话,以后的工作会很轻松:)。接下来,看看精灵是如何工作的吧。。。

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

然后,我们创建了新的CCSprite,通过给定的CCSpriteFrame,并且把精灵放置在box的合适的位置。我们把所有的Tile的imgValue都设置成一样的。

最后,看看PlayLayer类,我们应该很熟悉了。。。它将会处理touches,以及box类的初始化工作。我们需要追踪前面两次touch--“selectedTile”变量指代玩家当前选择的Tile,这时候,如果玩家选择另外一个Tile的时候,就会和前面的Tile进行交换。接下来,看看具体实现吧。

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

接下来是其实现:

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

这个类中主要有3个方法---init方法用来初始化box类,ccTouchesBegan方法,决定哪个Tile被用户选择,如果新选择的Tile等于原Tile的话就直接返回;如果是邻近的Tile的话,就交换;否则,就不做任何事情。交换Tile的时候,调用CCSequence 和 CCMoveTo来展示交换的动画。实际上,我们真正交换的代码只有[a trade b]。

希望你们喜欢这个教程,如果有任何疑问,请在下方留言。

  下篇教程见!





相关阅读:
Top