Sprite Kit教程:如何拖放Sprites

注:本文译自Sprite Kit Tutorial: How To Drag and Drop Sprites

目录

  • 开始
  • 用触摸的方式选中sprite
  • 用触摸的方式移动sprite和layer
  • 在Sprite Kit中如何使用手势识别
  • 何去何从

本文中,你可以学到如下内容:

  • 利用触摸来拖放sprite的基本知识
  • 利用触摸滚动view
  • How to keep coordinates straight in your head
  • 如何在Sprite Kit中使用手势识别

为了让本文有趣一点,这里提供了一些可爱的动物图片。

本文假设你已经了解了Sprite Kit的一些基本知识。如果还不了解的话,先看看下面的文章吧:

Sprite Kit教程:初学者 1 Sprite Kit教程:初学者 2 Sprite Kit教程:初学者 3

英文原文在这里:Sprite Kit Tutorial for Beginners

下面我们就开始吧。

开始

在实现触摸处理之前,我们先来创建一个基本的Sprite Kit工程,并在scene中显示出一些sprite(动物)和背景。

打开Xcode,选择File\New Project\Application\SpriteKit Game,然后单击Next。

Sprite Kit教程:如何拖放Sprites_第1张图片

将工程命名为DragDrop,devices选择iPhone,然后单击Next,把工程保存到磁盘中。

Sprite Kit教程:如何拖放Sprites_第2张图片

跟Sprite Kit教程:初学者 1一样,我们希望这个程序只支持横屏显示(landscape)。所以在Project Navigator中选中DragDrop工程,然后选择DragDrop target,在弹出的画面中,只需要勾选上Landscape Left和Landscape Right。如下图所示:

Sprite Kit教程:如何拖放Sprites_第3张图片

打开ViewController.m文件,并用下面的代码替换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];  } } 

接着来这里下载本文需要用到的图片资源。下载并解压之后,将所有的文件拖到工程中,其中把Copy items into destination group’s folder (if needed)勾选上,然后单击Finish。

Sprite Kit教程:如何拖放Sprites_第4张图片

完成上面的步骤之后,打开MyScene.m文件,并在@implementation上面添加一个class extension,并声明两个属性,如下所示:

1 2 3 4 5 6
@interface MyScene ()  @property (nonatomic, strong) SKSpriteNode *background; @property (nonatomic, strong) SKSpriteNode *selectedNode;  @end 

稍后会用到上面的这两个属性来存储背景图片,已经当前选中的node/sprite。接着在@interface前面添加如下这行代码:

1
static NSString * const kAnimalNodeName = @"movable";

稍后将会用这个字符串来标示可移动的node。接着找到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
- (id)initWithSize:(CGSize)size {  if (self = [super initWithSize:size]) {   // 1) Loading the background  _background = [SKSpriteNode spriteNodeWithImageNamed:@"blue-shooting-stars"];  [_background setName:@"background"];  [_background setAnchorPoint:CGPointZero];  [self addChild:_background];   // 2) Loading the images  NSArray *imageNames = @[@"bird", @"cat", @"dog", @"turtle"];  for(int i = 0; i < [imageNames count]; ++i) {  NSString *imageName = [imageNames objectAtIndex:i];  SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:imageName];  [sprite setName:kAnimalNodeName];   float offsetFraction = ((float)(i + 1)) / ([imageNames count] + 1);  [sprite setPosition:CGPointMake(size.width * offsetFraction, size.height / 2)];  [_background addChild:sprite];  }  }   return self; } 

我们来看看上面的代码都干了什么。

1) 加载背景图片

上面方法中的第一部分代码是为scene加载背景图片(blue-shooting-stars.png)。并将该note的anchor设置为图片的左下角(0, 0)。

在Sprite Kit中,设置一个node的位置时,实际上是设置它的anchor。默认情况下,node的anchor被设置为node的正中间。在此,将anchor设置为左下角。

方法中,并没有设置背景图片的position,所以背景图的的位置默认为(0,0)。最终,图片的左下角位置是(0,0),并向右边延伸。

2) 加载小动物

函数中接下来的代码是循环遍历列表中的图片,并将其加载到scene中。为了好的布局,其中各个node根据屏幕的长度来定位,另外还将这些node的名字设置为kAnimalNodeName。

之后将创建好的node添加到_background中。

OK!编译并运行程序,会看到屏幕中已经显示出了一些可爱的动物了。

Sprite Kit教程:如何拖放Sprites_第5张图片

用触摸的方式选中sprite

下面我们来实现一下根据用户当前触摸的位置判断出哪个sprite应该被选中。

用下面的代码替换touchesBegan:withEvent::

1 2 3 4 5
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {  UITouch *touch = [touches anyObject];  CGPoint positionInScene = [touch locationInNode:self];  [self selectNodeForTouch:positionInScene]; } 

