Zooming and scrolling in SpriteKit
Asked Answered
L

5

15

I am new to SpriteKit. I need to display an image via UIImageView OR SpriteNode (it is a background image for a game). However, I need the user to be able to zoom in to the background and pan around. I accomplished this easily without using SpriteKit but cannot figure out how to make a node accept zoom and pan. If I use UIScrollView in the storyboard, all the sprites added in my scene don't become children and will not zoom or pan.

Is it possible and can you point me in the right direction?


EDIT:

I'm a little confused by your answer, but here's what I did:

In my root view controller .m:

- (void)viewDidLoad
{
    [super viewDidLoad];
    SKView * showDesigner = (SKView *)self.view;
    SKScene * scene = [iMarchMyScene sceneWithSize:showDesigner.bounds.size];
    scene.scaleMode = SKSceneScaleModeAspectFill;
    [showDesigner presentScene:scene];
}

In my scene .m: (your steps are commented)

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {

        //create SKView (step 1)
        SKView *skview_field = [[SKView alloc]initWithFrame:(CGRectMake(0, 0, size.width, size.height))];

        //create UIScrollView with same dimensions as your SpriteKit view (step 2)
        UIScrollView* uiscrollview_field = [[UIScrollView alloc]initWithFrame:skview_field.frame];

        //initialize the field (field is the game's background node)
        SKSpriteNode *field = [SKSpriteNode spriteNodeWithImageNamed:@"Field"];
        field.position = CGPointMake(CGRectGetMidX(self.frame), (CGRectGetMidY(self.frame)));
        field.size = CGSizeMake(self.size.width, self.size.height / 3);

        //create a UIView (called scrollViewContent) with dimensions of background node (step 3)
        UIView* scrollViewContent = [[UIView alloc]initWithFrame:field.frame];

        //set UIScrollView contentSize with same dimensions as your scrollViewContent and add a scrollContentview as your UISCrollView subview (step 4)
        [uiscrollview_field setContentSize:(CGSizeMake(scrollViewContent.frame.size.width, scrollViewContent.frame.size.height))];
        scrollViewContent.frame = field.frame;
        [uiscrollview_field addSubview:scrollViewContent];



        //[self addChild:field]; -- my original code
        //.....does a bunch of other inits

        return self;
}

And here's where I get lost: Step 5: Now, to your root view add your SKView, and UIScrollView on top of SKView.

If I understood you correctly, I need to go to myRootViewController.m and in the viewDidLoad method, add the SKView and UIScrollView (which are initialized in myScene.m) as subviews to SKView initialized here in the viewDidLoad method? I don't see how to do that part.

Lynnelynnea answered 29/9, 2013 at 19:22 Comment(1)
I would use actions, they solved my scaling issue, and seems where sprite kit holds all its power!Caplin
S
16

Here is my solution to the scrolling problem. It revolves around "stealing" the behaviour from the UIScrollView. I learned this from a WWDC Video from 2012 about mixing UIKit with OpenGL.

  1. Add the UIScrollViewDelegate methods to your ViewController and set the scroll view delegate to the ViewController
  2. Add/Move the PanGestureRecognizer from the UIScrollView to the SKView
  3. Use the scrollViewDidScroll callback-method to control whatever node your want when the user scrolls

Example project: https://github.com/bobmoff/ScrollKit

Like I mentioned in a comment above, I am experiencing some tiny lag about 30% of the times I am running the app. The FPS is still 60 but there seem to be some slight conflict or something with how often the delegate method is called, as it sometimes feel a little laggy. If anyone manages to solve this issue, I would love to hear about it. It seems to be only when I am holding my finger down, it never lags when the deceleration is happening.

