iOS开发——UI篇OC篇&SpriteKit详解

转自:https://www.bbsmax.com/A/WpdKoZ8odV/

SpriteKit,iOS/Mac游戏制作的新纪元

这是我的WWDC2013系列笔记中的一篇,完整的笔记列表请参看这篇总览。本文仅作为个人记录使用,也欢迎在许可协议范围内转载或使用,但是还烦请保留原文链接,谢谢您的理解合作。如果您觉得本站对您能有帮助,您可以使用RSS邮件方式订阅本站,这样您将能在第一时间获取本站信息。

本文涉及到的WWDC2013 Session有

  • Session 502 Introduction to Sprite Kit
  • Session 503 Designing Games with Sprite Kit

SpriteKit的加入绝对是iOS 7/OSX 10.9的SDK最大的亮点。从此以后官方SDK也可以方便地进行游戏制作了。

如果你在看这篇帖子,那我估计你应该稍微知道一些iOS平台上2D游戏开发的东西,比如cocos2d,那很好,因为SpriteKit的很多概念其实和 cocos2d非常类似,你应该能很快掌握。如果上面这张图你看着眼熟,或者自己动手实践过,那更好,因为这篇文章的内容就是通过使用SpriteKit 来一步一步带你重新实践一遍这个经典教程。如果你既不知道cocos2d,更没有使用游戏引擎开发iOS游戏的经验,只是想一窥游戏开发的天地,那现 在,SpriteKit将是一个非常好的入口,因为是iOS SDK自带的框架,因此思想和用法上和现有的其他框架是统一的,这极大地降低了学习的难度和门槛。

什么是SpriteKit

首先要知道什么是Sprite。 Sprite的中文译名就是精灵,在游戏开发中,精灵指的是以图像方式呈现在屏幕上的一个图像。这个图像也许可以移动,用户可以与其交互,也有可能仅只是 游戏的一个静止的背景图。塔防游戏中敌方源源不断涌来的每个小兵都是一个精灵,我方防御塔发出的炮弹也是精灵。可以说精灵构成了游戏的绝大部分主体视觉内 容,而一个2D引擎的主要工作,就是高效地组织,管理和渲染这些精灵。SpriteKit是在iOS7 SDK中Apple新加入的一个2D游戏引擎框架,在SpriteKit出现之前,iOS开发平台上已经出现了像cocos2d这样的比较成熟的2D引擎 解决方案。SpriteKit展现出的是Apple将Xcode和iOS/Mac SDK打造成游戏引擎的野心,但是同时也确实与IDE有着更好的集成,减少了开发者的工作。

Hello SpriteKit

废话不多说,本文直接上实例教程来说明SpriteKit的基本用法。

好吧,我要做的是将非常风靡流行妇孺皆知的raywenderlich的经典cocos2d教程使 用全新的SpriteKit重新实现一遍。重做这个demo的主要原因是cocos2d的这个入门实在是太经典了,包括了精灵管理,交互检测,声音播放和 场景切换等等方面的内容,麻雀虽小,却五脏俱全。这个小demo讲的是一个无畏的英雄抵御外敌侵略的故事,英雄在画面左侧,敌人源源不断从右侧涌来,通过 点击屏幕发射飞镖来消灭敌人,阻止它们越过屏幕左侧边缘。在示例中用到的素材,可以从这里下载。另外为了方便大家,整个工程示例我也放在了github上,传送门在此

配置工程

首先当然是建立工程,Xcode5提供了SpriteKit模板,使用该模板建立新工程,名字就叫做SpriteKitSimpleGame好了。

新建一个SpriteKit工程

因为我们需要一个横屏游戏,所以在新建工程后,在工程设定的General标签中,把Depoyment Info中Device Orientation中的Portrait勾去掉,使应用只在横屏下运行。另外,为了使之后的工作轻松一些,我们可以选择在初始的view显示完成,尺 寸通过rotation计算完毕之后再添加新的Scene,这样得到的Scene的尺寸将是宽480(或者568)高320的size。如果在 appear之前就使用bounds.size添加的话,将得到宽320 高480(568)的size,会很麻烦。将ViewController.m中的-viewDidLoad:方法全部替换成下面的-viewDidAppear:

 
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
 
  1. - (void)viewDidAppear:(BOOL)animated
  2. {
  3. [super viewDidAppear:animated];
  4.  
  5. // Configure the view.
  6. SKView * skView = (SKView *)self.view;
  7. skView.showsFPS = YES;
  8. skView.showsNodeCount = YES;
  9.  
  10. // Create and configure the scene.
  11. SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
  12. scene.scaleMode = SKSceneScaleModeAspectFill;
  13.  
  14. // Present the scene.
  15. [skView presentScene:scene];
  16. }

