游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

如何使用Cocos2D制作简单的iPhone游戏(2)

发布时间:2012-05-09 14:47:32 Tags:,,,

作者:Ray Wenderlich

如下部分主要关于如何将炮台瞄准射击方向。这是很多游戏的必要条件——包括一类我最喜欢的游戏题材,塔防游戏。(本系列第1部分详见此处

所以下文主要谈论这一话题及在简单游戏中添加旋转炮台。

着手设置

若你有遵照上一指南的操作,那么现在你就可以轻松接下去。

接着,下载新玩家精灵和抛射体精灵图像,将它们添加至项目中,将之前的Player.jpg和Projectile.jpg从项目中删除。然后将生成精灵的代码行修改成如下内容:

// In the init method
CCSprite *player = [CCSprite spriteWithFile:@"Player2.jpg"];
// In the ccTouchesEnded method
CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile2.jpg"];

注意这次,我们无需具体设定精灵的宽度和高度,而是让Cocos2D替我们完成这些操作。

编译和运行你的项目,若运行顺利,你会看到炮塔发射子弹。但画面看起依然有些不妥,因为炮塔并没有旋转至射击方向,所以下面我们将就此进行修复。

Screenshot(from raywenderlich)

Screenshot(from raywenderlich)

旋转至射击方向

在旋转炮塔前,我们首先需要存储玩家精灵的的引用,这样我们稍后才能够进行旋转。打开HelloWorldScene.h,修改class,将如下变量纳入在内:

CCSprite *_player;

然后在init方法中修改代码,将玩家对象添加至图层中:

_player = [[CCSprite spriteWithFile:@"Player2.jpg"] retain];
_player.position = ccp(_player.contentSize.width/2, winSize.height/2);
[self addChild:_player];

最后在dealloc中添加清除代码:

[_player release];
_player = nil;

现在我们已获得一个玩家对象的引用,接着将其进行旋转!要进行旋转,我们首先需要确定旋转角度。

关于确定角度,不妨试着回想高中的三角法。是否还记得助记符SOH CAH TOA?这令我们牢记,角度的正切值=对边/邻边。下图进一步说明:

Shooting Angle Math(from raywenderlich)

Shooting Angle Math(from raywenderlich)

就如上图所示,我们想要旋转的角度=Arc(Y坐标/X坐标)。

但有两点我们需要牢记。首先,计算Arc(Y坐标/X坐标)时,所得结果是弧度,而Cocos2D处理的是角度。幸运的是,Cocos2D提供便捷的转换宏指令。

其次,虽然我们通常将上述画面的角度视作正角(约20°),但在Cocos2D旋转中,正角将呈顺时针模式,如下图所示:

Shooting Angle Cocos(from raywenderlich)

Shooting Angle Cocos(from raywenderlich)

所要要瞄准正确方向,我们需要将结果乘以-1。所以如果我们将上述图像的角度乘以-1,我们就会得到-20°,这表示逆时针旋转20°。

现在就将此放入代码中。将下述代码添加至ccTouchesEnded中,就在你调用抛射体的runAction前。

// Determine angle to face
float angleRadians = atanf((float)offRealY / (float)offRealX);
float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
float cocosAngle = -1 * angleDegrees;
_player.rotation = cocosAngle;

编译和运行项目,炮台如今应该转向射击方向。

Screenshot(from raywenderlich)

Screenshot(from raywenderlich)

旋转然后进行射击

目前一切进展顺利,但画面稍微有些奇怪,因为炮塔直接瞄准特定方向射击,而不是呈现动态流畅模式。这个问题可以解决,但需要进行稍微的代码重构。

首先,打开HelloWorldScene.h,将如下变量添加至class中:

CCSprite *_nextProjectile;

然后修改你的ccTouchesEnded,添加名为ccTouchesEnded的新方法,这样它就呈现如下样式:

- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

if (_nextProjectile != nil) return;

// 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];
_nextProjectile = [[CCSprite spriteWithFile:@"Projectile2.jpg"] retain];
_nextProjectile.position = ccp(20, winSize.height/2);

// Determine offset of location to projectile
int offX = location.x – _nextProjectile.position.x;
int offY = location.y – _nextProjectile.position.y;

// Bail out if we are shooting down or backwards
if (offX <= 0) return;

// Play a sound!
[[SimpleAudioEngine sharedEngine] playEffect:@”pew-pew-lei.caf”];

// Determine where we wish to shoot the projectile to
int realX = winSize.width + (_nextProjectile.contentSize.width/2);
float ratio = (float) offY / (float) offX;
int realY = (realX * ratio) + _nextProjectile.position.y;
CGPoint realDest = ccp(realX, realY);

// Determine the length of how far we’re shooting
int offRealX = realX – _nextProjectile.position.x;
int offRealY = realY – _nextProjectile.position.y;
float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));
float velocity = 480/1; // 480pixels/1sec
float realMoveDuration = length/velocity;

