让我们快速开始吧
在实现触摸处理之前,我们先来创建一个基本的Sprite Kit工程,并在scene中显示出一些sprite(动物)和背景。
至于怎么创建工程这里就不再重复讲述了,如确实有问题建议回到 SpriteKit快速入门。
将工程命名为DragDrop
,devices
选择iPhone
,跟
SpriteKit快速入门
一样,我们希望这个程序只支持横屏显示(landscape)。所以在Project Navigator
中选中DragDrop
工程,
然后选择DragDrop target
,在弹出的画面中,只需要勾选上Landscape Left
和Landscape Right
。
在SpriteKit快速入门
我们通过修改-viewDidLoad:
为-viewDidAppear:
来解决屏幕尺寸问题,现在我们采用另一种方法,将-viewDidLoad:
修改为-viewDidLayoutSubviews
。
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
// Configure the view.
SKView * skView = (SKView *)self.view;
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。
突然发现好像一只没有强调这个操作,特意附上一截图。
现在让我们开始coding吧,首先在MyScene.h
添加两个声明:
static NSString * const kAnimalNodeName = @"movable";
@interface MyScene : SKScene
@property (nonatomic, strong) SKSpriteNode *background;
@property (nonatomic, strong) SKSpriteNode *selectNode;
@end
后面会用到上面属性存放背景图片和当前被选中的精灵,还有不要忘记那个静态全局变量,后面会用来用作点击标记的。
现在我们开始修改-initWithSize:
的代码:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
_background = [SKSpriteNode spriteNodeWithImageNamed:@"blue-shooting-stars"];
[_background setName:@"background"];
[_background setAnchorPoint:CGPointZero];
[self addChild:_background];
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;
}
加载背景图片,并将该node的anchor设置为图片的左下角(0, 0),在SpriteKit中设置node的位置,实际上是设置它的anchor, 默认情况下,node的anchor被设置为node的正中间。
加载小动物,为了好的布局,其中各个node根据屏幕的长度来定位,另外还将这些node的名字设置为kAnimalNodeName。 最后将创建好的node加载到
_background
中。
编译运行,可以看见可爱的小动物已经显示在屏幕上了。
触摸的方式选中Sprite
先修改-touchesBegan:
,然后添加-selectNodeForTouch
、degToRad
。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//Called when a touch begins
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
[self selectNodeForTouch:location];
}
}
- (void)selectNodeForTouch:(CGPoint)touchLocation {
//1
SKSpriteNode *touchNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
//2
if (! [_selectNode isEqual:touchNode]) {
[_selectNode removeAllActions];
[_selectNode runAction:[SKAction rotateToAngle:0.0f duration:0.1f]];
_selectNode = touchNode;
//3
if ([[touchNode name] isEqualToString:kAnimalNodeName]) {
SKAction *sequence = [SKAction sequence:@[[SKAction rotateByAngle:degToRad(-4.0f) duration:0.1],
[SKAction rotateByAngle:0.0f duration:0.1],
[SKAction rotateByAngle:degToRad(4.0f) duration:0.1]]];
[_selectNode runAction:[SKAction repeatActionForever:sequence]];
}
}
}
float degToRad(float degree) {
return degree / 180.0f * M_PI;
}
通过scene(self)获得touchLocation位置对应的node。
获取匹配的node后,检查与上次的node是否相同,如果相同的话,在这里直接就返回了。如果是一个新的node或一个未被选中的node,则 出现抖动动画。不过动画启动前需要移除当前已经选中node上的所有running actions,并在这个node上运行一个action,这样做是为了确保 当前只有一个node在做动画,其他node恢复到原样。
判断选中的node知否可以进行动画,如果选中的node可以做动画处理,那么就创建一个sequence action。
触摸的方式移动Sprite和Layer
开始coding前,我们先整理一下思路,通过-touchesBegan:
获取选中node的坐标,-touchesMoved:
获取移动后的坐标,从中计算出
移动的距离,如果选中的动物,则动物移动相应的距离,如果没选中动物,则移动整个Layer。
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
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:
方法,并将移动距离传入即可。
- (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 = [_selectNode position];
if ([[_selectNode name] isEqualToString:kAnimalNodeName]) {
[_selectNode 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:
,首先判断_selectNode
是否为动物,如果是,则根据传入的值将node移动到新的位置,若是background layer,
则调用-boundLayerPos:
将它移动到新的位置。
编译运行,大功告成,小动物和背景都可以移动。
SpriteKit使用手势
你可能会问,SpriteKit是否支持手势?答案是肯定的。SpriteKit肯定支持手势。
通过手势识别,我们可以不用写大量的代码来识别不同的手势(如tap,double tap,swipe或pan), 只需要创建一个手势识别对象并将其添加到view中,即可进行手势识别。当有手势发生,会有一个回调。
下面就来看看如何在Sprite Kit中使用手势识别。
首先我们需要注释触摸处理方法:-touchesBegan:withEvent:
和-touchesMoved:withEvent:
。
- (void)didMoveToView:(SKView *)view {
UIPanGestureRecognizer *gestureRecognizes = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanFrom:)];
[[self view] addGestureRecognizer:gestureRecognizes];
}
当scene第一次显示出来时会调用这个方法。在上面的方法中创建了一个pan手势识别器,并用当前的scene来对其做初始化,
另外还传入一个callback:-handlePanFrom:
。接着把这个手势识别器添加到scene中的view里面。
注意:
可能你会问为什么要在这里添加识别器,而不是在scene的init方法中。SKScene
有一个view属性,保存着SKView
该view用来显示scene,不过只有scene显示到屏幕中时这个属性才会被初始化,所以在init方法被调用时该属性是nil。
此处的-didMoveToView:
类似于UIKit中的-viewDidAppear:
,当scene显示出来时,-didMoveToView:
会被调用。
接着,将下面的代码添加到MyScene.m
添加如下代码:
- (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 tranliation = [recognizer translationInView:recognizer.view];
tranliation = CGPointMake(tranliation.x, -tranliation.y);
[self panForTranslation:tranliation];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
if (! [[_selectNode name] isEqualToString:kAnimalNodeName]) {
float scrollDuration = 0.2f;
CGPoint velocity = [recognizer velocityInView:recognizer.view];
CGPoint pos = [_selectNode position];
CGPoint p = mutil(velocity, scrollDuration);
CGPoint newPos = CGPointMake(pos.x + p.x, pos.y + p.y);
newPos = [self boundLayerPos:newPos];
[_selectNode removeAllActions];
SKAction *moveTo = [SKAction moveTo:newPos duration:scrollDuration];
[moveTo setTimingMode:SKActionTimingEaseOut];
[_selectNode runAction:moveTo];
}
}
}
CGPoint mutil(const CGPoint v, const CGFloat s) {
return CGPointMake(v.x*s, v.y*s);
}
当手势开始、改变(例如用户持续drag),以及结束时,上面这个callback函数都会被调用。该方法会进入不同的case,以处理不同的情况。
当手势开始时,将坐标转变为node坐标系(不要奢望有更便捷的方法),然后就进行node点击判断。
当手势改变时,需要计算出手势的移动量,注意的是平移(pan)之后,将移动设置为0,否则移动量会继续更加。
当手势结束时,UIPanGestureRecognizer可以为我们提供一个移动的速度,这样看起来有一个摇动的效果。 所以,在这里包含的代码用来计算基于速度移动的一个point,然后运行一个moveTo action。
OK!编译运行,现在可以通过手势移动精灵了。