然后编译运行,应如果一切正常,该显示类似于下面的画面,每点击画面时,会出现一架不停旋转的飞机。

SpriteKit正常运行

加入精灵

SpriteKit是基于场景(Scene)来组织的,每个SKView(专门用来呈现SpriteKit的View)中可以渲染和管理一个SKScene,每个Scene中可以装载多个精灵(或者其他Node,之后会详细说明),并管理它们的行为。

现在让我们在这个Scene里加一个精灵吧,先从我们的英雄开始。首先要做的是把刚才下载的素材导入到工程中。我们这次用资源目录(Asset Catalog)来管理资源吧。点击工程中的Images.xcassets, 以打开Asset Catalog。将下载解压后Art文件夹中的图片都拖入到打开的资源目录中,资源目录会自动根据文件的命名规则识别图片,1x的图片将用于 iPhone4和iPad3之前的非retina设备,2x的图片将用于retina设备。当然,如果你对设备性能有信心的话,也可以把1x的图片删除 掉,这样在非retina设备中也将使用2x的高清图(画面上的大小自然会帮你缩小成2x的一半),以获取更好的视觉效果。做完这一步后,工程的资源目录 会是这个样子的:

将图片素材导入工程中

开始coding吧~默认的SpriteKit模板做的事情就是在ViewController的self.view(这个view是一个SKView, 可以到storyboard文件中确认一下)中加入并显示了一个SKScene的子类实例MyScene。正如在做app开发时我们主要代码量会集中在 ViewController一样,在用SpriteKit进行游戏开发时,因为所有游戏逻辑和精灵管理都会在Scene中完成,我们的代码量会集中在 SKScene中。在MyScene.m中,把原来的-initWithSize替换成这样:

 
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
 
  1. -(id)initWithSize:(CGSize)size {
  2. if (self = [super initWithSize:size]) {
  3. /* Setup your scene here */
  4.  
  5. //1 Set background color for this scene
  6. self.backgroundColor = [SKColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
  7.  
  8. //2 Create a new sprite
  9. SKSpriteNode *player = [SKSpriteNode spriteNodeWithImageNamed:@"player"];
  10.  
  11. //3 Set it's position to the center right edge of screen
  12. player.position = CGPointMake(player.size.width/2, size.height/2);
  13.  
  14. //4 Add it to current scene
  15. [self addChild:player];
  16. }
  17. return self;
  18. }
  1. 因为默认工程的Scene背景偏黑,而我们的主角和怪物也都是黑色的,所以先 设定为白色。SKColor只是一个define定义而已,在iOS平台下被定义为UIColor,在Mac下被定义为NSColor。在 SpriteKit开发时,尽量使用SK开头的对应的UI类可以统一代码而减少跨iOS和Mac平台的成本。类似的定义还有SKView,它在iOS下是 UIView的子类,在Mac下是NSView的子类。
  2. 在SpriteKit中初始化一个精灵很简单,直接用SKSpriteNode+spriteNodeWithImageNamed:,指定图片名就行。实际上一个SKSpriteNode中包含了贴图(SKTexture对象),颜色,尺寸等等参数,这个简便方法为我们读取图片,生成SKTexture,并设定精灵尺寸和图片大小一致。在实际使用中,绝大多数情况这个简便方法就足够了。
  3. 设 定精灵的位置。SpriteKit中的坐标系和其他OpenGL游戏坐标系是一致的,屏幕左下角为(0,0)。不过需要注意的是不论是横屏还是竖屏游 戏,view的尺寸都是按照竖屏进行计算的,即对于iPhone来说在这里传入的sizewidth是320,height是480或者568,而不会因 为横屏而发生交换。因此在开发时,请千万不要使用绝对数值来进行位置设定及计算(否则你会死的很难看啊很难看)。
  4. 把player加入到当前scene中,addChild接受SKNode对象(SKSprite是SKNode的子类),关于SKNode稍后再说。

运行游戏,yes~主角出现在屏幕上了。

在屏幕左侧添加了一个精灵

源源不断涌来的怪物大军

没有怪物的陪衬,主角再潇洒也是寂寞。添加怪物精灵的方法和之前添加主角没什么两样,生成精灵,设定位置,加到scene中。区别在于怪物是会移动的 & 怪物是每隔一段时间就会出现一个的。

在MyScene.m中,加入一个方法-addMonster

 
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
 
  1. - (void) addMonster {
  2.  
  3. SKSpriteNode *monster = [SKSpriteNode spriteNodeWithImageNamed:@"monster"];
  4.  
  5. //1 Determine where to spawn the monster along the Y axis
  6. CGSize winSize = self.size;
  7. int minY = monster.size.height / 2;
  8. int maxY = winSize.height - monster.size.height/2;
  9. int rangeY = maxY - minY;
  10. int actualY = (arc4random() % rangeY) + minY;
  11.  
  12. //2 Create the monster slightly off-screen along the right edge,
  13. // and along a random position along the Y axis as calculated above
  14. monster.position = CGPointMake(winSize.width + monster.size.width/2, actualY);
  15. [self addChild:monster];
  16.  
  17. //3 Determine speed of the monster
  18. int minDuration = 2.0;
  19. int maxDuration = 4.0;
  20. int rangeDuration = maxDuration - minDuration;
  21. int actualDuration = (arc4random() % rangeDuration) + minDuration;
  22.  
  23. //4 Create the actions. Move monster sprite across the screen and remove it from scene after finished.
  24. SKAction *actionMove = [SKAction moveTo:CGPointMake(-monster.size.width/2, actualY)
  25. duration:actualDuration];
  26. SKAction *actionMoveDone = [SKAction runBlock:^{
  27. [monster removeFromParent];
  28. }];
  29. [monster runAction:[SKAction sequence:@[actionMove,actionMoveDone]]];
  30.  
  31. }
  1. 计算怪物的出生点(移动开始位置)的Y值。怪物从右侧屏幕外随机的高度处进入屏幕,为了保证怪物图像都在屏幕范围内,需要指定最小和最大Y值。然后从这个范围内随机一个Y值作为出生点。
  2. 设定出生点恰好在屏幕右侧外面,然后添加怪物精灵。
  3. 怪物要是匀速过来的话太死板了,加一点随机量,这样怪物有快有慢不会显得单调
  4. 建立SKAction。SKAction可以操作SKNode,完成诸如精灵移动,旋转,消失等等。这里声明了两个SKAction,actionMove负责将精灵在actualDuration的时间间隔内移动到结束点(直线横穿屏幕);actionMoveDone负责将精灵移出场景,其实是run一段接受到的block代码。runAction方法可以让精灵执行某个操作,而在这里我们要做的是先将精灵移动到结束点,当移动结束后,移除精灵。我们需要的是一个顺序执行,这里sequence:可以让我们顺序执行多个action。

然后尝试在上面的-initWithSize:里调用这个方法看看结果

 
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
 
  1. -(id)initWithSize:(CGSize)size {
  2. if (self = [super initWithSize:size]) {
  3. //...
  4. [self addChild:player];
  5. [self addMonster];
  6. }
  7. return self;
  8. }

在游戏中加入会动的敌人

Cool,我们的游戏有个能动的图像。知道么,游戏的本质是什么?就是一堆能动的图像!

只有一个怪物的话,英雄大大还是很寂寞,所以我们说好了会有源源不断的怪物..在-initWithSize:的4之后加入以下代码

 
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
 
  1. //...
  2. //5 Repeat add monster to the scene every 1 second.
  3. SKAction *actionAddMonster = [SKAction runBlock:^{
  4. [self addMonster];
  5. }];
  6. SKAction *actionWaitNextMonster = [SKAction waitForDuration:1];
  7. [self runAction:[SKAction repeatActionForever:[SKAction sequence:@[actionAddMonster,actionWaitNextMonster]]]];
  8. //...

这里声明了一个SKAction的序列,run一个block,然后等待1秒。用这个动作序列用-repeatActionForever:生成一个无限重复的动作,然后让scene执行。这样就可以实现每秒调用一次-addMonster来 向场景中不断添加敌人了。如果你对Cocoa(Touch)开发比较熟悉的话,可能会说,为什么不用一个NSTimer来做同样的事情,而要写这样的 SKAction呢?能不能用NSTimer来达到同样的目的?答案是在对场景或者精灵等SpriteKit对象进行类似操作时,尽量不要用 NSTimer。因为NSTimer将不受SpriteKit的影响和管理,使用SKAction可以不加入其它任何代码就获取如下好处:

  • 自动暂停和继续,当设定一个SKNode的paused属 性为YES时,这个SKNode和它管理的子node的action都会自动被暂停。这里详细说明一下SKNode的概念:SKNode是 SpriteKit中要素的基本组织方式,它代表了SKView中的一种游戏资源的组织方式。我们现在接触到的SKScene和SKSprite都是 SKNode的子类,而一个SKNode可以有很多的子Node,从而构成一个SKNode的树。在我们的例子中,MyScene直接加在SKView中 作为最root的node存在,而英雄或者敌人的精灵都作为Scene这个node的子node被添加进来。SKAction和node上的各种属性的的 作用范围是当前这个node和它的所有子node,在这里我们如果设定MySecnen这个node(也就是self)的paused属性被设为YES的话,所有的Action都会被暂停,包括这个每隔一秒调用一次的action,而如果你用NSTimer的话,恭喜,你必须自行维护它的状态。
  • 当SKAction依附的结点被从结点树中拿掉的时候,这个action会自动结束并停止,这是符合一般逻辑的。

编译,运行,一切如我们所预期的那样,每个一秒有一个怪物从右侧进入,并以不同的速度穿过屏幕。

添加了源源不断滚滚而来的敌人大军

奥特曼打小怪兽是天经地义的

有了英雄,有了怪兽,就差一个“打”了。我们打算做的是在用户点击屏幕某个位置时,就由英雄所在的位置向点击位置发射一枚固定速度的飞镖。然后这每飞镖要是命中怪物的话,就把怪物从屏幕中移除。

先来实现发射飞镖吧。检测点击,然后让一个精灵朝向点击的方向以某个速度移动,有很多种SKAction可以实现,但是为了尽量保持简单,我们使用上面曾经使用过的moveTo:duration:吧。 在发射之前,我们先要来做一点基本的数学运算,希望你还能记得相似三角形之类的概念。我们的飞镖是由英雄发出的,然后经过手指点击的点,两点决定一条直 线。简单说我们需要求解出这条直线和屏幕右侧边缘外的交点,以此来确定飞镖的最终目的。一旦我们得到了这个终点,就可以控制飞镖moveTo到这个终点, 从而模拟出发射飞镖的action了。如图所示,很简单的几何学,关于具体的计算就不再讲解了,要是算不出来的话,请考虑call你的中学数学老师并负荆 请罪以示诚意。

通过点击计算飞镖终止位置

然后开始写代码吧,还记得我们之前点击会出现一个飞机的精灵么,找到相应的地方,MyScene.m里的-touchesBegan:withEvent::,用下面的代码替换掉原来的。

 
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
 
  1. -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  2. /* Called when a touch begins */
  3.  
  4. for (UITouch *touch in touches) {
  5. //1 Set up initial location of projectile
  6. CGSize winSize = self.size;
  7. SKSpriteNode *projectile = [SKSpriteNode spriteNodeWithImageNamed:@"projectile.png"];
  8. projectile.position = CGPointMake(projectile.size.width/2, winSize.height/2);
  9.  
  10. //2 Get the touch location tn the scene and calculate offset
  11. CGPoint location = [touch locationInNode:self];
  12. CGPoint offset = CGPointMake(location.x - projectile.position.x, location.y - projectile.position.y);
  13.  
  14. // Bail out if you are shooting down or backwards
  15. if (offset.x <= 0) return;
  16. // Ok to add now - we've double checked position
  17. [self addChild:projectile];
  18.  
  19. int realX = winSize.width + (projectile.size.width/2);
  20. float ratio = (float) offset.y / (float) offset.x;
  21. int realY = (realX * ratio) + projectile.position.y;
  22. CGPoint realDest = CGPointMake(realX, realY);
  23.  
  24. //3 Determine the length of how far you're shooting
  25. int offRealX = realX - projectile.position.x;
  26. int offRealY = realY - projectile.position.y;
  27. float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));
  28. float velocity = self.size.width/1; // projectile speed.
  29. float realMoveDuration = length/velocity;
  30.  
  31. //4 Move projectile to actual endpoint
  32. [projectile runAction:[SKAction moveTo:realDest duration:realMoveDuration] completion:^{
  33. [projectile removeFromParent];
  34. }];
  35. }
  36. }
  1. 为飞镖设定初始位置。
  2. 将点击的位置转换为node的坐标系的坐标,并计算点击位置和飞镖位置的偏移量。如果点击位置在飞镖初始位置的后方,则直接返回
  3. 根据相似三角形计算屏幕右侧外的结束位置。
  4. 移 动飞镖,并在移动结束后将飞镖从场景中移除。注意在移动怪物的时候我们用了两个action(actionMove和actionMoveDone来做移 动+移除),这里只使用了一个action并用带completion block移除精灵。这里对飞镖的这种做法是比较简明常见高效的,之前的做法只是为了说明action的sequence:的用法。

运行看看现在的游戏吧,我们有英雄有怪物还有打怪物的小飞镖,好像气氛上已经开始有趣了!

发布了227 篇原创文章 · 获赞 231 · 访问量 169万+

猜你喜欢

转载自blog.csdn.net/haima1998/article/details/104097755