// Determine angle to face
float angleRadians = atanf((float)offRealY / (float)offRealX);
float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
float cocosAngle = -1 * angleDegrees;
float rotateSpeed = 0.5 / M_PI; // Would take 0.5 seconds to rotate 0.5 radians, or half a circle
float rotateDuration = fabs(angleRadians * rotateSpeed);
[_player runAction:[CCSequence actions:
[CCRotateTo actionWithDuration:rotateDuration angle:cocosAngle],
[CCCallFunc actionWithTarget:self selector:@selector(finishShoot)],
nil]];

// Move projectile to actual endpoint
[_nextProjectile runAction:[CCSequence actions:
[CCMoveTo actionWithDuration:realMoveDuration position:realDest],
[CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)],
nil]];

// Add to projectiles array
_nextProjectile.tag = 2;

}

- (void)finishShoot {

// Ok to add now – we’ve finished rotation!
[self addChild:_nextProjectile];
[_projectiles addObject:_nextProjectile];

// Release
[_nextProjectile release];
_nextProjectile = nil;

}

代码看起来很多,但我们做出的调整不多(游戏邦注:大多是细微的代码重构)。下列是我们所做出的调整:

* 若nextProjectile存在数值,我们会在函数开始处输出内容(通过bail()函数),这意味着我们处在射击过程中。

* 在应用本地对象前,给添加至情境中的抛射体命名。在此版本中,我们在变量nextProjectile中创建对象,但随后才进行添加。

* 我们定义预期的炮塔旋转速度,半秒旋转半圈。记住一圈有两个PI弧度。

* 所以为计算出旋转需耗费多久时间,我们将移动弧度乘以速度。

* 随后我们就开始系列操作,将炮台旋转至正确角度,然后调用函数,将抛射体添加至情境中。

所以不妨大胆进行尝试!编译和运行项目,炮塔现在应该旋转得更流畅。

现在我们拥有旋转的炮塔、射击的怪兽及优质的音效。

但我们的炮塔过于简单,怪兽只进行一次射击,关卡只有一个。

下面我们将对项目进行扩展,这样我们就能够制作出难度程度不同的各类怪兽,在游戏中植入多层次的关卡。

更棘手的怪兽

为融入更多趣味性,我们将制作两种类型的怪兽:软弱但快速的怪兽,强壮但缓慢的怪兽。

现在就来设置怪兽class。设置怪兽class的方式很多,我们将采用最简单的方式,就是将怪兽class设置成CCSprite子类。我们将创建两个怪兽子类:一个针对软弱但快速的怪兽,一个针对强壮但缓慢的怪兽。

切换至File\New File,选择Cocoa Touch Class\Objective-C类,勾选NSObject子类,然后点击Next。将文件命作Monster.m,选中“Also create Monster.h”。

然后将Monster.h替换成如下内容:

#import “cocos2d.h”

@interface Monster : CCSprite {
int _curHp;
int _minMoveDuration;
int _maxMoveDuration;
}

@property (nonatomic, assign) int hp;
@property (nonatomic, assign) int minMoveDuration;
@property (nonatomic, assign) int maxMoveDuration;

@end

@interface WeakAndFastMonster : Monster {
}
+(id)monster;
@end

@interface StrongAndSlowMonster : Monster {
}
+(id)monster;
@end

这里相当明了:我们从CCSprite导出怪兽,添加若干变量,追踪怪兽状态,然后针对两类怪兽导出两个怪兽子类。

然后打开Monster.m,添加执行(implementation)项:

#import “Monster.h”

