Move a node to finger using Swift + SpriteKit - ios

UPDATE: I have solved the problem, and figured out a more simplified way to do this then the answer provided. My solution was to make the velocity of the SPACESHIP equal the distance it was from my finger touch. For faster movement, you can multiply this velocity by a constant. In this case, I used 16. I also got rid of setting lastTouch to nil in the touchesEnd event. That way, the ship will still stop even when I release my finger.
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if let touch = lastTouch {
myShip.physicsBody.velocity = CGVector(dx: (lastTouch!.x - myShip.position.x) * 16, dy: 0)
}
}
===============================
I have a SPACESHIP node with movement restricted to the X-Axis. When the user PRESSES and HOLDS somewhere on the screen, I want the SPACESHIP to be able to move to the finger's x-coordinate, and not stop moving toward the finger until the finger is RELEASED. If the SPACESHIP is close to the users finger and the users finger is still pressed down, I want it to gradually slow down and stop. I also want this smooth motion to be applied when the SPACESHIP changes direction, starts, and stops.
I am trying to figure out the best way to do this.
So far, I have created the node and it moves correctly, but there is a problem: If I press on the screen and hold down, the ship will eventually cross over my finger and keep moving. This is because the logic to change direction of the ship is only triggered if I move my finger. So essentially, moving my finger over the ship to change the ships' direction works, but if the ship crosses over my still finger, it does't change direction
I need the SPACESHIP node to recognize when it has crossed over my still finger, and either change its direction or stop based on how close it is to my finger.
Here is the relevant code:
Part 1: When the user presses down, find out where the touch is coming from and move myShip (SPACESHIP) accordingly using velocity
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
if (touchLocation.x < myShip.position.x) {
myShip.xVelocity = -200
} else {
myShip.xVelocity = 200
}
}
Part 2 When the user moves their finger, trigger an event that checks to see if the finger has now moved to the other side of the ship. If so, change direction of the ship.
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
//distanceToShip value will eventually be used to figure out when to stop the ship
let xDist: CGFloat = (touchLocation.x - myShip.position.x)
let yDist: CGFloat = (touchLocation.y - myShip.position.y)
let distanceToShip: CGFloat = sqrt((xDist * xDist) + (yDist * yDist))
if (myShip.position.x < touchLocation.x) && (shipLeft == false) {
shipLeft = true
myShip.xVelocity = 200
}
if (myShip.position.x > touchLocation.x) && (shipLeft == true) {
shipLeft = false
myShip.xVelocity = -200
}
}
Part 3 When the user releases their finger from the screen, I want the ship to stop moving.
override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
myShip.xVelocity = 0
}
Part 4 Update event that changes the Ship's position
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let rate: CGFloat = 0.5; //Controls rate of motion. 1.0 instantaneous, 0.0 none.
let relativeVelocity: CGVector = CGVector(dx:myShip.xVelocity - myShip.physicsBody.velocity.dx, dy:0);
myShip.physicsBody.velocity = CGVector(dx:myShip.physicsBody.velocity.dx + relativeVelocity.dx*rate, dy:0);
Thanks for reading, and looking forward to a response!

You can save yourself a lot of trouble by using: myShip.physicsBody.applyImpluse(vector). It works by acting as if you gave myShip a push in the direction vector points. If you calculate vector as the x distance from your last touch location to myShip, then it'll accelerate, decelerate, change direction, etc. pretty close to the way you're describing because it'll be giving it little pushes in the right direction on each update.
Basically you store the last touch location then, in your update function, you calculate the CGVector pointing from myShip to lastTouch and apply that as an impulse to your physics body.
Something like:
var lastTouch: CGPoint? = nil
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
lastTouch = touchLocation
}
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) {
let touch = touches.anyObject() as UITouch
let touchLocation = touch.locationInNode(self)
lastTouch = touchLocation
}
// Be sure to clear lastTouch when touches end so that the impulses stop being applies
override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
lastTouch = nil
}
override func update(currentTime: CFTimeInterval) {
// Only add an impulse if there's a lastTouch stored
if let touch = lastTouch {
let impulseVector = CGVector(touch.x - myShip.position.x, 0)
// If myShip starts moving too fast or too slow, you can multiply impulseVector by a constant or clamp its range
myShip.physicsBody.applyImpluse(impulseVector)
}
}
You'll also probably want to play with the linearDamping and angularDamping values on myShip.physicsBody. They'll help determine how fast myShip accelerates and decelerates.
I maxed out the values at 1.0 in my app:
myShip.physicsBody.linearDamping = 1.0
myShip.physicsBody.angularDamping = 1.0
If myShip doesn't stop fast enough for you, you can also try applying some breaking in your update function:
override func update(currentTime: CFTimeInterval) {
// Only add an impulse if there's a lastTouch stored
if let touch = lastTouch {
let impulseVector = CGVector(touch.x - myShip.position.x, 0)
// If myShip starts moving too fast or too slow, you can multiply impulseVector by a constant or clamp its range
myShip.physicsBody.applyImpluse(impulseVector)
} else if !myShip.physicsBody.resting {
// Adjust the -0.5 constant accordingly
let impulseVector = CGVector(myShip.physicsBody.velocity.dx * -0.5, 0)
myShip.physicsBody.applyImpulse(impulseVector)
}
}