首先从touches set中获得touch。然后将touch的位置转换到一个指定node中的位置,上面的代码中使用了scene。让后将获得的方法传递给selectNodeForTouch:方法,该方法是一个新方法,下面我们就来看看这个方法的实现。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
- (void)selectNodeForTouch:(CGPoint)touchLocation {  //1  SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];   //2  if(![_selectedNode isEqual:touchedNode]) {  [_selectedNode removeAllActions];  [_selectedNode runAction:[SKAction rotateToAngle:0.0f duration:0.1]];   _selectedNode = touchedNode;  //3  if([[touchedNode name] isEqualToString:kAnimalNodeName]) {  SKAction *sequence = [SKAction sequence:@[[SKAction rotateByAngle:degToRad(-4.0f) duration:0.1],  [SKAction rotateByAngle:0.0 duration:0.1],  [SKAction rotateByAngle:degToRad(4.0f) duration:0.1]]];  [_selectedNode runAction:[SKAction repeatActionForever:sequence]];  }  }  } 

这是一个helper方法,它主要做三件不同的事情:

  1. 通过scene(self)获得touchLocation位置对应的node。
  2. 获得匹配的node之后,检查一下这个node与上一次选中的node是否相同,如果相同的话,在这里直接就返回了。如果是一个新选中的node,或者还没有选中过,这个node会有一点小小的挪动动画,以此可以看出哪个node被选中了。不过在开始动画之前,需要移除当前已经选中node上的所有running actions,并在这个node上运行一个action:rotateToAngle:duration:。这样可以确保只有一个node在做动画,而另外的node恢复到原样。
  3. 这个if语句用来判断一下选中的node是否可以进行动画(只需要检查一下node的name就可以做出判断——还记得在initWithSize:方法中设置的这个属性值吗?)。如果选中的node可以做动画处理,那么就创建一个sequence action——是一个动画效果,就像在主屏幕中重排/删除程序那样的效果,然后在选中的node上运行这个sequence。为了避免动画运行完毕之后会停止,在这里运行了一个一直重复的action。

下面将helper函数degToRad添加到文件的底部:

1 2 3
float degToRad(float degree) {  return degree / 180.0f * M_PI; } 

由于Sprite Kit是利用弧度来做旋转效果的,所以上面这个方法将角度转换为弧度。

编译并运行程序,现在可以在屏幕上tap一个动物,当选中某个动物时,该动物会做出相应的动画效果,以表示被选中!

Sprite Kit教程:如何拖放Sprites_第6张图片

用触摸的方式移动sprite和layer

下面来看看如何移动这些动物!基本思路是这样的:实现touchesMoved:withEvent:方法,计算出距离上一次触摸移动了多远,如果有动物被选中,动物将被移动相应的距离,如果没有选中动物,那么就移动整个layer,这样用户可以从左向右的滚动layer。

在添加代码之前,我们先来探讨一下在Sprite Kit中,一个node是如何滚动的。

看看下面的图片:

如上图所示,我们已经初始化了一个背景,所以背景的anchor点是(0, 0),并且向右边扩展。黑色框中的区域表示当前的可视区域(window的大小)。

如果希望将图片往右边滚动100 points,可以通过将整个node往左边移动100 points,如第二幅图看到的效果一样。

当然,也可能希望不要移动太远。例如,不应该让layer可以往右边移动,否则会看到空白的点。

下面来看看相应的代码!将如下方法添加到文件的底部:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
- (CGPoint)boundLayerPos:(CGPoint)newPos {  CGSize winSize = self.size;  CGPoint retval = newPos;  retval.x = MIN(retval.x, 0);  retval.x = MAX(retval.x, -[_background size].width+ winSize.width);  retval.y = [self position].y;  return retval; }  - (void)panForTranslation:(CGPoint)translation {  CGPoint position = [_selectedNode position];  if([[_selectedNode name] isEqualToString:kAnimalNodeName]) {  [_selectedNode setPosition:CGPointMake(position.x + translation.x, position.y + translation.y)];  } else {  CGPoint newPos = CGPointMake(position.x + translation.x, position.y + translation.y);  [_background setPosition:[self boundLayerPos:newPos]];  } } 

第一个方法boundLayerPos:是为了确保不会将layer移动到背景图片范围之外。在这里传入一个需要移动到的位置,然后该方法会对位置做适当的判断处理,以确保不会移动太远。

接着方法panForTranslation:首先判断一下_selectedNode是否为动物node,如果是的话,根据传入的参数来为node设置新的位置。如果是background layer,同样也会设置一个新的位置,只不过新的位置需要调用boundLayerPos:方法获得。

完成上面之后,可以实现touchesMoved:withEvent:方法了:

1 2 3 4 5 6 7 8 9
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {  UITouch *touch = [touches anyObject];  CGPoint positionInScene = [touch locationInNode:self];  CGPoint previousPosition = [touch previousLocationInNode:self];   CGPoint translation = CGPointMake(positionInScene.x - previousPosition.x, positionInScene.y - previousPosition.y);   [self panForTranslation:translation]; } 

跟touchesBegan:withEvent:一样,先获得touch,然后将它的位置转换为scene中的相应位置。为了计算出移动的距离,需要上一次触摸的位置。

通过当前位置减去上一次的位置就可以计算出需要移动的距离了。最后调用panForTransaltion:方法,并将移动距离传入即可。

搞定!编译并运行程序,现在可以通过拖放的方式移动sprite(以及layer)了!

Sprite Kit教程:如何拖放Sprites_第7张图片

在Sprite Kit中如何使用手势识别

在Sprite Kit中还可以使用手势识别来处理触摸!

手势识别可以识别不同的手势,如tap,double tap,swipe或pan。

通过手势识别,我们可以不用写大量的代码来识别不同的手势(如tap,double tap,swipe或pan),只需要创建一个手势识别对象并将其添加到view中,即可进行手势识别。当有手势发生,会有一个回调。

下面就来看看如何在Sprite Kit中使用手势识别。

首先,注释掉触摸处理方法:touchesBegan:withEvent:和touchesMoved:withEvent:(因为要使用不同的处理方法啦)。

然后添加如下方法:

1 2 3 4
- (void)didMoveToView:(SKView *)view {  UIPanGestureRecognizer *gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanFrom:)];  [[self view] addGestureRecognizer:gestureRecognizer]; } 