@implementation Monster

@synthesize hp = _curHp;
@synthesize minMoveDuration = _minMoveDuration;
@synthesize maxMoveDuration = _maxMoveDuration;

@end

@implementation WeakAndFastMonster

+ (id)monster {

WeakAndFastMonster *monster = nil;
if ((monster = [[[super alloc] initWithFile:@”Target.png”] autorelease])) {
monster.hp = 1;
monster.minMoveDuration = 3;
monster.maxMoveDuration = 5;
}
return monster;

}

@end

@implementation StrongAndSlowMonster

+ (id)monster {

StrongAndSlowMonster *monster = nil;
if ((monster = [[[super alloc] initWithFile:@”Target2.png”] autorelease])) {
monster.hp = 3;
monster.minMoveDuration = 6;
monster.maxMoveDuration = 12;
}
return monster;

}

@end

这里真正的代码是,我们用于返回各类实体的静态方式,设置默认HP及移动持续时间。

现在将我们的新怪兽类植入剩余的代码中。首先将import(输入)添加至HelloWorldScene.m顶部的新文件中:

#import “Monster.h”

然后通过修改addTarget方法,构建新类的实体,而不是直接创建精灵。将spriteWithFile代码行替换成如下内容:

//CCSprite *target = [CCSprite spriteWithFile:@"Target.png" rect:CGRectMake(0, 0, 27, 40)];
Monster *target = nil;
if ((arc4random() % 2) == 0) {
target = [WeakAndFastMonster monster];
} else {
target = [StrongAndSlowMonster monster];
}

这里各类型的怪兽有50%的繁衍几率。此外,由于我们已将怪兽速度移至类中,因此将最高/最低持续时间代码修改成如下样式:

int minDuration = target.minMoveDuration; //2.0;
int maxDuration = target.maxMoveDuration; //4.0;

最后是对updateMethod的些许调整。首先在说明targetsToDelete前添加一个布尔值(boolean)。

BOOL monsterHit = FALSE;

然后在CGRectIntersectsRect测试中添加如下代码(游戏邦注:而不是直接在targetsToDelete中添加对象):

//[targetsToDelete addObject:target];
monsterHit = TRUE;
Monster *monster = (Monster *)target;
monster.hp–;
if (monster.hp <= 0) {
[targetsToDelete addObject:target];
}
break;

所以根本来说,我们不会直接杀死怪兽,我们会扣除一个HP值,只在数值处在0或0以下时消灭它。此外,注意若抛物体击中怪兽,我们就会摆脱循环机制,这意味着抛物体一次只能够击中一个怪兽。

最后,按照如下方式修改projectilesToDelete测试:

if (monsterHit) {
[projectilesToDelete addObject:projectile];
[[SimpleAudioEngine sharedEngine] playEffect:@”explosion.caf”];
}

编译和运行代码,若一切进展顺利,你会看到两类怪兽从屏幕中穿过——这令炮塔的存在更具挑战性。

Screenshot(from raywenderlich)

Screenshot(from raywenderlich)

多重关卡

为植入多重关卡,我们首先要进行代码重构。代码重构非常简单,但其中包括丰富内容,文章就不加以详述。

相反,我将从更高层面概述所完成的操作,详细介绍样本项目。

抽象提取一个Level类。目前,主要是关于“关卡”的HelloWorldScene硬编码信息,例如,生成哪个怪兽,生成频率是多少等。所以第一步就是,抽取若干这类信息,将其植入Level类中,这样我们就能够在HelloWorldScene中重复使用同个逻辑,创造多重关卡。

重复利用情境。目前,每次切换情境时,我们都会创造新的情境类实体。这里的一个弊端是,若没有小心管理,你就会在于init方法中加载资源时出现延误。

由于我们的游戏相当简单,因此我们需要进行的操作是,创建各情境的实体,调用reset()方法,清除之前的状态(游戏邦注:如来自上个关卡的怪兽或抛物体)。

将应用授权当作转换台。目前,我们没有什么总体状态,例如处于哪个关卡或关卡的具体设置是什么,各情境只是就具体应切换至哪个情境进行硬编码。