For 2017 here's the easy way to do what is explained in the correct answer here.
There's no need to store the previous position, it is given to you...
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let t: UITouch = touches.first! as UITouch
let l = t.location(in: parent!)
let prev = t.previousLocation(in: parent!)
let delta = (l - prev).vector
physicsBody!.applyImpulse(delta)
}
That's it.
Two notes. (A) properly you should divide the delta distance by the deltaTime to get the correct impulse. If you're a hobbyist really just multiply by "about 100" and you'll be fine. (B) note that of course you will need an extension or function to convert CGPoint to CGVector, it's impossible to do anything without that.

In your thuchesBegan and touchesMoved store the touch location as the "target". In the update then check the position of your ship and reset the xVelocity to 0 if the ship has reached/passed the target.
Since you are only interested in the x coordinate you could also store just touchLocation.x. You can also reverse the velocity but I think that would look strange. Note that if the user moves the finger again, your ship will start moving again because the touchMoved will be triggered again.
On a side note, within touchesMoved you are also setting the shipLeft property but this is not set in your touchesBegan. If this property is used elsewhere you should sync its use.

Related

How to replicate iOS home screen behaviour?

I am currently working on a game in SpriteKit, where I need to move a sprite in response to touch (i.e when user swipes or pans anywhere in SKView.
I want to get the direction of pan (for swipe I know how to do it),so that the sprite will move according to pan (I have a path defined for the sprite if user pans or according to swipe if user swipes), the way touch in iOS appdrawer works i.e it responds to slightest of swipes and also pans (i.e when you pan forwards or backwards, it makes a decision whether you want to move to the next screen or not).
Is there any documentation or so? (I have gone through the UIGestureRecognizer documentation, but I haven't been able to find a way to implement it.)
I use something similar on my MenuScene, I have 3 pages setup that the user can scroll through to get various game data. But I don't want the slightest touch to move the screen, it would be to jarring for the user. So I just watch the finger movements in the Touches functions and check if the movement is greater that an amount I designate as the minimum move amount and if it is greater than I scroll the page. In your case you could handle it as; if it is greater than the minimum move amount treat as a pan else treat it as a swipe
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
initialTouch = touch.location(in: self.view!)
moveAmtY = 0
moveAmtX = 0
initialPosition = menuScroller.position
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let movingPoint: CGPoint = touch.location(in: self.view!)
moveAmtX = movingPoint.x - initialTouch.x
moveAmtY = movingPoint.y - initialTouch.y
//their finger is on the page and is moving around just move the scroller and parallax backgrounds around with them
//Check if it needs to scroll to the next page when they release their finger
menuScroller.position = CGPoint(x: initialPosition.x + moveAmtX, y: initialPosition.y)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
//they havent moved far enough so just reset the page to the original position
if fabs(moveAmtX) > 0 && fabs(moveAmtX) < minimum_detect_distance {
resetPages()
}
//the user has swiped past the designated distance, so assume that they want the page to scroll
if moveAmtX < -minimum_detect_distance {
moveLeft()
}
else if moveAmtX > minimum_detect_distance {
moveRight()
}
}

Distinguish Swipe from Touch in Spritekit - Swift 3

I am currently working on an arcade app where the user taps for the sprite to jump over an obstacle and swipes down for it to slide under an obstacle. My problem is that when I begin a swipe the touchesBegan function is called so the sprite jumps instead of sliding. Is there a way to distinguish these two?
You can use a gestures state to fine tune user interaction. Gestures are coordinated, so shouldn't interfere with each other.
func handlePanFrom(recognizer: UIPanGestureRecognizer) {
if recognizer.state != .changed {
return
}
// Handle pan here
}
func handleTapFrom(recognizer: UITapGestureRecognizer) {
if recognizer.state != .ended {
return
}
// Handle tap here
}
How about using a slight delay for your touch controls? I have a game where I do something similar using a SKAction with delay. Optionally you can set a location property to give your self a bit of wiggle room with the touchesMoved method incase someone has a twitchy finger (thanks KnightOfDragon)
let jumpDelayKey = "JumpDelayKey"
var startingTouchLocation: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
// starting touch location
startingTouchLocation = location
// start jump delay
let action1 = SKAction.wait(forDuration: 0.05)
let action2 = SKAction.run(jump)
let sequence = SKAction.sequence([action1, action2])
run(sequence, withKey: jumpDelayKey)
}
}
func jump() {
// your jumping code
}
Just make sure the delay is not too long so that your controls dont feel unresponsive. Play around with the value for your desired result.
Than in your touches moved method you remove the SKAction if your move threshold has been reached
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
guard let startingTouchLocation = startingTouchLocation else { return }
// adjust this value of how much you want to move before continuing
let adjuster: CGFloat = 30
guard location.y < (startingTouchLocation.y - adjuster) ||
location.y > (startingTouchLocation.y + adjuster) ||
location.x < (startingTouchLocation.x - adjuster) ||
location.x > (startingTouchLocation.x + adjuster) else {
return }
// Remove jump action
removeAction(forKey: jumpDelayKey)
// your sliding code
}
}
You could play around with Gesture recognisers although I am not sure that will work and how it affects the responder chain.
Hope this helps

