注:本文译自Sprite Kit Tutorial: Making a Universal App: Part 1
本文将介绍如何制作一个通用程序(打鼹鼠的游戏)——可以在iPhone和iPad上运行(包括retina显示的支持。)
学习本文之前,需要掌握以下知识:
Sprite Kit教程:初学者 1 Sprite Kit教程:初学者 2 Sprite Kit教程:初学者 3
英文原文在这里:Sprite Kit Tutorial for Beginners
Sprite Kit教程:动画和纹理图集 1 Sprite Kit教程:动画和纹理图集 2
英文原文在这里:Sprite Kit Tutorial: Animations and Texture Atlases
Sprite Kit教程:如何拖放Sprites
英文原文在这里:Sprite Kit Tutorial: How To Drag and Drop Sprites
如果还没有看上面的这些文章(或者相关的知识),建议你先去看一下。
本文会有两篇文章。第一篇,会先创建一个基本的游戏——可爱的小鼹鼠聪洞里面弹出来。为了让游戏在iPhone和iPad(支持retina显示)上看起来很优美,本文还花了大量的时间来考虑如何做游戏的美术规划和坐标。
我们希望程序可以在iPhone 3.5英寸,4英寸(iPhone 5)和iPad上良好的运行,所以在开始之前,我们需要认真的做好UI规划。
为了搞明白需要什么样的UI尺寸,我们先来看看下面的相关内容:
下面开始吧!
在iPhone中,non-retina和retina在显示上的最大区别就是retina的分辨率是non-retina的2倍。所以在non-retina上面分辨率为 480 * 320(landscape),而retina则是960 * 640.
同样iPad也分为non-retina和retina,它们的分辨率相差也为2倍,non-retina显示的分辨率是1024 * 768像素,而retina上面则是2048 * 1536像素!
稍等,你可能在想:双倍分辨率岂不是打乱了所有已经写好的程序,例如iPhone上的480 * 320和iPad的1024 * 768?这是有可能的,除非是在Sprite Kit中设置尺寸或者坐标,此时实际上是在UIKit中进行设置,并且设置的尺寸单位叫做points,而不是像素(pixels)。
在non-retina显示上,无论是iPhone火iPad,一个point代表一个pixel,而在retina上面,一个point代表2个pixels。所以将位置设置为(10,10)point时,non-retina上将是(10,10),而retina上则是(20,20),所以它们依然会显示在相同的偏移量上。不错吧!
当使用苹果提供的控件或者Core Graphics时,苹果已经写好了相关代码,让它们在retina显示起来很好看。
唯一需要注意的就是关于使用的图片。比如在iPhone或iPad程序中又一个200 * 200d 图片。如果什么事情都不做的话,在retina上面会自动的将这个图片放大两倍——这看起来不是太好,因为我们并没有提供相关分辨率的图片。
因此针对retina显示我们需要提供所有图片的另外一个版本,也就是说需要一个普通的版本,以及另外2倍分辨率的一个版本。如果将2倍分辨率图片命名为”@2x”后缀,那么当利用[SKSpriteNode spriteNodeWithImageNamed:…]或者类似的APIs加载sprite时,它会自动的将@2x图片加载到retina显示上。
所以在开发针对retina显示的Sprite Kit游戏时也很简单——只需要添加@2x的图片,基本上就搞定了。
iPhone 5设备在屏幕上显示的分辨率比以前的更大了,对于游戏显示上来说,这非常的好。本文中的处理很简单,只需要将背景图片做一个扩展延伸即可。
iPhone 5的分辨率是1136 * 640——宽高比为16:9。用point来衡量的话则是568 * 320.
上面我们已经看到要处理retina显示很容易,但是要想创建一个通用的程序呢(可以运行在iPhone和iPad设备上)。
其实要想创建一个通用的程序还真有一个麻烦的事情——iPhone和iPad的宽高比不一样!
iPhone的比例是1.5(480 * 320 或960 * 640),而iPad是1.33(768 * 1024或1536 * 2048)。
由于比例不同,如果一副能够在non-retina iPad(768×1024)上完整显示,你希望将其在iPhone上重用,那么不会完整的匹配上,如果将其缩放,按照宽度进行适配(乘以0.9375),会得到720×960的尺寸,这样就会把高度剪切掉一部分。
发生这种情况会让人比较烦恼,我们不仅需要处理背景图片的问题,不同的宽高比导致不同设备间使用相同的坐标比较困难。
下面是我了解到的一些对应的处理方法:
下面这些模拟器可以运行iOS 7:
注意:这里并没有non-retina iPhone——因为没有任何一台no-retina iPhone或iPod touch可以运行iOS 7。
另外由于Sprite Kit是在iOS 7中才引入的,所以就不用考虑no-retina iPhone或iPod touch设备了。
基于上面的一些讨论,下面是本文的相关计划:
来这里可以下载到本文的UI资源。解压出下载到的文件,可以看到如下一些内容:
上面搞了这么多,现在终于可以开始了!
打开Xcode,选择File > New > Project…,然后选中Sprite Kit Game并单击Next。将工程命名为WhackAMole,devices选中universal,接着再单击Next。选择一个路径来保存工程,然后单击Create。
当工程打开之后,应该能看到Project Navigator中的工程文件已经被选中了,如果没有选中,那么将其选中,然后在target中选中WhackAModle,以及选中顶部的General,在Deployment info里面可以看到一些设备朝向的勾选框。在这里我们的游戏是landscape的,所以勾选上iPhone和iPad的Landscape Left和Landscape Right。
另外,为了让朝向正确,还需要对代码做一些修改。打开ViewController.m文件并用下面的viewWillLayoutSubviews:方法替换viewDidLoad方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; // Configure the view. SKView * skView = (SKView *)self.view; if (!skView.scene) { skView.showsFPS = YES; skView.showsNodeCount = YES; // Create and configure the scene. SKScene * scene = [MyScene sceneWithSize:skView.bounds.size]; scene.scaleMode = SKSceneScaleModeAspectFill; // Present the scene. [skView presentScene:scene]; } } |
为什么要这样做呢?默认情况下View Controller views是以竖直的方式加载,所以横屏模式下,当viewDidLoad被调用的时候不能保证尺寸是正确的,不过当viewWillLayoutSubviews被调用的时候view的size将是正确的。如上代码所示,大多数代码与viewdidLoad中的相同。需要关注的就是if语句中关于skView.scene的配置。当然在这里需要判断一下skView.scene是否已经存在(viewWillLayoutSubviews方法可能会被多次调用)。
纹理图集的配置非常简单。首选创建一个文件夹并且文件名已.atlas结尾。接着将那些UI元素拷贝到这个文件夹里面。然后在Xcode工程中添加这个文件夹即可!
简单吧!当在编译程序的时候,Xcode会把.atlas结尾的文件夹中的图片生成纹理图集。
注意:添加到.atlas文件夹中的图片尺寸不能超过2048×2048 pixels,否则会出错——2048×2048 pixels是自动生成纹理图集的最大尺寸。
下面看看具体如何做。找到之前下载的压缩文件,在压缩文件中有一个名为TextureAtlases的文件夹。这个文件夹中包含了3中设备类型的UI元素(iPad, iPhone, 和 WidescreeniPhone)。这些文件家中都包含有.atlas文件夹。我们将TextureAtlases文件夹拖至工程中,确保勾选上Copy items into destination group’s folder (if needed)。
本文中为了让一切变得简单点,我们为每种类型的设备准备了一套纹理图集(iPhone 3.5-inch, iPhone 4-inch 和 iPads)。在iPhone 4英寸中可以重用iPhone3.5英寸中的一些纹理图集,
在开始修改scene中显示内容之前,我们需要添加一个宏以及一个helper方法。打开MyScene.m文件,并在文件的头部添加如下一行代码(在#import下面):
1 |
#define IS_WIDESCREEN ( fabs( ( double )[ [ UIScreen mainScreen ] bounds ].size.height - ( double )568 ) < DBL_EPSILON ) |
上面这个宏可以判断程序是否允许在4英寸的屏幕中,该宏将被用在helper方法中,如果要了解上面宏的详细内容,看这里。
接着添加一个helper方法——为运行程序的设备获取正确的SKTextureAtla。这个方法接收一个文件名,并在文件名尾部添加一个正确的标示符,然后返回正确的一个SKTextureAtla。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (SKTextureAtlas *)textureAtlasNamed:(NSString *)fileName { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { if (IS_WIDESCREEN) { // iPhone Retina 4-inch fileName = [NSString stringWithFormat:@"%@-568", fileName]; } else { // iPhone Retina 3.5-inch fileName = fileName; } } else { fileName = [NSString stringWithFormat:@"%@-ipad", fileName]; } SKTextureAtlas *textureAtlas = [SKTextureAtlas atlasNamed:fileName]; return textureAtlas; } |
上面的代码做了些什么?
接着找到initWithSize:方法。移除掉设置背景颜色和创建Hell World lable的6行代码,然后用下面的代码替换之:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Add background SKTextureAtlas *backgroundAtlas = [self textureAtlasNamed:@"background"]; SKSpriteNode *dirt = [SKSpriteNode spriteNodeWithTexture:[backgroundAtlas textureNamed:@"bg_dirt"]]; dirt.scale = 2.0; dirt.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); dirt.zPosition = 0; [self addChild:dirt]; // Add foreground SKTextureAtlas *foregroundAtlas = [self textureAtlasNamed:@"foreground"]; SKSpriteNode *upper = [SKSpriteNode spriteNodeWithTexture:[foregroundAtlas textureNamed:@"grass_upper"]]; upper.anchorPoint = CGPointMake(0.5, 0.0); upper.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); upper.zPosition = 1; [self addChild:upper]; SKSpriteNode *lower = [SKSpriteNode spriteNodeWithTexture:[foregroundAtlas textureNamed:@"grass_lower"]]; lower.anchorPoint = CGPointMake(0.5, 1.0); lower.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)); lower.zPosition = 3; [self addChild:lower]; // Add more here later... |
我们来看看上面的代码都做了什么。
在运行程序之前,再做一点清理工作。找到touchesBegan:方法,并将其删除掉。
编译并运行程序,现在可以看到屏幕上显示出了背景图和前景图!并且在iPhone和iPad模拟器中运行,也能正确的显示!如下图所示:
在这个游戏中,我们将添加3个鼹鼠到scene中——上图中的每个洞放一个。鼹鼠默认是在地下的,偶尔会弹出来,当弹出来时,我们可以打击它们。
首先我们先将鼹鼠放到每个洞中。为了确保鼹鼠位置的正确,最好先把鼹鼠显示在最上面,等调好位置之后,在将其放到后台去。
打开MyScene.h文件,并按照如下代码进行修改:
1 2 3 4 5 6 7 8 |
#import <SpriteKit/SpriteKit.h> @interface MyScene : SKScene @property (strong, nonatomic) NSMutableArray *moles; @property (strong, nonatomic) SKTexture *moleTexture; @end |
上面的代码添加了一个SKTexture和一个数组。创建鼹鼠的时候会用到SKTexture,创建好的每个鼹鼠会被添加到数组中,这样方便之后循环获得每个鼹鼠。
在添加鼹鼠之前,首先定位到MyScene.m的顶部,并将下面这行代码添加到@implementation MyScene之前。
1 |
const float kMoleHoleOffset = 155.0; |
这是一个float类型的常量,用来对鼹鼠进行定位。
接着,将如下代码添加到initWithSize:方法最后面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// Load sprites self.moles = [[NSMutableArray alloc] init]; SKTextureAtlas *spriteAtlas = [self textureAtlasNamed:@"sprites"]; self.moleTexture = [spriteAtlas textureNamed:@"mole_1.png"]; float center = 240.0; if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && IS_WIDESCREEN) { center = 284.0; } SKSpriteNode *mole1 = [SKSpriteNode spriteNodeWithTexture:self.moleTexture]; mole1.position = [self convertPoint:CGPointMake(center - kMoleHoleOffset, 85.0)]; mole1.zPosition = 999; mole1.name = @"Mole"; mole1.userData = [[NSMutableDictionary alloc] init]; [self addChild:mole1]; [self.moles addObject:mole1]; SKSpriteNode *mole2 = [SKSpriteNode spriteNodeWithTexture:self.moleTexture]; mole2.position = [self convertPoint:CGPointMake(center, 85.0)]; mole2.zPosition = 999; mole2.name = @"Mole"; mole2.userData = [[NSMutableDictionary alloc] init]; [self addChild:mole2]; [self.moles addObject:mole2]; SKSpriteNode *mole3 = [SKSpriteNode spriteNodeWithTexture:self.moleTexture]; mole3.position = [self convertPoint:CGPointMake(center + kMoleHoleOffset, 85.0)]; mole3.zPosition = 999; mole3.name = @"Mole"; mole3.userData = [[NSMutableDictionary alloc] init]; [self addChild:mole3]; [self.moles addObject:mole3]; |
上面的代码首先创建并加载一个SKTextureAtlas。接着根据sprite纹理图集中的mole_1.png 创建一个SKTexture,这将用来创建3个鼹鼠。Texture的重用性可以让Sprite Kit处理和渲染sprite更加高效。
接下来的这个值用来设置center。如果设备是4英寸的iPhone,那么这个center值将反映出额外的尺寸。
接着为每个鼹鼠创建对应的sprite,并将它们放置到scene中,还把它们添加到鼹鼠数组中。注意,每个鼹鼠的位置是利用center位置和文件头部定义的常量决定的。针对iPhone 3.5英寸的设备,鼹鼠位置处在480×320的可玩区域,而如何是iPad,相关位置需要做转换,所以下面写了一个helper方法convertPoint。
将下面这个方法添加到initWithSize:方法后面:
1 2 3 4 5 6 7 8 |
- (CGPoint)convertPoint:(CGPoint)point { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { return CGPointMake(32 + point.x*2, 64 + point.y*2); } else { return point; } } |
上面这个方法将可玩区域的point转换到iPad上适当的位置。记住:
就这样,上面的方法就是简单的给出iPad中正确的位置。
编译并运行程序,可以看到scene中有3个鼹鼠,它们的位置已经设置正确!你最好在iPhone 3.5-inch, iPhone 4-inch, iPad, 和 iPad Retina设备上都运行一下,以确保位置的正确。
至此,我们已经把鼹鼠放置好了,下面我们添加一些代码让鼹鼠从洞里面跳出来吧。
首先,将这些sprite(鼹鼠)的zPosition从999设置为2,这样就可以把鼹鼠藏起来了。
然后,将下面的代码添加到update:方法中:
1 2 3 4 5 6 7 |
for (SKSpriteNode *mole in self.moles) { if (arc4random() % 3 == 0) { if (!mole.hasActions) { [self popMole:mole]; } } } |
需要注意的是每帧的显示都会调用update方法。该方法被调用的时候我们都会尝试着弹出一些鼹鼠。在代码中循环遍历处理了每个鼹鼠,并给每个鼹鼠1/3的机会从洞中弹出来。不过记住我们只能弹出那么还没有弹出来的鼹鼠——很简单的一个判断方法就是检查一下sprite的属性hasActions返回的值,如果还有action在运行,那么hasActions将返回YES。
接着,实现一下popMole方法:
1 2 3 4 5 6 7 8 9 10 11 |
- (void)popMole:(SKSpriteNode *)mole { SKAction *easeMoveUp = [SKAction moveToY:mole.position.y + mole.size.height duration:0.2f]; easeMoveUp.timingMode = SKActionTimingEaseInEaseOut; SKAction *easeMoveDown = [SKAction moveToY:mole.position.y duration:0.2f]; easeMoveDown.timingMode = SKActionTimingEaseInEaseOut; SKAction *delay = [SKAction waitForDuration:0.5f]; SKAction *sequence = [SKAction sequence:@[easeMoveUp, delay, easeMoveDown]]; [mole runAction:sequence]; } |
上面的代码使用了Sprite Kit中的一些action,让鼹鼠弹出洞来,并暂停半秒钟,然后在弹回去。我们来细看一下上面代码的意思:
搞定!编译并运行程序,可以看到鼹鼠会从它们的洞口弹出来!
本文的代码工程在这里。
下一篇文章Sprite Kit教程:制作一个通用程序 2中会给鼹鼠添加一些可爱的动画(笑和被击中),并添加一个玩法——打击鼹鼠,并赚取点数,并添加一些音效。