我们将就此进行修改,这样App Delegate商店就能够指示总体状态,例如关卡信息,因为这是个方便所有情境访问的中心地带。我们还在App Delegate植入特定方法,以切换情境,将逻辑元素聚集起来,降低内部情境的依赖性。

所以这些是代码重构的重点。记住这只是其中一种设置方式。

在此,我们已创建众多游戏要素—–旋转的炮塔,众多不同特性的敌人,多重关卡,胜败情境,当然还有优质音效!

游戏邦注:原文发布于2010年3月,文章叙述以当时为背景。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

Rotating Turrets: How To Make A Simple iPhone Game with Cocos2D Part 2

By Ray Wenderlich

There’s been a surprising amount of interest in the post on How To Make a Simple iPhone Game with Cocos2D – and several of you guys have asked for some more in this series!

Specifically, some of you asked for a tutorial on how to rotate a turret to face the shooting direction. This is a common requirement for a lot of games – including one of my favorite genres, tower defense!

So in this tutorial we’ll cover how do exactly that, and add a rotating turret into the simple game. Special thanks goes to Jason and Robert for suggesting this tutorial!

(And don’t forget Part 3 in this series – Harder Monsters and More Levels!)

Getting Set Up

If you have followed along with the last tutorial, you can continue with the project exactly how we left off. If not, just download the code from the last tutorial and let’s start from there.

Next, download the new player sprite and projectile sprite images, add them into your project, and delete the old Player.jpg and Projectile.jpg from your project. Then modify the lines of code that create each sprite to read as follows:

Note that this time, we don’t bother specifying the width and height of our sprites and let Cocos2D handle it for us instead.

Compile and run your project, and if all looks well you should see a turret shooting bullets. However, it doesn’t look right because the turret doesn’t rotate to face where it’s shooting – so let’s fix that!

Rotating To Shoot

Before we can rotate the turret, we first need to store a reference to our Player sprite so we can rotate it later on. Open up HelloWorldScene.h and modify the class to include the following member variable:

Then modify the code in the init method that adds the player object to the layer as follows:

And finally let’s add the cleanup code in dealloc before we forget:

Ok, now that we’ve got a reference to our player object, let’s rotate it! To rotate it, we first need to figure out the angle that we need to rotate it to.

To figure this out, think back to high school trigonometry. Remember the mnemonic SOH CAH TOA? That helps us remember that the Tangent of an angle is equal to the Opposite over the Adjacent. This picture helps explain:

As shown above, the angle we want to rotate is equal to the arctangent of the Y offset divided by the X offset.

However, there are two things we need to keep in mind. First, when we compute arctangent(offY / offX), the result will be in radians, while Cocos2D deals with degrees. Luckily, Cocos2D provides an easy to use conversion macro we can use.

Secondly, while we’d normally consider the angle in the picture above positive angle (of around 20°), in Cocos2D rotations are positive going clockwise (not counterclockwise), as shown in the following picture:

So to point in the right direction, we’ll need to multiply our result by negative 1. So for exaple, if we multiplied the angle in the picture above by negative 1, we’d get -20°, which would represent a counterclockwise rotation of 20°.

Ok enough talk, let’s put it into code! Add the following code inside ccTouchesEnded, right before you call runAction on the projectile:

Compile and run the project and the turret should now turn to face the direction it’s shooting!

Rotate Then Shoot

It’s pretty good so far but is a bit odd because the turret just jumps to shoot in a particular direction rather than smoothly flowing. We can fix this, but it will require a little refactoring.

First open up HelloWorldScene.h and add the following member variables to your class:

Then modify your ccTouchesEnded and add a new method named finishShoot so it looks like the following:

That looks like a lot of code, but we actually didn’t change that much – most of it was just some minor refactoring. Here are the changes we made:

* We bail at the beginning of the function if there is a value in nextProjectile, which means we’re in the process of shooting.

* Before we used a local object named projectile that we added to the scene right away. In this version we create an object in the member variable nextProjectile, but don’t add it until later.

* We define the speed at which we want our turret to rotate as half a second for half a circle’s worth of rotation. Remember that a circle has 2 PI radians.

* So to calculate how long this particular rotation should take, we multiply the radians we’re moving by the speed.

* Then we start up a sequence of actions where we rotate the turret to the correct angle, then call a function to add the projectile to the scene.