当scene第一次显示出来时会调用这个方法。在上面的方法中创建了一个pan手势识别器,并用当前的scene来对其做初始化,另外还传入一个callback:handlePanFrom:。接着把这个手势识别器添加到scene中的view里面。

注意:可能你会问为什么要在这里添加识别器,而不是在scene的init方法中。答案很简单:SKScene有一个view属性,保存着SKView——该view用来显示scene,不过只有scene显示到屏幕中时这个属性才会被初始化,所以在init方法被调用时该属性是nil的。此处的didMoveToView:类似于UIKit中的viewDidAppear:,当scene显示出来时,didMoveToView:会被调用。

接着,将下面的代码添加到MyScene.m文件底部:

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 36
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer {  if (recognizer.state == UIGestureRecognizerStateBegan) {   CGPoint touchLocation = [recognizer locationInView:recognizer.view];   touchLocation = [self convertPointFromView:touchLocation];   [self selectNodeForTouch:touchLocation];    } else if (recognizer.state == UIGestureRecognizerStateChanged) {   CGPoint translation = [recognizer translationInView:recognizer.view];  translation = CGPointMake(translation.x, -translation.y);  [self panForTranslation:translation];  [recognizer setTranslation:CGPointZero inView:recognizer.view];   } else if (recognizer.state == UIGestureRecognizerStateEnded) {   if (![[_selectedNode name] isEqualToString:kAnimalNodeName]) {  float scrollDuration = 0.2;  CGPoint velocity = [recognizer velocityInView:recognizer.view];  CGPoint pos = [_selectedNode position];  CGPoint p = mult(velocity, scrollDuration);   CGPoint newPos = CGPointMake(pos.x + p.x, pos.y + p.y);  newPos = [self boundLayerPos:newPos];  [_selectedNode removeAllActions];   SKAction *moveTo = [SKAction moveTo:newPos duration:scrollDuration];  [moveTo setTimingMode:SKActionTimingEaseOut];  [_selectedNode runAction:moveTo];  }   } } 

当手势开始、改变(例如用户持续drag),以及结束时,上面这个callback函数都会被调用。该方法会进入不同的case,以处理不同的情况。

当手势开始时,将坐标系统转换为node坐标系(注意这里没有便捷的方法,只能这样处理)。然后电泳之前写的helper方法selectNodeForTouch:。

当手势发生改变时,需要计算出手势移动的量。还在手势识别器已经为我们存储了手势移动的累计量(translation)!不过考虑到效果的差异,我们需要在UIKit坐标系和Sprite Kit坐标系中对坐标进行转换。

平移(pan)之后,需要把手势识别器上的translation设置为0,否则该值会继续被累加。

当手势结束之后,上面的函数中有一些有趣的代码!UIPanGestureRecognizer可以为我们提供一个移动的速度。通过这个速度可以对node做一个动画——滑动一小点,这样用户可以对node做一个快速的摇动,就像table view上的那种效果一样。

所以,在这里包含的代码用来计算基于速度移动的一个point,然后运行一个moveTo action(为了更加好看,附带SKActionTimingEaseOut效果)。

接着添加如下一个方法到文件中:

1 2 3
CGPoint mult(const CGPoint v, const CGFloat s) {  return CGPointMake(v.x*s, v.y*s); } 

上面这个方法是将滚动的时间乘以速度。

编译并运行程序,现在应该可以用手势识别器滑动和移动动物了。

Sprite Kit教程:如何拖放Sprites_第8张图片

何去何从

本文的代码工程在这里。

至此,你应该知道如何在Sprite Kit程序中使用touch来移动node,以及如何在Sprite Kit中使用手势识别器。

现在,你也可以尝试利用别的手势识别器对上面的工程做扩展处理,例如pinch或rotate手势识别器——可以让猫长大哦!

如果你希望学习更多相关Sprite Kit内容,可以看看这本书:iOS Games by Tutorials。本书会告诉你需要知道的内容——从物理特性,到磁贴地图,以及粒子系统,甚至是制作自己的关卡编辑器。

你可能感兴趣的:(Sprite Kit教程:如何拖放Sprites)