moving sprite based on drag length/direction

In my game I have a node on the bottom of the screen that id like to move along the x axis using touch. I'd like for my node to move left or right depending on the direction of the drag, and to also move the same distance as the drag. So if the user drags from left to right (CGPoint(x: 200, y: 500)toCGPoint(x:300, y: 500)) the node would move 100 to the right. This is what I've tried to do, but it didn't work. If anyone has a way to fix this I'd really appreciate it
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
firstTouch = touchLocation
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
secondTouch = touchLocation
if gameStarted {
let change = secondTouch.x - firstTouch.x
let move = SKAction.moveToX(greenGuy.position.x + change, duration: 0.1)
greenGuy.runAction(move)
}
}
Update your touchesMoved with the following code:
let touch = touches.first as! UITouch
let touchLocation = touch.locationInNode(self)
secondTouch = touchLocation
if gameStarted {
let change = secondTouch.x - firstTouch.x
//Update greenGuys position
greenGuy.position = CGPoint(x: greenGuy.position.x + change, y:greenGuy.position.y)
//Update the firstTouch
firstTouch = secondTouch
}
The reason not using an SKAction as I commented before is because we don't know how much time will pass between two calls of touchesMoved method, so we don't know exactly what time to input in SKAction duration.
You have a pretty good start. First, change:
let move = SKAction.moveToX(greenGuy.position.x + change, duration: 0.1)
to:
let move = SKAction.moveByX(greenGuy.position.x + changeInX, duration: moveDuration)
If you want 2-dimensional movement, use the SKAction moveByX:ChangeInX y:ChangeInY duration:moveDuration instead. Now, you have a few more variables that are based on the duration/distance of the swipe. The duration that you will choose for moveDuration will be up to you, and it will be the product of some coefficient and the swipe distance.
To get the swipe distance:
Id advise you to ditch the touches method and use a UIGestureRecognizer. The one you need is UIPanGestureRecognizer.
Here's a useful link detailing its use: UISwipeGestureRecognizer Swipe length .
Essentially it has different states that get set when the user starts or ends a swipe/drag motion. Then, you could take the locationInView at those moments and calculate the distance between them :D
Hope I helped. I know touchesMoved is also a way to do it, but Ive had problems with it in the past (unnecessary lagging and uncertainty) and gesture recognizers are much more intuitive and easier to use.