So let’s give it a shot! Compile and run the project, and the turret should now rotate much more smoothly.

Harder Monsters and More Levels: How To Make A Simple iPhone Game with Cocos2D Part 3

So far, the game we’ve been making in How To Make A Simple iPhone Game with Cocos2D is pretty cool. We have a rotating turret, monsters to shoot, and uber sound effects.

But our turret has it too easy. The monsters only take one shot, and there’s just one level! He’s not even warming up yet.

In this tutorial, we will extend our project so that we make different types of monsters of varying difficulty, and implement multiple levels into the game.

Tougher Monsters

For fun, let’s create two types of monsters: a weak and fast monster, and a strong and slow monster. To help the player distinguish between the two, download this modified monster image and add it to your project. While you’re at it, download this explosion sound effect I made and add it to the project as well.

Now let’s make our Monster class. There are many ways to model your Monster class, but we’re going to do the simplest thing, which is to make our Monster class a subclass of CCSprite. We’re also going to create two subclasses of Monster: one for our weak and fast monster, and one for our strong and slow monster.

Go to File\New File, choose Cocoa Touch Class\Objective-C class, make sure Subclass of NSObject is selected, and click Next. Name the file Monster.m and make sure “Also create Monster.h” is checked.

Then replace Monster.h with the following:

Pretty straightforward here: we just derive Monster from CCSprite and add a few variables for tracking monster state, and then derive two subclasses of Monster for two different types of monsters.

Now open up Monster.m and add in the implementation:

The only real code here is two static methods we added to return instances of each class, set up with the default HP and move durations.

Now let’s integrate our new Monster class into the rest of the code! First add the import to your new file to the top of HelloWorldScene.m:

Then let’s modify the addTarget method to construct instances of our new class rather than creating the sprite directly. Replace the spriteWithFile line with the following:

This will give a 50% chance to spawn each type of monster. Also, since we’ve moved the speed of the monsters into the classes, modify the min/max duration lines as follows:

Finally, a couple mods to the updateMethod. First add a boolean right before the declaration of targetsToDelete:

Then, inside the CGRectIntersectsRect test, instead of adding the object immediately in targetsToDelete, add the following code:

So basically, instead of instantly killing the monster, we subtract an HP and only destroy it if it’s 0 or lower. Also, note that we break out of the loop if the projectile hits a monster, which means the projectile can only hit one monster per shot.

Finally, modify the projectilesToDelete test as follows:

Compile and run the code, and if all goes well you should see two different types of monsters running across the screen – which makes our turret’s life a bit more challenging!

Multiple Levels

In order to implement multiple level support, we need to do some refactoring first. The refactoring is all pretty simple but there is a lot of it, and including it all in this post would make for a long, boring post.

Instead, I’ll include some high level overview of what was done and refer you to the sample project for full details.

Abstract out a Level class. Currently, the HelloWorldScene hard-coded information about the “level” such as which monsters to spawn, how often to spawn them, etc. So the first step is to pull out some of this information into a Level class so we can re-use the same logic in the HelloWorldScene for multiple levels.

Re-use scenes. Currently, we’re creating new instances of the scene class each time we switch between scenes. One of the drawbacks to this is that without careful management, you can incur delays as you load up your resources in the init method.

Since we have a simple game, what we’re going to do is just create one instance of each scene and just call a reset() method on it to clear out any old state (such as monsters or projectiles from the last level).

Use the app delegate as a switchboard. Currently, we don’t have any global state like what level we’re on or what the settings for that level are, and each scene just hard-codes which scenes it should switch to.

We’ll modify this so that the App Delegate stores pointers to the global state such as the level information, since it is a central place that is easy to access by all of the scenes. We’ll also put methods in the App Delegate to switch between scenes to centralize that logic and reduce intra-scene dependencies.

So those are the main points behind the refactoring – check out the sample project to see the full details. Keep in mind this is only one of many ways of doing it – if you have another cool way to organize the scene and game objects for your game please share!

So anyway, download the code and give it a whirl! We have a nice start to a game going here – a rotating turret, tons of enemies to shoot with varying qualities, multiple levels, win/lose scenes, and of course – awesome sound effects!(Source:Raywenderlich Part 2Part 3


上一篇:

下一篇: