Kingyeung Chan

THINK GREAT THOUGHTS AND YOU WILL BE GREAT

大家好,我系Monster.Chan,一名来自中国的 iOS / AWS / Unity3D 开发者,就职于SAINSTORE。在不断修炼,努力提升自己


结合工作经验,目前写了《Getting Started with AWS》、《Websites & Web Apps on AWS》,欢迎试读或者购买

SpriteKit-拖放Sprites

让我们快速开始吧

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

至于怎么创建工程这里就不再重复讲述了,如确实有问题建议回到 SpriteKit快速入门

将工程命名为DragDropdevices选择iPhone,跟 SpriteKit快速入门 一样,我们希望这个程序只支持横屏显示(landscape)。所以在Project Navigator中选中DragDrop工程, 然后选择DragDrop target,在弹出的画面中,只需要勾选上Landscape LeftLandscape 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;
}
  1. 加载背景图片,并将该node的anchor设置为图片的左下角(0, 0),在SpriteKit中设置node的位置,实际上是设置它的anchor, 默认情况下,node的anchor被设置为node的正中间。

  2. 加载小动物,为了好的布局,其中各个node根据屏幕的长度来定位,另外还将这些node的名字设置为kAnimalNodeName。 最后将创建好的node加载到_background中。

编译运行,可以看见可爱的小动物已经显示在屏幕上了。

触摸的方式选中Sprite

先修改-touchesBegan:,然后添加-selectNodeForTouchdegToRad

-(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;
}
  1. 通过scene(self)获得touchLocation位置对应的node。

  2. 获取匹配的node后,检查与上次的node是否相同,如果相同的话,在这里直接就返回了。如果是一个新的node或一个未被选中的node,则 出现抖动动画。不过动画启动前需要移除当前已经选中node上的所有running actions,并在这个node上运行一个action,这样做是为了确保 当前只有一个node在做动画,其他node恢复到原样。

  3. 判断选中的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!编译运行,现在可以通过手势移动精灵了。

代码下载

最近的文章

市场模型和Hurst指数

投资者总是希望能够弄清楚他们投资市场的特性。他们急需要一个能够有效描述市场规律的理论模型。于是上世纪中后期数学界大牛和金融界大牛进行了第一次研究,这就是首次把统计分析的方法应用于股票收益。这就是现代技术分析和量化金融分析的起源。股票收益率序列波动的数学期望值总为零,这为有效市场假说(Efficient Markets Hypothesis,以下简称EMH)奠定了理论基础。作为现代金融理论基石的EMH中,最重要的是确认了奥斯本提出了随机漫步理论。奥斯本认为股票价格的变化类似于化学中的分子布朗...…

继续阅读
更早的文章

SpriteKit-动画与纹理图集

这次我们使用SpriteKit框架创建一个简单的动画:在屏幕上行走的熊。还会使用到纹理图集来制作动画效果,通过在屏幕上点击,触发事件让熊移动,并且改变熊的运动方向。创建一个工程我们先来创建一个SpriteKit的默认工程,如下图: 并将工程命名为AnimatedBear,将Devices选为iPad为了让熊有更多的空间行走。现在我们直接编译运行,点击屏幕你会看见一个自由旋转的飞机在屏幕上:好的,我们创建的工程没有问题,现在我们开始大改造,首先我们先下载熊的动画资源。解压后将BearIma...…

继续阅读