免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文链接地址:http://www.raywenderlich.com/2593/how-to-create-a-mole-whacking-game-with-cocos2d-part-2
教程截图:
这篇文章是《如何使用cocos2d来制作一个打地鼠的游戏》的第二部分。打地鼠系列教程,里面用到的很多概念和方法是从这个博客的其它教程中拿来的,但是,同时,本系列教程还引入了一些新的概念。
在第一部分教程中,我们创建了一个游戏的基本框架--让可爱地地鼠从洞里面钻出来。我们花费了大量时间来讨论如何规划图片资源及其坐标,以便可以开发出一个游戏,让它同时能够在iPhone、iPad和Retina display的设备上运行--并且要保证尽可能地高效率!
在这篇教程中,我们将会增加一些很酷的动画效果,比如地鼠大笑和被打中时的动画。同时,会增加一些游戏逻辑,以便你能够打击地鼠并且获得相应的分数,当然,还会添加一些非常好听的音乐和音效。
如果你还没有上一个教程的工程,可以从这里下载一份工程拷贝。
定义动画:实用性
为了使游戏变得更有趣,我们将给地鼠增加两个动画。首先,当它从洞里钻出来的时候,它会笑一下(那笑声你绝对会忍不住想打它!)。然后,如果你打中它了,那么你会看到地鼠被打中时的面部表情。
但是,在我们开始之前,先讨论一下代码中如何组织动画。
回想我们之前的教程《如何在cocos2d里面使用spritesheet和动画》,其中,在创建动画过程中,有一个步骤是,创建一系列的精灵帧(sprite frames)。因此,对于你的动画效果中的每一张不同的图片,你必须为之增加精灵帧,如下所示:
[animFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
@"
myImage.jpg
"
]];
我们的地鼠笑的动画将会是下面的一些图片序列: mole_laugh1.jpg, mole_laugh2.jpg mole_laugh3.jpg, mole_laugh2.jpg, mole_laugh3.jpg, mole_laugh1.jpg.
因此,我们可以硬编码来建立动画,如下所示:
[animFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
@"
mole_laugh1.jpg
"
]];
[animFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
@"
mole_laugh2.jpg
"
]];
[animFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
@"
mole_laugh3.jpg
"
]];
[animFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
@"
mole_laugh2.jpg
"
]];
//
And so on...
但是,那会使我们的代码变得非常难看。为了使代码变得更简洁,我们不是直接在代码里面定义这些图片,取而代之的是,我们把它们的名字都存到一个plist文件中。
Property List文件
如果你之前没有使用过plist文件,你需要知道,其实就是一种xml格式的文件。只不过是,它的后缀不一样,同时,它能够被Xcode直接识别,并且可以方便地存储数组,字典、字符串和数字等等。创建这种类型的文件非常方便,当然使用也一样很方便。
让我们看一下,它在Xcode里面是什么样子。右键点击Resources,选择“Add\New File...”,再选择 “Mac OS X\Resource\Property List”,再点击“Next”。把这个文件命名为“laughAnim.plist”,然后点Finish。这时,属性列表文件laughAnim.plist文件的结构应该如下图所示:
每一个属性列表文件(plist文件)有一个根元素。这个根元素要么是一个数组或者一个字典。这里的plist文件将会包含一个组成地鼠笑的动画的一系列图片名字的数组。因此,点击根元素的第二列,(当前默认是Dictionary),我们把它更改成Array。
接下来,点击最右边的小按钮(有三行的那个按钮)--这样会往数组里面增加一个新的实体。默认情况下,这个新添加的实体的类型是String--那也正是我们想要的数据类型。把Item0的名称改成“mole_laugh1.jpg”。
然后再点+号来添加更多的行,重复这个过程,最终的结果如下图所示:
接下来,重复上述的过程来创建地鼠被打中时的动画属性列表文件。按上面所说的,创建一个hitAnim.plist文件,然后把它建立成下图所示的结构:
现在,是时候添加一些代码来加载这些动画了。打开HelloWorldScene.h文件,然后为每一个动画定义一个成员变量,如下所示:
//
Inside @interface HelloWorld
CCAnimation
*
laughAnim;
CCAnimation
*
hitAnim;
这样做的目的主要是重用,因为可以在init函数里面初使化好这些动画效果,那么在其它的地方就直接可以使用这些动画效果了。(这里需要记住的一点是,游戏里面的任何对象都要事先分配好,在玩家玩游戏的过程中,只需要按照某种规则把它们拿出来即可)。
接下来,基于先前创建的plist文件来创建CCAnimation,如下所示:
-
(CCAnimation
*
)animationFromPlist:(NSString
*
)animPlist delay:(
float
)delay {
NSString
*
plistPath
=
[[NSBundle mainBundle] pathForResource:animPlist ofType:
@"
plist
"
];
//
1
NSArray
*
animImages
=
[NSArray arrayWithContentsOfFile:plistPath];
//
2
NSMutableArray
*
animFrames
=
[NSMutableArray array];
//
3
for
(NSString
*
animImage
in
animImages) {
//
4
[animFrames addObject:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:animImage]];
//
5
}
return
[CCAnimation animationWithFrames:animFrames delay:delay];
//
6
}
理解这个方法非常重要,所以让我们一行一行代码来看。
- 属性列表文件包含在工程当中,因此,它也在应用程序的“main bundle”中。这个方法会返回main bundle中的文件的完整路径,也就是我们需要读取的plist文件的完整路径。
- 为了读取一个plist文件,我们调用NSArray的arrayWithContentsOfFile方法,然后把plist文件的完整路径传递给它。这样就会把plist文件中的内容初使化成一个数组。(本例中,这个数组就是一系列图片名字的数组)。注意,这个方法可行,是因为我们把根元素设置成了NSArray。如果我们把它设置成NSDictionary的话,那么我们就要用一个NSDictionary去初使化它。具体的方法就是调用 [NSDictionary dictionaryWithContentsOfFile...] 。
- 创建一个空的数组来存储这些动画帧。
- 从plist文件中循环遍历每一张图片并把它存到一个数组中去。
- 为每一张图片创建一个精灵帧,同时把它加到 animFrames数组里面去。
- 基于一个精灵帧数组,返回一个CCAnimation对象。
接下来,在init方法的末尾为每一个动画调用这个辅助函数来创建相应的动画:
laughAnim
=
[self animationFromPlist:
@"
laughAnim
"
delay:
0.1
];
hitAnim
=
[self animationFromPlist:
@"
hitAnim
"
delay:
0.02
];
[[CCAnimationCache sharedAnimationCache] addAnimation:laughAnim name:
@"
laughAnim
"
];
[[CCAnimationCache sharedAnimationCache] addAnimation:hitAnim name:
@"
hitAnim
"
];
注意,在存储动画对象的引用之后,我们把它们加入到了动画缓存中(animation cache)。这个非常重要,因为我们可以在其他地方很容易地使用引用。(对于laughAnim和hitAnim,不用retain就可以使用了。因为,加入到动画缓存中的时候,CCAnimationCache已经帮你ratain了)。这样做还有一个好处就是,你可以通过CCAnimationCache来获得你想要的动画对象引用,只需要提供动画的名字即可,因为它内部实现是采用的字典。)
最后一步--让我们来使用动画(先只使用笑的动画)。修改popMole方法,如下所示:
-
(
void
) popMole:(CCSprite
*
)mole {
CCMoveBy
*
moveUp
=
[CCMoveBy actionWithDuration:
0.2
position:ccp(
0
, mole.contentSize.height)];
CCEaseInOut
*
easeMoveUp
=
[CCEaseInOut actionWithAction:moveUp rate:
3.0
];
CCAction
*
easeMoveDown
=
[easeMoveUp reverse];
CCAnimate
*
laugh
=
[CCAnimate actionWithAnimation:laughAnim restoreOriginalFrame:YES];
[mole runAction:[CCSequence actions:easeMoveUp, laugh, easeMoveDown, nil]];
}
这里唯一的差别就是,在钻出来和钻回去的action中间,我们不是延迟几秒,取而代之的是播放地鼠笑的动画。CCAnimate action使用之前已经创建好的laughAnim,同时设置restoreOriginalFrame为yes。这样的话,当动画结束的时候,它会回到播放动画之前的面貌。
编译并运行代码,现在,当地鼠从洞里钻出来的时候,它会朝着你大笑!是不是想打它?有木有!
是时候让这些地鼠的笑容消失了,让我们开始添加打击逻辑吧!
增加游戏逻辑
现在我们将往游戏中添加一些玩法逻辑。主要就是记录有多少个地鼠钻出来过,还有就是通过打地鼠,你能得到多少分。你会尝试尽可能多地获得分数。
因此,我们将保存分数,并且显示给用户看。当地鼠钻回去的时候,我们也要告诉用户。
所以,再打开HelloWorldScene.h文件,添加下面一些实例变量到HelloWord层中:
CCLabelTTF
*
label;
int
score;
int
totalSpawns;
BOOL gameOver;
这里保存了一个分数label,当前的分数值,总共钻出来的地鼠数目,以及游戏是否结束。
接下来,在你的init方法的结尾添加下列初始化代码:
self.isTouchEnabled
=
YES;
float
margin
=
10
;
label
=
[CCLabelTTF labelWithString:
@"
Score: 0
"
fontName:
@"
Verdana
"
fontSize:[self convertFontSize:
14.0
]];
label.anchorPoint
=
ccp(
1
,
0
);
label.position
=
ccp(winSize.width
-
margin, margin);
[self addChild:label z:
10
];
首先,设置层能够接收到touch事件,因为你想检查用户击打屏幕的消息。然后创建一个label来显示分数。注意,这里把label的锚点设置成右下角,那样可以非常方便地把它放置在屏幕的右下方。
你也要注意到,我们并不是直接传递字体大小,而是通过一个辅助函数来决定字体的大小。这是因为,在iPad上面,字体应该大一些,因为它的屏幕大一些。所以要实现一个convertFontSize方法,如下所示:
-
(
float
)convertFontSize:(
float
)fontSize {
if
(UI_USER_INTERFACE_IDIOM()
==
UIUserInterfaceIdiomPad) {
return
fontSize
*
2
;
}
else
{
return
fontSize;
}
}
这个非常简单--在iPad上运行的时候,字体大小加倍,否则,就不变。
接下一,我们将添加touch检测代码,来检测用户是否击中一个地鼠。但是,在这之前,我们需要添加一个标记,标记地鼠是否可以击打。因为地鼠应该只有在它朝着你笑的时候才能够被击打,而在它笑完钻回去的时候,你是不能够击打它的。
我们可以创建CCSprite的一个子类来做这个事,但是,因为我们只需要存储一点点信息,所以,我们只需要使用CCSprite的userData属性即可。因此,添加两个辅助方法,并且修改popMole方法,如下所示:
-
(
void
)setTappable:(
id
)sender {
CCSprite
*
mole
=
(CCSprite
*
)sender;
[mole setUserData:TRUE];
}
-
(
void
)unsetTappable:(
id
)sender {
CCSprite
*
mole
=
(CCSprite
*
)sender;
[mole setUserData:FALSE];
}
-
(
void
) popMole:(CCSprite
*
)mole {
if
(totalSpawns
>
50
)
return
;
totalSpawns
++
;
[mole setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
@"
mole_1.jpg
"
]];
//
Pop mole
CCMoveBy
*
moveUp
=
[CCMoveBy actionWithDuration:
0.2
position:ccp(
0
, mole.contentSize.height)];
CCCallFunc
*
setTappable
=
[CCCallFuncN actionWithTarget:self selector:@selector(setTappable:)];
CCEaseInOut
*
easeMoveUp
=
[CCEaseInOut actionWithAction:moveUp rate:
3.0
];
CCAnimate
*
laugh
=
[CCAnimate actionWithAnimation:laughAnim restoreOriginalFrame:YES];
CCCallFunc
*
unsetTappable
=
[CCCallFuncN actionWithTarget:self selector:@selector(unsetTappable:)];
CCAction
*
easeMoveDown
=
[easeMoveUp reverse];
[mole runAction:[CCSequence actions:easeMoveUp, setTappable, laugh, unsetTappable, easeMoveDown, nil]];
}
popMole方法做了如下一些变动:
- 在地鼠大笑之前,它通过运行一个CCCallFunc action来调用一个方法setTappable。这个方法会把精灵的userData属性设置成True,表明当前地鼠是可以被击打的。
- 类似的,在地鼠笑完之后,同样运行一个CCCallFunc action来调用unsetTappable方法,把是否可击打的标记又设置回去。
- 只要超过50个地鼠从洞里钻出来后,这个方法就返回,因此,这个游戏的限制就是只出现50个地鼠。
- 在这个方法的开始部分,还把精灵的显示帧设置成初使图片(“mole.jpg”),因为,如果地鼠上一次被打中了,它下次再钻出来的时候,还会显示被打中。所以需要在它每次从洞里钻出来的时候,设置它的显示帧为初使图片。
- 好了,现在,这个精灵有一个userData标记,可以表明当前它是否可以被击打了。我们接下来,添加下面的击打检测代码:
-
(
void
) registerWithTouchDispatcher
{
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:kCCMenuTouchPriority swallowsTouches:NO];
}
-
(BOOL) ccTouchBegan:(UITouch
*
)touch withEvent:(UIEvent
*
)
event
{
CGPoint touchLocation
=
[self convertTouchToNodeSpace:touch];
for
(CCSprite
*
mole
in
moles) {
if
(mole.userData
==
FALSE)
continue
;
if
(CGRectContainsPoint(mole.boundingBox, touchLocation)) {
mole.userData
=
FALSE;
score
+=
10
;
[mole stopAllActions];
CCAnimate
*
hit
=
[CCAnimate actionWithAnimation:hitAnim restoreOriginalFrame:NO];
CCMoveBy
*
moveDown
=
[CCMoveBy actionWithDuration:
0.2
position:ccp(
0
,
-
mole.contentSize.height)];
CCEaseInOut
*
easeMoveDown
=
[CCEaseInOut actionWithAction:moveDown rate:
3.0
];
[mole runAction:[CCSequence actions:hit, easeMoveDown, nil]];
}
}
return
TRUE;
}
这个registerWithTouchDispatcher方法会使得每一个touch事件到来的时候,都会先调用ccTouchBegan方法,如果ccTouchBegan返回yes,则有touch事件,否则没有。对于更多的细节信息,请查照第一篇教程《如何使用cocos2d来制作一个基于tiled地图的游戏》。
ccTouchBegan方法把touch坐标转换成相对于层的本地坐标,然后循环遍历每一个地鼠。如果地鼠不可以击打(它的userData属性是false),那么就直接看下一个地鼠。否则的话,就使用CGRectContainPoint来检测touch点是否在地鼠的精灵边框之内。
如果地鼠被击中了,就把它设置成不可击打的,同时增加分数。并且停止所有正在运行的action,然后播放“被打中”的动画,并且立马把地鼠缩回洞里去。
最后一步--添加一些代码来更新分数label以及检查关卡是否完成。
if
(gameOver)
return
;
[label setString:[NSString stringWithFormat:
@"
Score: %d
"
, score]];
if
(totalSpawns
>=
50
) {
CGSize winSize
=
[CCDirector sharedDirector].winSize;
CCLabelTTF
*
goLabel
=
[CCLabelTTF labelWithString:
@"
Level Complete!
"
fontName:
@"
Verdana
"
fontSize:[self convertFontSize:
48.0
]];
goLabel.position
=
ccp(winSize.width
/
2
, winSize.height
/
2
);
goLabel.scale
=
0.1
;
[self addChild:goLabel z:
10
];
[goLabel runAction:[CCScaleTo actionWithDuration:
0.5
scale:
1.0
]];
gameOver
=
true
;
return
;
}
就这么多了!编译并运行,你现在可以尽情打地鼠赚分啦!你能得多少分呢?
免费的音效
和之前一样,让我们添加一些非常酷的音效。下载这些音效(它们是用Garage Band和Audacity制作的,这两个在iPad上面有),解压之,并把它们拖到Resource文件夹下面。同时,确保 “Copy items into destination group’s folder”被选中,再点Add。
然后,修改HelloWorldScene.m:
//
Add to top of file
#import
"
SimpleAudioEngine.h
"
//
Add at the bottom of your init method
[[SimpleAudioEngine sharedEngine] preloadEffect:
@"
laugh.caf
"
];
[[SimpleAudioEngine sharedEngine] preloadEffect:
@"
ow.caf
"
];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:
@"
whack.caf
"
loop:YES];
//
Add at bottom of setTappable
[[SimpleAudioEngine sharedEngine] playEffect:
@"
laugh.caf
"
];
//
Add inside ccTouchBegan, inside the CGRectContainsPoint case
[[SimpleAudioEngine sharedEngine] playEffect:
@"
ow.caf
"
];
编译并运行,心情享受打地鼠的乐趣吧!
何去何从
这里有本教程的完整源代码。
这个系列的教程到此基本上就结束了,为什么不往工程里添加更多的东西呢?我确定你可以往这个游戏添加一些更加好玩的元素。
如果你们有什么好的想法,或者好的建议,可以在下面留言。
译者的话:希望对大家有帮助。
著作权声明:本文由http://www.cnblogs.com/andyque翻译,欢迎转载分享。请尊重作者劳动,转载时保留该声明和作者博客链接,谢谢!