iOS Spritekit intersectsnode does not work for SKShapeNode

I am using the iOS SpriteKit Game Swift template on iOS8.3. I am trying to use the function func intersectsNode(_ node: SKNode) -> Bool to detect overlap of two circles created as SKShapeNodes. Turns out the function does not detect the intersection if its SKShapeNodes. But on further troubleshooting, it turns out the function works if I use the default Spaceship sprite of the template. Here is the code which works:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
let sprite = SKSpriteNode(imageNamed:"Spaceship")
sprite.xScale = 0.3
sprite.yScale = 0.3
sprite.position = location
self.circles.append(sprite)
self.addChild(sprite)
if circles.count == 1 {
println("One circle")
} else {
for var index = 0; index < circles.count-1; ++index {
if sprite.intersectsNode(circles[index]) {
println(circles[index].frame)
println("Circle intersects another")
}
}
}
}
}
When two sprites overlap in the above code the function returns YES and prints the intersection string. Here is the code which does NOT work:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
let sprite = SKShapeNode(circleOfRadius: 50)
sprite.position = location
self.circles.append(sprite)
self.addChild(sprite)
if circles.count == 1 {
println("One circle")
} else {
for var index = 0; index < circles.count-1; ++index {
if sprite.intersectsNode(circles[index]) {
println(circles[index].frame)
println("Circle intersects another")
}
}
}
}
}
As you can see the code blocks are completely identical except in one case its an SKSpriteNode and the other case its SKShapeNode. I also printed the frames of the SKShapeNode Circles and I can see they have have valid frames. So I am puzzled by this as I would like to use SKShapeNodes in my code for now but I cannot use the intersectNode function as it does not work.
The documentation for intersectsNode says this:
The two nodes are considered to intersect if their frames intersect. The children of both nodes are ignored in this test.
Which means you won't get the result you want when using circles.
However, checking if two circles overlap is as easy as checking if the distance between their centers is less than the sum of their radiuses.

SKLabelNode will disappear but is still clickable

I am making a game using SpriteKit and Swift, running Xcode 6. I have an SKLabelNode, let's call it myLabelNode for this example. When I call myLabelNode.removeFromParent() it removes the node from the scene, as it should. The node count drops by 1, and it isn't visible anywhere on the screen. However, when I click the spot where myLabelNode previously was, my program will still call out the function that should only happen when myLabelNode is touched. I also tried combining myLabelNode.removeFromParent() with myLabelNode.hidden = true, but it is still touchable, and calls the function even though it shouldn't. How should I fix this? Is there a different method I should be using? Is this supposed to happen?
Edit:
let lemonadeLabel = SKLabelNode(fontNamed: "Optima-ExtraBlack")
override func didMoveToView(view: SKView) {
lemonadeLabel.text = "Lemonade Stand"
lemonadeLabel.fontSize = 24
lemonadeLabel.fontColor = SKColor.yellowColor()
lemonadeLabel.position = CGPoint(x: size.width/2, y: size.height*0.66)
lemonadeLabel.zPosition = 2.0
addChild(lemonadeLabel)
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self)
if lemonadeLabel.containsPoint(location) {
println("lemonadeLabel pressed")
lemonadeLabel.removeFromParent()
/*lemonadeLabel is now be removed,
however if I click the area where it
used to be, "lemonadeLabel pressed"
will print to the console*/
}
}
You are trying to determine if the constrainPoints' location are being touched. Even if you remove the label from the scene, it is still an object in memory, i.e: you could re-ad it later.. it still has all it's properties including position, etc..
I would try this instead:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
if nodeAtPoint(touch.locationInNode(self)) == lemonadeLabel {
println("lemonadeLabel pressed")
lemonadeLabel.removeFromParent()
}
}
}
You basically determine if the lemonadeLabel is the node at that position, if yes you remove it. Since you compare with the added node in the scene, if it's gone, it will not be there for comparison ;)
Your labelNode may not be inside the SKScene anymore. This does not mean that it will not respond to the containsPoint function. The labelNode still has a position assigned to it and it can calculate if a point falls inside it using containsPoint function.
Instead you can try this.
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) === lemonadeLabel {
println("lemonadeLabel pressed")
lemonadeLabel.removeFromParent()
}
}

Resources