移动目标
下面我们需要在场景中添加一些目标让忍者去打击。为了让事情变的更有趣一些,我们要让这些目标移动起来-要不然没什么挑战性。我们在稍稍偏屏幕右边的地方创建一些目标,并为它们建立动作来让它们向左移动。
在init方法之前添加下面的方法:
-(void)addTarget {
CCSprite *target = [CCSprite spriteWithFile:@"Target.png"}
rect:CGRectMake(0, 0, 27, 40)];
// Determine where to spawn the target along the Y axis
CGSize winSize = [[CCDirector sharedDirector] winSize];
int minY = target.contentSize.height/2;
int maxY = winSize.height - target.contentSize.height/2;
int rangeY = maxY - minY;
int actualY = (arc4random() % rangeY) + minY;
// Create the target slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
target.position = *****(winSize.width + (target.contentSize.width/2), actualY);
[self addChild:target];
// Determine speed of the target
int minDuration = 2.0;
int maxDuration = 4.0;
int rangeDuration = maxDuration - minDuration;
int actualDuration = (arc4random() % rangeDuration) + minDuration;
// Create the actions
id actionMove = [CCMoveTo actionWithDuration:actualDuration
position:*****(-target.contentSize.width/2, actualY)];
id actionMoveDone = [CCCallFuncN actionWithTarget:self
selector:@selector(spriteMoveFinished:)];
[target runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
在这里我以一种详细的方式来阐明事情以便让事情更容易理解。第一部分我们应该理解目前已经讨论了:我们做一些简单的计算,以确定我们要创建对象,设置对象的位置,并把它以添加玩家精灵的相同方式添加到场景中去。
这里边的新元素是添加动作。Cocos2D提供了很多非常方便内置的行动可以用来制作动画的行动,如移动,跳跃的行动,褪色的行动,动画动作及更多。在这里我们为目标使用了三项动作。
•CCMoveTo:我们使用CCMoveTo动作来指导物体屏幕左边。请注意,我们可以指定运动应采取的持续时间,在这里,我们采用2-4秒的随机速度。
•CCCallFuncN: 该CCCallFuncN函数允许我们指定一个回调到我们的对象出现时,执行操作。我们正在指定一个回调称为"spriteMoveFinished”我们还没有写呢 - 更多如下
•CCSequence: 该CCSequence动作让我们创建一系列的动作,一次一个。这样,我们可以先执行CCMoveTo动作,一旦完成执行CCCallFuncN动作。
下面,添加前面我们已经在CCCallFuncN动作中已经提过的回调函数。你可以在addTarget函数前面添加:
-(void)spriteMoveFinished:(id)sender {
CCSprite *sprite = (CCSprite *)sender;
[self removeChild:sprite cleanup:YES];
}
该函数的目的是从场景中移除精灵,一旦该精灵离开屏幕。这一点很重要,这样我们不会随着时间的推移,有许许多多的无用精灵在场景之外而内存泄漏。请注意,还有其他(更好)的方式来解决这个问题诸如具有可重复使用Sprite的数组,但这个初级教程,我们正在采取简单的方法。
最后一件事情在我们运行程序前。我们需要实际调用的方法来创建目标!为了让事情更有趣点,我们让目标随着时间的推移持续大量的出现。我们可以在Cocos2D中通过安排一个回调函数的定期调用来完成这个任务。每1秒执行一次。因此,在init函数返回之前调用下面的函数调用。
self schedule:@selector(gameLogic:) interval:1.0];
现在像下面这样简单的实现这个回调函数:
-(void)gameLogic:(ccTime)dt {
[self addTarget];
}
就是这样!所以,现在,如果你编译并运行该项目,现在你应该看到目标愉快地在屏幕上移动:
因为我们已经让图层支持触摸,现在我们可以收到触摸事件的回调。因此,让我们实现ccTouchesEnded方法,只要用户完成了接触该方法就会调用,具体如下:
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Choose one of the touches to work with
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:[touch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
// Set up initial location of projectile
CGSize winSize = [[CCDirector sharedDirector] winSize];
CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png"
rect:CGRectMake(0, 0, 20, 20)];
projectile.position = *****(20, winSize.height/2);
// Determine offset of location to projectile
int offX = location.x - projectile.position.x;
int offY = location.y - projectile.position.y;
// Bail out if we are shooting down or backwards
if (offX <= 0) return;
// Ok to add now - we've double checked position
[self addChild:projectile];
// Determine where we wish to shoot the projectile to
int realX = winSize.width + (projectile.contentSize.width/2);
float ratio = (float) offY / (float) offX;
int realY = (realX * ratio) + projectile.position.y;
CGPoint realDest = *****(realX, realY);
// Determine the length of how far we're shooting
int offRealX = realX - projectile.position.x;
int offRealY = realY - projectile.position.y;
float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));
float velocity = 480/1; // 480pixels/1sec
float realMoveDuration = length/velocity;
// Move projectile to actual endpoint
[projectile runAction:[CCSequence actions:
[CCMoveTo actionWithDuration:realMoveDuration position:realDest],
[CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)], nil]];
}
在第一部分,我们选择一个触摸点来使用,先得到在当前视图中的位置,然后调用convertToGL来把坐标转化到当前的布局。这点很重要,因为我们现在是横向模式。
接下来我们加载子弹精灵并像往常一样设置初始坐标。然后,我们确定我们希望子弹移动的位置,按照前面描述的算法,使用玩家和触摸点之间的联系来作为指导载体。
请注意,该算法并不理想。我们正在迫使子弹继续前进直到在X方向上离开屏幕-即使首先在Y方向上已经离开了屏幕!有不同的方法来解决这个,包括检查离开屏幕的最小长度,让游戏的逻辑回调核查离开屏幕的子弹和消除回调,而不是使用回调方法,等等。但这个初级教程,我们将保持原样。
我们最需要做的就是确定为运动时间。我们希望,子弹会于一个恒定的速率按照射击方向前进,所以我们再次做一些数学。我们可以找出利用勾股定理来算出移动的距离。记得在几何学里边,这是规则,指出了一个直角三角形的斜边长度等于两直角边的平方的和的开方。
一旦我们有了距离,我们只是除以速度,以获得所需的时间。这是因为速度=距离/时间,或换句话说 时间=距离/速度。
剩下的事情就是设置动作,就想给目标设置动作一样。编译并运行,现在你的忍者可以向前来的一大群目标开火了 !
[_targets removeObject:sprite];
} else if (sprite.tag == 2) { // projectile
[_projectiles removeObject:sprite]; }
编译并运行该项目以确保一切正常。在这个时候应该没有什么明显的不同,但现在我们有标记,我们要实现碰撞检测。
现在在HelloWorldScene类中添加下面的方法:
- (void)update:(ccTime)dt {
NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];
for (CCSprite *projectile in _projectiles) {
CGRect projectileRect = CGRectMake(
projectile.position.x - (projectile.contentSize.width/2),
projectile.position.y - (projectile.contentSize.height/2),
projectile.contentSize.width,
projectile.contentSize.height);
NSMutableArray *targetsToDelete = [[NSMutableArray alloc] init];
for (CCSprite *target in _targets) {
CGRect targetRect = CGRectMake(}
target.position.x - (target.contentSize.width/2),
target.position.y - (target.contentSize.height/2),
target.contentSize.width,
target.contentSize.height);
if (CGRectIntersectsRect(projectileRect, targetRect)) {
[targetsToDelete addObject:target];
}
for (CCSprite *target in targetsToDelete) {
[_targets removeObject:target];}
[self removeChild:target cleanup:YES];
if (targetsToDelete.count > 0) {
[projectilesToDelete addObject:projectile];}
[targetsToDelete release];
}
for (CCSprite *projectile in projectilesToDelete) {
[_projectiles removeObject:projectile];}
[self removeChild:projectile cleanup:YES];
[projectilesToDelete release];
}
以上应该很清楚了。我们只是通过子弹和目标数组,按照它们的边界框创建相应的矩形,并使用CGRectIntersectsRect方法来检查交叉。如果发现有,我们从场景和数组中把它们移除。请注意,我们是把这些对象添加到一个toDelete数组中,因为你不能在一个正在迭代的数组中删除一个对象。同样,有更多的最佳方法来实现这种事情,但我采用了这个简单的方法
在你准备要运行前你只需要做一件事-通过添加下面的代码到init方法中去安排上面的方法尽可能多的运行
[self schedule:@selector(update:)];
让它编译并运行,现在当你的子弹和目标碰撞时它们就会消失!
最后的润色
我们非常接近拥有一个可行的(但非常简单)的游戏了。我们只需要添加一些声音效果和音乐(因为什么类型的游戏没有音乐的!)和一些简单的游戏逻辑。
如果您一直关注我的blog series on audio programming for the iPhone
- (void)setNumberOfLoops:(NSInteger)theNumberOfLoops { numberOfLoops = theNumberOfLoops; audioSourcePlayer.numberOfLoops = theNumberOfLoops; }
在你的ccTouchesEnded方法中播放下面的声音效果:
[[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];
现在,让我们创建一个新的场景,将作为我们的“你赢了”,或“你输”的指示。点击Classes文件夹,进入File\New File,并选择Objective-C class,并确定了NSObject类被选中。单击Next,然后输入GameOverScene作为文件名,并确保“Also create GameOverScene.h”被选中。
然后用下面的代码来代替GameOverScene.h中的内容:
#import "cocos2d.h"
@interface GameOverLayer : CCColorLayer {
CCLabel *_label;
}
@property (nonatomic, retain) CCLabel *label;
@end
@interface GameOverScene : CCScene {
GameOverLayer *_layer;
}
@property (nonatomic, retain) GameOverLayer *layer;
@end
再用下面的代码来代替GameOverScene.m中的内容
#import "GameOverScene.h"
#import "HelloWorldScene.h"
@implementation GameOverScene
@synthesize layer = _layer;
- (id)init {
if ((self = [super init])) {
self.layer = [GameOverLayer node];
[self addChild:_layer];
}
return self;
}
- (void)dealloc {
[_layer release];
_layer = nil;
[super dealloc];
}
@end
@implementation GameOverLayer
@synthesize label = _label;
-(id) init {
if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {
CGSize winSize = [[CCDirector sharedDirector] winSize];
self.label = [CCLabel labelWithString:@"" fontName:@"Arial" fontSize:32];
_label.color = ccc3(0,0,0);
_label.position = *****(winSize.width/2, winSize.height/2);
[self addChild:_label];
[self runAction:[CCSequence actions:
[CCDelayTime actionWithDuration:3],
[CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)],
nil]];
}
return self;
}
- (void)gameOverDone {
[[CCDirector sharedDirector] replaceScene:[HelloWorld scene]];
}
- (void)dealloc {
[_label release];
_label = nil;
[super dealloc];
}
@end
请注意,这里有两个不同的对象:一个场景(scene)和一个图层(layer)。场景可以包含多个图层,尽管在这个例子是它只有一个。图层里边只在屏幕中心放了一个标签,并安排了一个3秒中的过渡,然后返回到HelloWorld场景中。
最后,让我们添加一些非常基本的游戏逻辑。首先,让我们跟踪玩家破坏的目标。添加一个成员变量到您的HelloWorldScene.h中 HelloWorld类如下:
int _projectilesDestroyed;
在HelloWorldScene.m中,添加GameOverScene类的声明:
#import "GameOverScene.h"
在update方法中removeChile:target:后面的targetsToDelete循环中增加计数并检查获胜条件
_projectilesDestroyed++;
if (_projectilesDestroyed > 30) {
GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Win!"];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
}
最后我们这样来规定,即使只有一个目标过去了,你就输了。修改spriteMoveFinished方法,在removeChild:sprite:方法的后面的tag == 1条件里边添加下面的代码:
GameOverScene *gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString:@"You Lose :["];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
继续编译并运行该项目,这样你有了羸和输的判断条件并会在适当的时候看到游戏结束的场景。