Multi-Touch Gesture in Sprite Kit

Multi-touch gesture in Sprite Kit

Moving multiple nodes simultaneously is fairly straightforward. The key is to track each touch event independently. One way to do that is to maintain a dictionary that uses the touch event as the key and the node being moved as the value.

First, declare the dictionary

var selectedNodes:[UITouch:SKSpriteNode] = [:]

Add each sprite touched to the dictionary with the touch event as the key

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in:self)
if let node = self.atPoint(location) as? SKSpriteNode {
// Assumes sprites are named "sprite"
if (node.name == "sprite") {
selectedNodes[touch] = node
}
}
}
}

Update the positions of the sprites as needed

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in:self)
// Update the position of the sprites
if let node = selectedNodes[touch] {
node.position = location
}
}
}

Remove the sprite from the dictionary when the touch ends

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if selectedNodes[touch] != nil {
selectedNodes[touch] = nil
}
}
}

Enabling Sprite Kit Multiple Touch

iOS tracks each touch event throughout its lifecycle. In other words, when the user touches the screen, a touch object is created and passed to touchesBegan. If the user's finger moves, the object is updated with the new touch location and passed to touchesMoved. When the user's finger is no longer touching the screen, the object is passed to touchesEnded.

The following demonstrates one way to implement multi-touch in SpriteKit. Since multiple touches are involved, this code must be tested on an actual iOS device.

First, create and add sprites to the scene (for testing purposes).

- (void)didMoveToView:(SKView *)view {    
for (int i=-2;i<3;i++) {
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
[sprite setScale:0.25];
sprite.position = CGPointMake(0, i * 75);
[self addChild:sprite];
}
}

Next, use the touch's previous location to determine which sprite is being dragged and move the sprite to the touch's current location. By looping over the touch events in the touches, we can move multiple sprites at the same time with different fingers.

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
CGPoint previousLocation = [touch previousLocationInNode:self];
SKNode *node = [self nodeAtPoint:previousLocation];
if (node != self) {
node.position = location;
}
}
}

determine multitouch under touchesEnded event

It looks like i simply needed to do a

[[event allTouches] count]

to actually get the touch count.

Swift 3.0 - Sprite Kit - Multitouch

The problem you are facing is that you are mixing the contexts for your touches. This is making things more difficult and complicated than they need to be.

The easiest thing to do would be to make your virtual joystick a separate SKSpriteNode class that tracks its own touches and reports them. Same with the buttons - they track their own touches and report their state.

But if you want to continue with your current approach of having a high-level object track multiple touches, what you want to do is capture the context that each touch is associated with in touchesBegan, and then just update things on touchesMoved as necessary, canceling the touches in touchesEnded.

For instance, you want to associate a particular touch with the virtual joystick, because you don't want weirdness if they drag their finger off of it and over to the button, say. And you want to know exactly which touch is lifted off when the user lifts a finger.

Here's some sample code that should illustrate the process:

 //
// This scene lets the user drag a red and a blue box
// around the scene. In the .sks file (or in the didMove
// function), add two sprites and name them "red" and "blue".
//

import SpriteKit
import GameplayKit

class GameScene: SKScene {

private var redTouch:UITouch?
private var blueTouch:UITouch?

override func didMove(to view: SKView) {
super.didMove(to: view)
isUserInteractionEnabled = true
}

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

// Grab some references to the red and blue sprites.
// (They must be direct children of the scene, or the
// paths must be amended.)
guard let redBox = childNode(withName: "red") else { return }
guard let blueBox = childNode(withName: "blue") else { return }

for touch in touches {
// Get the location of the touch in SpriteKit Scene space.
let touchLocation = touch.location(in: self)
// Check to see if the user is touching one of the boxes.
if redBox.contains( touchLocation ) {
// If we already have a touch in the red box, do nothing.
// Otherwise, make this our new red touch.
redTouch = touch
} else if blueBox.contains( touchLocation ) {
// If we already have a touch in the blue box, do nothing.
// Otherwise, make this our new blue touch.
blueTouch = touch
}
}

}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
// We have already established which touches are active,
// and we have already tied them to the two contexts, so
// we just need to read their current location and update
// the location of the red and blue boxes for the touches
// that are active.
if let redTouch = redTouch {
guard let redBox = childNode(withName: "red") else { return }
let location = redTouch.location(in:self)
redBox.position = location
}
if let blueTouch = blueTouch {
guard let blueBox = childNode(withName: "blue") else { return }
let location = blueTouch.location(in:self)
blueBox.position = location
}
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// The parameter touches contains a list of ending touches,
// so we check the touches we are currently tracking to
// see if they are newly lifted. If so, we cancel them.
if let touch = redTouch {
if touches.contains( touch ) {
redTouch = nil
}
}
if let touch = blueTouch {
if touches.contains( touch ) {
blueTouch = nil
}
}
}

}