Substage answered 5/10, 2013 at 12:44 Comment(12)
I would like to try this but using actions etc its pretty easy to add a your own custom scrolling - and its really smooth.Caplin
Yes @Smick, It's pretty easy to just hook up tochesMoved and make nodes move but how do you handle deceleration and bouncing which is, in my opinon, the hard part of get a good enough scroll feeling.Substage
just gave it a spin, actually works pretty good. Thanks for the tip! Now to try the pinch / zoom ??? .....Caplin
think there is a bug - SKScene: Setting the scale of a SKScene has no effect! But it shrinks ok ...Caplin
I dont believe its possible to the change the position of the SKScene. I tried it before and got a warning message, that the scenes position should not be altered during runtime or something like that, so I its better to change the position of a SKNode or any decendant to that class.Substage
so you cannot really use it as you have to use, - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return [_scene view]; } , i got it it zooming in and out but its not a desirable effect.Caplin
This seems to be a solution for zooming an entire scene, with all the nodes in it zooming at the same time, right? What if you only want the background node to zoom, and leave all of the other nodes at higher zPositions unzoomed?Haver
This was only a solution to the scrolling problem. But zooming might be handled the same way. Create a UIView that is the same size as the node you want to zoom and add that into the scrollview and also return that view in the viewForZoomingInScrollView scrollview-delegate method. I have only tried this for Cocos2d but it should be same I guess.Substage
any chance you can upload this working? I've been hacking away at this though for a few hours now and i simply can't get it working.Flatter
I will create a clean project with it and put it on github. But my batteries are dying on my laptop and I forgot the charger-cable at a client, so wont be able to do it until tomorrow.Substage
I went over to our office and picked up another one. Here is an example project where the scrollview is setup and working: github.com/bobmoff/ScrollKitSubstage
@Substage It is a fantastically well written project, thanks you for this. I went through and copied it in manually to understand it better and I can't help but envy how well organised and flexible your solution is.Tonality
T
4

Justin,

you can use a little trick, I am using it and it works great for me. Here is the setup:

  • create SKView
  • create UIScrollView with same dimensions as your SpriteKit view
  • create a UIView (lets name it scrollContentView) with same dimensions as your background node
  • set your UIScrollView contentSize with same dimensions as your scrollContentView and add scrollContentView as your UIScrollView subview

Now, to your root view (UIView) add your SKView, and UIScrollView on top of SKView. It goes something like this (view controller):

self.scrollView              = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds.size];
self.scrollContentView       = [[UIView alloc] init];
self.scrollView.delegate     = self;

self.spriteKitView           = [[SKView alloc] initWithFrame:[UIScreen mainScreen].bounds.size];
self.scrollContentView.frame = self.scene.bacgroundNode.frame;
self.scrollView.contentSize  = self.scrollContentView.frame.size;

[self.scrollView addSubview:self.scrollContentView];

[self.view addSubview:self.spriteKitView];
[self.view addSubview:self.scrollView];

[self.spriteKitView presentScene:self.scene];


