http://www.raywenderlich.com/3997/introduction-to-augmented-reality-on-the-iphone
示例代码在此:
http://www.raywenderlich.com/downloads/ARSpaceships.zip
在这部分的教程中,我们将学习如何为iPhone/iPod touch制作一款简单的增加现实游戏!
我们将会用到摄像头,陀螺仪和Cocos2d。怎么样,是不是已经热血沸腾了?
在写这篇教程的时候,哥实在是激情万丈,探索这些新的技术实在是太激动人心了!当然,我们不得不遇到一些数学和转换的问题,但是哥保证,任何一个懂得加减乘除的攻城师应该不会对此感到头疼的!
当然,为了学习这篇教程,你得有iPhone4,因为我们需要用到gyroscope(陀螺仪)来移动你的视角。
同时,你必须对cocos2d的基础知识有所了解,是的,还是从第一章开始学起吧。
开始前的准备
打开Xcode,选择创建New Project,选择cocos2d模板,把项目命名为ARSpaceships。
这个游戏中的部分资源来自之前学习过的激战太空射击游戏,所以让我们下载并解压缩这些文件。
下载完文件后,将Fonts, Sounds,Spritesheets这些资源文件都拖到项目的Resources里面。此时你的项目看起来会是这样的:
摄像头就位!
如果现在就编译运行项目,你神马感觉都没有!是的,漆黑一片的夜空,白得煞人的Hello World,一切都是如此的苍白!攻城师朋友们,让我们来点乐子吧!选择AppDelegate.h文件,然后在interface的声明部分添加一个UIView。
UIView *overlay;
现在让我们切换到AppDelegate.m文件。下滚到EAGLView *glView这一行代码,把pixelFormat(像素格式)调整为kEAGLColorFormatRGBA8,如下所示:
EAGLView *glView =[EAGLView viewWithFrame:[window bounds]
如果你不修改这里的像素格式,那么摄像头所获取的图像是无法显示的。既然我们玩的是增强现实游戏,这怎么了得!
然后,在[window addSubView: viewController.view]这个代码的下面,我们需要添加如下几行代码:
// set the background color of the view
[CCDirector sharedDirector].openGLView.backgroundColor =[UIColor clearColor];
[CCDirector sharedDirector].openGLView.opaque =NO;
// set value for glClearColor
glClearColor(0.0, 0.0, 0.0, 0.0);
// prepare the overlay view and add it to the window
overlay =[[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
overlay.opaque =NO;
overlay.backgroundColor=[UIColor clearColor];
[window addSubview:overlay];
在上面的代码中,我们将openGLView的背景色设置为清除(灰度值和透明度均为0),将视图层的不透明设置为否。
接下来我们设置了glClearColor(四个值分别是红色,绿色,蓝色和透明度)
最后我们创建了一个UIView,并添加到主窗口中,用来在后面显示从摄像头获取的图像。
下一步需要在刚才添加的代码之下继续增加新的代码:
#define CAMERA_TRANSFORM
UIImagePickerController *uip;
@try{
}
@catch(NSException* e){
[uip release];
}
@finally{
if(uip){
[overlay addSubview:[uip view]];
[overlay release];
}
}
[window bringSubviewToFront:viewController.view];
首先我们定义了摄像头的缩放常量值。摄像头所获取图像的长宽比是4:3,而iphone屏幕的长宽比是3:4,所以我们需要调整摄像头获取图像的比例,以弥补这个差距。
然后我们创建了一个UIImagePickerController,并设置了部分属性,然后缩放了它的比例,并将其添加到overlay视图中。注意这里使用了异常处理,不太了解的朋友可以看看Programming in Objective-C中的相关内容。
最后,我们需要将viewController.view(里面包含了cocos2d的显示)设置到前面,这样的话它会显示到摄像头视图的前面。
现在让我们来编译运行,你可以看到来自摄像头的图像会作为背景显示在Hello World的后面。不得不说,如果你够用心,可以弄个红心或者I love you神马的,拿这个来讨好你的小女友!
摇一摇,晃一晃,摆一摆!
现在这款游戏的现实部分已经搞定了,我们可以集中精力攻克教程中更难的部分了!
首先我们需要把CoreMotion这个框架添加到项目中。点击项目名,选择ARSpaceships target,选择Build Phases选项,展开Link Binary With Libraries。
点击 + 按钮,选择CoreMotion.framework,然后点击Add 按钮。现在一切就绪,我们可以在项目里面使用陀螺仪了!
打开HelloWorldLayer.h文件,在顶部添加下面的代码:
#include <CoreMotion/CoreMotion.h>
#import <CoreFoundation/CoreFoundation.h>
在interface的声明部分添加以下变量:
CMMotionManager *motionManager;
CCLabelTTF *yawLabel;
CCLabelTTF *posIn360Label;
在interface的声明下面添加属性语句:
@property(nonatomic, retain) CMMotionManager *motionManager;
接下来我们要进入最精彩的部分了!打开HelloWorldLayer.m文件,在init方法的if((self = [super init]))语句中,删除创建”Hello World”标签的语句,并使用下面的代码来设置新的标签。
// add and position the labels
yawLabel =[CCLabelTTF labelWithString:@"Yaw: " fontName:@"Marker Felt" fontSize:12];
posIn360Label =[CCLabelTTF labelWithString:@"360Pos: " fontName:@"Marker Felt" fontSize:12];
yawLabel.position =
posIn360Label.position =
[self addChild: yawLabel];
[self addChild:posIn360Label];
上面的代码平淡无奇,添加了两个标签,设置了字体和标签上的文字,仅此而已。至于标签的位置,我们都放在屏幕的左侧。
接下来我们需要设置motion manager,将使用它来启动陀螺仪。
self.motionManager =[[[CMMotionManager alloc] init] autorelease];
motionManager.deviceMotionUpdateInterv
if(motionManager.isDeviceMotionAvailable){
[motionManager startDeviceMotionUpdates
}
[self scheduleUpdate];
使用上面的代码,我们对motion manager进行了初始化。同时,设置更新频率为每秒60次。如果设备带有陀螺仪,就会开始更新。最后我们使用定时器进行更新。
别忘了在文件的顶部添加了synthesize语句:
@synthesize motionManager;
同时,由于我们这里使用定时器更新,所以需要添加一个update方法。在init方法的下面添加这样一个方法:
-(void)update:(ccTime)delta {
// 1: Convert the radians yaw value to degrees then round up/down
float yaw = roundf((float)(CC_RADIANS_TO_DEGREES(currentAttitude.yaw)));
// 2: Convert the degrees value to float and use Math function to round the value
[yawLabel setString:[NSString stringWithFormat:@"Yaw: %.0f", yaw]];
// 3: Convert the yaw value to a value in the range of 0 to 360
int positionIn360 = yaw;
if(positionIn360 <0){
}
[posIn360Label setString:[NSString stringWithFormat:@"360Pos: %d", positionIn360]];
}
现在让我们来编译和运行程序。你可以在对应的标签上看到Yaw和positionIn360的数值变化。
冲破迷雾!
好吧,数值是看到了,但是---究竟是怎么实现的?上面代码好像看不懂也!!!
让我们花点时间好好解释一下吧。
首先从iTunes appstore中下载一个免费的Gyrosocope app(http://itunes.apple.com/us/app/gyroscope/id381953722?mt=8),运行这个应用,当我们移动iPhone的时候,你会看到很炫的视觉效果!
在上面的数值中,需要关注的是Yaw(摇摆,偏航)。Yaw代表的是左右摇晃的运动。在这款应用中是使用degree(角度)为单位的,而从motion manager中获得的数值是以radian(弧度)为单位标示的。所以我们用了CC_RADIANS_TO_DEGRESS方法来完成这个转换工作。
所以,在第一部分的代码中,我们获取了以弧度为单位的yaw value(摆动值),将其转换为角度,然后将数值赋予yaw 这个变量。
第二部分的代码就相对简单了,只是把偏航值显示在屏幕上。运行程序的时候,你可以看到数值的变动是从0变到180,然后从-180变回0。
再来看第三部分的代码,这个positionIn360变量到底是个神马东西?!好吧,其实没别的意思,哥是图方便,这样的话把飞碟放在屏幕上时会更简单。
这里所使用的逻辑很简单。如果偏航值是正,那么什么都不需要做。如果偏航值是负值,我们需要将其加上360。而最后一行代码不过是把这个数值显示在屏幕上而已。
灯光就位!摄像头就位!开机!
现在我们多多少少了解了下陀螺仪的相关知识(更多可以参考CMMotionManager类的官方参考文档-在xcode的documentations中搜索即可),该是添加太空飞船的时候了!
首先让我们创建一个新文件。左键点击ARSpaceships,然后选择New File,选择iOS\Cocoa Touch\Objective-C class,然后点击Next。确保选中Subclass of NSObject,然后点击Next,保存名称为EnemyShip.m,然后点击Save。
使用下面的代码来替代EnemyShip.h中的内容:
#import "cocos2d.h"
@interface EnemyShip : CCSprite {
int yawPosition;
int timeToLive;
}
@property(readwrite)int yawPosition;
@property(readwrite)int timeToLive;
@end
然后用以下代码替代EnemyShip.m的内容:
#import "EnemyShip.h"
@implementation EnemyShip
@synthesize yawPosition, timeToLive;
-(id)init {
if(self){
return self;
}
@end
现在让我们重新切换到 HelloWorldLayer.h文件。在文件的顶部添加以下代码:
#import "EnemyShip.h"
然后在声明部分添加:
NSMutableArray*enemySprites;
int enemyCount;
CCSpriteBatchNode *batchNode;
最后,在interface声明部分的下面添加属性说明和方法定义,至于具体的作用当然要到后面来详细解释罗:
@property(readwrite)int enemyCount;
-(EnemyShip *)addEnemyShip:(int)shipTag;
-(void)checkEnemyShipPosition:(EnemyShip *)enemyShip withYaw:(float)yawPosition;
-(void)updateEnemyShipPosition:(int)positionIn360 withEnemy:(EnemyShip *)enemyShip;
-(void)runStandardPositionCheck
让我们切换到HelloWorldLayer.m文件,并做以下修改:
// Place after the #import statement
#include <stdlib.h>
// Place after the other @synthesize statement
@synthesize enemyCount;
#define kXPositionMultiplier 15
#define kTimeToLive 100
// Add to the bottom of init
batchNode =[CCSpriteBatchNode batchNodeWithFile:@"Sprites.pvr.ccz"];
[self addChild:batchNode];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"Sprites.plist"];
通过以上代码,我们载入了在教程开始所添加的精灵表单。
接下来我们要添加一个方法让太空飞船出现在屏幕上。在dealloc方法的上面添加以下方法:
-(EnemyShip *)addEnemyShip:(int)shipTag {
// Set position of the space ship randomly
int x = arc4random()60;
// Set the position of the space ship off the screen, but in the center of the y axis
// we will update it in another method
[enemyShip setPosition:ccp(5000, 160)];
// Set time to live on the space ship
[batchNode addChild:enemyShip z:3 tag:shipTag];
return enemyShip;
}
这个方法接收了一个标记值,并返回一个EnemyShip 精灵对象。首先我们从精灵表单中创建了一个EnemyShip精灵,然后使用arc4random方法来获取一个范围在0-360之间的随机整数。最后我们设置了飞船的位置,将timeToLive的数值设置为100,把飞船添加为batchNode的子节点,并在结束处返回所添加的飞船对象。
在addEnemyShip方法的下面,让我们添加checkEnemyShipPosition方法的代码:
-(void)checkEnemyShipPosition:(EnemyShip *)enemyShip withYaw:(float)yawPosition {
// Convert the yaw value to a value in the range of 0 to 360
int positionIn360 = yawPosition;
if(positionIn360 <0){
}
BOOL checkAlternateRange =false;
// Determine the minimum position for enemy ship
int rangeMin = positionIn360 -23;
if(rangeMin <0){
}
// Determine the maximum position for the enemy ship
int rangeMax = positionIn360 +23;
if(rangeMax >360){
}
if(checkAlternateRange){
if((enemyShip.yawPosition < rangeMax || enemyShip.yawPosition > rangeMin ) || (enemyShip.yawPosition > rangeMin || enemyShip.yawPosition < rangeMax)){
[self updateEnemyShipPosition:positionIn360 withEnemy:enemyShip];
}
}else{
if(enemyShip.yawPosition > rangeMin && enemyShip.yawPosition < rangeMax){
[self updateEnemyShipPosition:positionIn360 withEnemy:enemyShip];
}
}
}
在上面的代码中,一大堆的alternate,min和max是不是让你头大了?!其实,没那么复杂。
首先我们检查设备的yaw position(偏航位置),并将其转换为0-360之间的数值(一个整圆)。
由于我们所使用的数轴两端是0-360,所以需要确认是否设备的positionIn360在某一端上。这里我们使用23这个指定值代表显示在屏幕一半位置处的角度,如下图所示:
经过这种处理后,我们只需要关心0-23和337-360之间的数值。
此时,如果飞船的偏航位置在图中46度的范围内,就更新飞船的位置。checkAlternateRange的判断语句用于确定何时更新飞船的位置。
如果checkAlternateRange为True,我们需要检查是否飞船位置在最小和最大值之间。判断语句中的这些检查看起来令人抓狂,但是如果我们用实际的数值来验证,就明白这样做的道理了。
让我们假定:
positionIn360 = 20
rangeMin = 357
rangeMax = 20
enemyShip.yawPosition = 359
因为我们要考虑到数轴的两端,所以最小的范围值rangeMin要大于最大范围值rangeMax。接下来我们要完成所有的检查,发现飞船的位置大于rangeMin,这样我们会将飞船显示在屏幕上。
Else部分的语句看起来更直接一些。它只是检查飞船的位置是否在min和max之间。
接下来,让我们在checkEnemyShipPosition方法的下面添加以下方法:
-(void)updateEnemyShipPosition:(int)positionIn360 withEnemy:(EnemyShip *)enemyShip {
int difference =0;
if(positionIn360 <23){
// Run 1
if(enemyShip.yawPosition >337){
int xPosition =240+(difference * kXPositionMultiplier);
[enemyShip setPosition:ccp(xPosition, enemyShip.position.y)];
}else{
// Run Standard Position Check
[self runStandardPositionCheck
}
}elseif(positionIn360 >337){
// Run 2
if(enemyShip.yawPosition <23){
int xPosition =240-(difference * kXPositionMultiplier);
[enemyShip setPosition:ccp(xPosition, enemyShip.position.y)];
}else{
// Run Standard Position Check
[self runStandardPositionCheck
}
}else{
// Run Standard Position Check
[self runStandardPositionCheck
}
}
在这个方法中,我们需要判断设备的positionIn360是否在三个范围的一个中。在第一个测试中,判断positionIn360是否小于23,如果是,再判断是否有飞船出现在数轴的另一端(大于337)。
在第二个测试中,判断positionIn360是否大于337。如果是,再判断是否有飞船出现在数轴的另一端(小于23),和上一个测试完全相反。
第三个测试(也是最后一个),判断positionIn360是否在23和337之间。我们会调用runStandardPositionCheck
具体的代码实现如下:
-(void)runStandardPositionCheck
if(enemyShip.yawPosition > positionIn360){
int xPosition =240-(difference * kXPositionMultiplier);
[enemyShip setPosition:ccp(xPosition, enemyShip.position.y)];
}else{
int xPosition =240+(difference * kXPositionMultiplier);
[enemyShip setPosition:ccp(xPosition, enemyShip.position.y)];
}
}
在这个方法中,我们查看enemyShip的位置是否在设备的positionIn360的左边或右边。如果enemyShip的位置的坐标值小于positionIn360,则出现在屏幕的左侧。如果enemyShip的位置的坐标值大于positionIn360,则出现在屏幕的右侧。
等等!你是不是忘了解释difference这个变量?好吧,事实是这样的:
如果飞船的偏航值在屏幕的范围内(从positionIn360-23到positionIn360+23),我们会首先计算出它在屏幕的哪一侧。如果它大于positionIn360,就在屏幕的右侧,反之就在屏幕的左侧。
Difference变量用于测量设备的positionIn360和飞船偏航值间的角度差。一旦得出difference的值,我们用一个指定的乘数乘以difference。这个乘数代表每个角度的像素量。这里我们选择的数值是15。
接下来,根据在屏幕的哪一册,我们会使用240(屏幕宽度除以2)来加上或减去这个计算出的数值。
以上就是updateEnemyShipPosition方法的具体实现。
现在,该准备的方法都准备好了,接下来我们就需要调用这些方法了。
在init方法的底部,添加以下代码,从而在屏幕上添加五艘飞船。
// Loop through 1 - 5 and add space ships
enemySprites =[[NSMutableArray alloc] init];
for(int i =0; i <5; ++i){
[enemySprites addObject:enemyShip];
}
同时,因为我们把飞船添加到了屏幕中,就需要随时更新它们的位置。在update方法的结尾处添加以下代码:
// Loop through array of Space Ships and check the position
for(EnemyShip *enemyShip in enemySprites){
[self checkEnemyShipPosition:enemyShip withYaw:yaw];
}
同时,乘我们头脑还清醒,在dealloc方法的下面添加以下代码清除enemySpritesArray:
[enemySprites release];
编译运行游戏!现在你可以看到有5艘飞船出现在 你的视野之中!
发射激光,战斗打响!
现在我们的增强现实游戏其实已经挺酷了,不是吗?!不过这些外星飞船在眼前晃来晃去感觉真不爽,这可是哥自己的地盘呀!
好吧,让我们加点绚丽至极的激光和爆炸效果吧!
在开始之前,先让我们干掉屏幕上的那些标签吧——它们只是用来调试游戏用的。切换到HelloWorldLayer.m中,注释掉所有和yawLabel和posIn360Label相关的东西。然后编译运行,确保没有误删掉有用的代码!
有趣的东西来了,来给游戏加点炮火吧!首先我们需要添加一个方法,来检查玩家的开火区域是否击中了飞船。在HelloWorldLayer.h文件中,在@end之前添加以下代码:
-(BOOL) circle:(CGPoint) circlePoint withRadius:(float) radius collisionWithCircle:(CGPoint) circlePointTwo collisionCircleRadius:(float) radiusTwo;
然后切换到HelloWorldLayer.m文件,在dealloc方面前添加以下方法:
-(BOOL) circle:(CGPoint) circlePoint withRadius:(float) radius collisionWithCircle:(CGPoint) circlePointTwo collisionCircleRadius:(float) radiusTwo {
}
这个方法用来判断两个点的半径范围内是否重叠。所输入的参数是飞船的位置和屏幕的中心点。两个点的半径都设置为50。
首先计算出两点间的x和y坐标差。
接下来计算出两点间的距离。你应该还记得勾股定理吧,这里就不多介绍了。
然后我们需要在屏幕上添加一个瞄准镜,帮我们判断开火的位置。从这里下载项目所需的资源(http://www.raywenderlich.com/downloads/ARSpaceshipsResources.zip),然后把这个scope.png文件拖到Resources文件夹里。
切换到HelloWorldLayer.m文件,找到init方法,然后在[self scheduleUpdate];的前面添加以下代码:
// Add the scope crosshairs
CCSprite *scope =[CCSprite spriteWithFile:@"scope.png"];
scope.position = ccp(240, 160);
[self addChild:scope z:15];
// Allow touches with the layer
[self registerWithTouchDispatc
编译运行游戏,你会看到瞄准镜出现在屏幕的正中央!
太酷了!现在让我们添加一些爆炸效果,这样当玩家触碰屏幕的时候会更热血澎湃!首先把Explosition.plist添加到Resources里面。
好吧,这个文件是个神马东东,是干嘛的?
它是我用Particle Designer(http://particledesigner.71squared.com/)设计的粒子效果。这里我就不讲如何创建粒子效果了,但如果你亲自试一下,就会发现自己要做的一切不过是选择一种粒子系统,调整一些参数,然后导出为plist文件。是的,就这么简单!
Ok,现在让我们在dealloc方法的前面添加以下代码:
-(void)ccTouchesBegan:(NSSet*)touches withEvent:(UIEvent *)event {
// 1
for(EnemyShip *enemyShip in enemySprites){
if(enemyShip.timeToLive >0){
// Check to see if yaw position is in range
BOOL wasTouched =[self circle:location withRadius:50 collisionWithCircle:enemyShip.position collisionCircleRadius:50];
if(wasTouched){
}
}
}
// 2
[self addChild:particle z:20];
// 3
if(enemyCount ==0){
// Show end game
[self addChild:label z:30];
}
}
上面代码的第一部分使用我们之前添加的碰撞检测方法,用来判断飞船是否在瞄准镜的范围内。如果飞船被击中,我们会设置飞船的属性,将其隐藏,并减少enemyCount的变量值。
在第二部分,我们在屏幕的中间添加了粒子效果。
在最后一部分,我们添加了一个游戏逻辑,当enemyCount变量等于0的时候,会显示一个标签,提醒玩家已经赢得了游戏。
到此为止,游戏似乎有点无聊,我们需要给飞船添加一些最基本的AI。在update方法的最后添加以下代码:
// Loop through array of Space Ships and if the timeToLive is zero
// change the yawPosition of the sprite
for(EnemyShip *enemyShip in enemySprites){
if(enemyShip.timeToLive ==0){
int x = arc4random()60;
[enemyShip setPosition:ccp(5000, 160)];
}
}
使用这个方法,我们遍历了enemySprites数组,并更新timeToLive属性。然后会判断是否属性等于0,如果是,就会让飞船出现了另一个yawPosition,并重置timeToLive。编译运行游戏,现在有点难度了吧!
震耳欲聋!
没有音效的游戏会少了很多乐趣,所以让我们再来加点音乐元素吧!
在HelloWorldLayer.m的顶部添加一行代码导入音效引擎:
#import "SimpleAudioEngine.h"
向下滚动代码,在init方法中if语句的最后添加以下代码:
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"SpaceGame.caf" loop:YES];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"explosion_large.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"laser_ship.caf"];
以上代码会加载背景音乐和音效。
接下来继续向下滚动代码,在ccTouchesBegan方法中,在该方法开始的部分添加以下代码:
[[SimpleAudioEngine sharedEngine] playEffect:@"laser_ship.caf"];
当玩家触碰屏幕的时候,这行代码会发出激光的音效。
继续在ccTouchesBegan方法中,在enemyShip in enemySprites的for 循环语句中,在(wasTouched)的if语句中添加以下代码:
[[SimpleAudioEngine sharedEngine] playEffect:@"explosion_large.caf"];
这样,当飞船被击中时,会发出爆炸的音效!
终于,终于,终于搞定了,一个完整的增强现实游戏出现在你的面前!
更多帮助你深入学习的文档:
苹果官方的core motion文档:http://developer.apple.com/library/ios/#documentation/CoreMotion/Reference/CoreMotion_Reference/_index.html
苹果官方的UIImagePickerController类文档:
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIImagePickerController_Class/UIImagePickerController/UIImagePickerController.html<br />
关于摄像头和长宽比的更多文档:
http://stackoverflow.com/questions/3407986/uiimagepickercontroller-cameraviewtransform-acts-differently-in-ios-4
http://gotoandplay.freeblog.hu/archives/2010/07/06/3Dcompass_augmented_reality_085_-_fullscreen_camera_preview/
http://www.musicalgeometry.com/?p=821