In the above code, we have separated out the touches on the red box and the blue box. We always know which touch is dragging the red box around and which touch is dragging the blue box around, if any. This is a simple example, but it's generalizable to your situation, where you'd have touches for the virtual joystick and each individual button.

Note that this approach works well for multitouch elements, too. If you have a map that you want to be zoomable, you can keep track of two touches so that you can compare them for pinch gestures. That way, if your pinch gesture accidentally strays over a button, you've already marked it as part of the pinch gesture, and know not to start triggering that button.

But again, a better approach would be to have a separate SKSpriteNode subclass that just tracks the joystick touches and reports its state to some higher-level manager class. You already know everything you need to know to do this - it's like what you have without all the extra checking to see if there are other buttons pressed. Same with the buttons. The only new part would be messaging "up the chain" to a manager, and that's pretty straightforward to deal with.

How to disable second touch in an SpriteKit game?

You want to implement UIPanGestureRecognizer in your scene. It will allow you to track the location of the user's touch and at the same time control other "stray" touches: UIPanGestureRecognizer Documentation

After you initialize it, you need to implement a method to handle the user's pans. You will have to set flags inside of this method to control when the swipe started/ended. I think this answer on StackOverflow gave a really good explanation of using it (with Swift). BTW, when you initialize it, you should set the gesture recognizer's property maximumNumberOfTouches to 1 (that will cause it to ignore other touches while the user is panning).

The trickier part will be to translate the same code you wrote before to gesture recognizer. The difference is that your handler will be called only once for each "swipe" or "pan", while the touches method you are using now is called each time there is a "touch". There are a few ways to proceed at this point, and you could try whatever you like, but I think that this would be the easiest way to go once you have your gesture recognizer set up (spoiler):

  1. make sure the gesture recognizer is an instance variable so you can access it from all methods.

  2. go to the update: method and make an if statement that checks if gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged

  3. use the same algorithm that you had before in this if statement. To check the location that the touch is at use the method: locationInView:. Use self.view as the parameter.

Hope this helped! good luck.

How to recognize continuous touch in Swift?

The most difficult thing about this process is tracking single touches within a multitouch environment. The issue with the "simple" solution to this (i.e., turn "istouched" on in touchesBegan and turn it off in touchesEnded) is that if the user touches another finger on the screen and then lifts it, it will cancel the first touch's actions.

To make this bulletproof, you need to track individual touches over their lifetime. When the first touch occurs, you save the location of that touch and move the object towards that location. Any further touches should be compared to the first touch, and should be ignored if they aren't the first touch. This approach also allows you to handle multitouch, where the object could be made to move towards any finger currently on the screen, and then move to the next finger if the first one is lifted, and so on.

It's important to note that UITouch objects are constant across touchesBegan, touchesMoved, and touchesEnded. You can think of a UITouch object as being created in touchesBegan, altered in touchesMoved, and destroyed in touchesEnded. You can track the phase of a touch over the course of its life by saving a reference to the touch object to a dictionary or an array as it is created in touchesBegan, then in touchesMoved you can check the new location of any existing touches and alter the object's course if the user moves their finger (you can apply tolerances to prevent jitter, e.g., if the x/y distance is less than some tolerance, don't alter the course). In touchesEnded you can check if the touch in focus is the one that ended, and cancel the object's movement, or set it to move towards any other touch that is still occurring. This is important, as if you just check for any old touch object ending, this will cancel other touches as well, which can produce unexpected results.

This article is in Obj-C, but the code is easily ported to Swift and shows you what you need to do, just check out the stuff under "Handling a Complex Multitouch Sequence": https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/multitouch_background/multitouch_background.html



Related Topics



Leave a reply



Submit