Last part of the trick is to implement UIScrollViewDelegate:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

  //here you set your background node position based on scrollView contentoffset
}

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {

  //here you set background node scale based on scrollView zoomScale
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {

  //just return scrollContentView
}

This way you can simulate SKView inside UISCrollView. Hope this helps.

EDIT 2014-04-20

I have open sourced my component for panning and scrolling scenes in SpriteKit, please have a look at: https://github.com/pzbyszynski/PIOSpriteKit

Tabber answered 30/9, 2013 at 14:4 Comment(5)
I am using this method but sometimes (I cant find a pattern for when it happens) when I hold my finger down and moves it around the object that I am moving starts stuttering. The framerate is still 60fps, but its lika the delegate method only recieves about half of the amount of calls as it usually does, so it looks like the object that I am moving have around 10-15fps. Would love it if someone would solve that.Substage
I tried your solution, but got a little stuck. I edited my original post with code snips and where I got lost. Any help I would appreciate.Lynnelynnea
Justin, it seems strange to me that you initialize 2 instances of SKView. I think that in your scene you should setup only scene child nodes. Move your UIScrlooView setup code to your root view controller, and use only one SKView (I will edit my answer with some code snippets in just a moment)Tabber
@PiotrZbyszynski i just do not get this to work correctly, would you be able to provide a full code example how you do this, incl. ViewController and scene h&m files?Junkie
Wouldn't, with this method, the scrollView "absorb" the touches and prevent the scene to sensing any touch?Lubricity
I
3

If you are using an SKCameraNode you can calculate the difference between two touches to define zooming and panning at once.

  • Define an SKCameraNode for your SKView
  • Check if there are two touches
  • If so, calculate the difference between the previous touches and current touches
  • Tell the camera to scale by that much
  • Pan by the camera by either the average of the two points, or the first one (if there's only one)

Result should be something like this. Note that I used the event?.allTouches instead of the received set, if one finger doesn't move between touches, it's not in the touches set, which would cause a pan where a user would expect a zoom.

let cameraNode = SKCameraNode()

override func didMove(to view: SKView) {

    cameraNode.position = CGPoint(x: size.width / 2, y: size.height / 2)
    addChild(cameraNode)
    camera = cameraNode
}

override func touchesMoved(_ touch: Set<UITouch>, with event: UIEvent?) {

    guard let touches = event?.allTouches else {
        return
    }

    let arr = Array(touches)
    guard let touch = arr.first else {
        return
    }

    var positionInScene = touch.location(in: self)
    var previousPosition = touch.previousLocation(in: self)

    if touches.count > 1 {

        let touch2 = arr[1]

        let positionInScene2 = touch2.location(in: self)
        let previousPosition2 = touch2.previousLocation(in: self)

        let oldDistance = distance(previousPosition, previousPosition2)
        let newDistance = distance(positionInScene, positionInScene2)

        let diff = (oldDistance / newDistance)

        if diff.isNormal && diff != 1 {

            let scaleAction = SKAction.scale(by: diff, duration: 0)
            cameraNode.run(scaleAction)
        }

        previousPosition = average(previousPosition, previousPosition2)
        positionInScene = average(positionInScene, positionInScene2)
    }

    let translation = CGPoint(x: positionInScene.x - previousPosition.x, y: positionInScene.y - previousPosition.y)

    let panAction = SKAction.moveBy(x: -translation.x, y: -translation.y, duration: 0)
    cameraNode.run(panAction)
}

func average(_ a: CGPoint, _ b: CGPoint) -> CGPoint {

    return CGPoint(x: (a.x + b.x) / 2, y: (a.y + b.y) / 2)
}

func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {

    let xDist = a.x - b.x
    let yDist = a.y - b.y
    return CGFloat(sqrt((xDist * xDist) + (yDist * yDist)))
}
Ischia answered 7/12, 2017 at 10:33 Comment(0)
J
1

You could start with this Ray W tutorial SpriteKit tutorial how to drag and drop that includes some of the things you ask for including Gesture recognizers.

This is good: Apple gesture Recognizer reference

Another from Ray W: uigesturerecognizer-tutorial-in-ios-5-pinches-pans-and-more

The above is good to start with.

Junkie answered 29/9, 2013 at 19:58 Comment(2)
u welcome, BTW i do not think scrollView works for SpriteKit so i guess you must cater for it yourself with Gesture recs..Junkie
im trying to zoom at present on the scene node, using setScale, but then it messes up all the physics. The sprites are all scaled as expected but the mass of object remains the same..Caplin
H
1

I have created my own method for zooming a specific node without having to zoom the entire scene and this is the basics of it, however it is not perfect (and in fact I have created its own help request here: Zooming an SKNode inconsistent )

This if statement goes in the touchesMoved method of the scene:

   if (touches.count == 2) {
        // this means there are two fingers on the screen
        NSArray *fingers = [touches allObjects];
        CGPoint fingOneCurr = [fingers[0] locationInNode:self];
        CGPoint fingOnePrev = [fingers[0] previousLocationInNode:self];
        CGPoint fingTwoCurr = [fingers[1] locationInNode:self];
        CGPoint fingTwoPrev = [fingers[1] previousLocationInNode:self];

        BOOL yPinch = fingOneCurr.y > fingOnePrev.y && fingTwoCurr.y < fingTwoPrev.y;
        BOOL yUnpinch = fingOneCurr.y < fingOnePrev.y && fingTwoCurr.y > fingTwoPrev.y;

        BOOL xPinch = fingOneCurr.x > fingOnePrev.x && fingTwoCurr.x < fingTwoPrev.x;
        BOOL xUnpinch = fingOneCurr.x < fingOnePrev.x && fingTwoCurr.x > fingTwoPrev.x;

        if (xUnpinch | yUnpinch) {
            if (YES) NSLog(@"This means an unpinch is happening");
            mapScale = mapScale +.02;
            [map setScale:mapScale];
        }

        if (xPinch | yPinch) {
            if (YES) NSLog(@"This means a pinch is happening");
            mapScale = mapScale - .02;
            [map setScale:mapScale];
        }
    }

The only problem I am having with it is inconsistent behavior and unsmooth scrolling. I use the word 'only' in that sense with great liberalness.

Haver answered 19/10, 2013 at 2:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.