Move SKSpriteNode with UIPanGestureRecognizer continuously - ios

I am trying to move an SKSpriteNode continuously in a simple 2-dimensional SKScene. This involves both moving an array containing all background-nodes in the opposite direction of a UIGestureRecognizer's pan and animating the node at the same time.
I have already realized the movement by implementing the nodes and recognizer and calling its method when panning:
#objc func didPan(gesture: UIPanGestureRecognizer) {
let pan = gesture.velocity(in: self.view)
if gesture.state == .changed {
for sprites in levelSprites {
if pan.x > 0 && pan.x > pan.y {
sprites.run(SKAction.moveTo(x: sprites.position.x - 40, duration: 0.5))
self.player.run(SKAction(named: "WalkRight")!)
} else if pan.x < 0 && pan.x < pan.y {
sprites.run(SKAction.moveTo(x: sprites.position.x + 40, duration: 0.5))
self.player.run(SKAction(named: "WalkLeft")!)
} else if pan.y < 0 && pan.x > pan.y {
sprites.run(SKAction.moveTo(y: sprites.position.y - 30, duration: 0.5))
self.player.run(SKAction(named: "WalkUp")!)
} else if pan.y > 0 && pan.x < pan.y {
sprites.run(SKAction.moveTo(y: sprites.position.y + 30, duration: 0.5))
self.player.run(SKAction(named: "WalkDown")!)
}
}
}
}
The problem I'm facing is that this method only moves and animates the sprites for the value I set and the limited time that I have to maintain to accurately match the animations length, when I actually want to move them for the duration of the pan.
I deliberately implemented a recognizer for pan indstead of swipes, scince the scene will also contain a joystick for controlling the movement later on.
Is there maybe an easier solution to what I'm trying to accomplish not using a UIGestureRecognizer?

didPan() is called multiple times with state .changed during a single pan from the user, but not at a regular frequency.
Instead, you should declare two global state variables that save the current horizontal and vertical directions (left to right, or right to left, for the first one, for instance) and update these variables each time didPan() is called.
You should run an SKAction instance only when a direction changes and those SKAction instances should loop indefinitely, not have a one-time only specific duration of 0.5 sec (see SKAction.repeatForever() in the SceneKit API manual).
Also, you need to be sure to remove the already running moving action before running the next movement action.
Finally, call removeAllActions() when didPan() is called with state .ended.
enum VerticalState{
case .none
case .topToBottom
case .bottomToTop
}
enum HorizontalState{
case .none
case .leftToRight
case .rightToLeft
}
var verticalState = VerticalState.none
var horizontalState = HorizontalState.none
#objc func didPan(gesture: UIPanGestureRecognizer) {
let pan = gesture.velocity(in: self.view)
if gesture.state == .changed {
for sprites in levelSprites {
if pan.x > 0 && pan.x > pan.y && horizontalState <> .leftToRight{
horizontalState = .none
verticalState = .none
} else if pan.x < 0 && pan.x < pan.y && horizontalState <> .rightToLeft{
horizontalState = .rightToLeft
verticalState = .none
} else if pan.y < 0 && pan.x > pan.y && verticalState <> .bottomToTop{
horizontalState = .none
verticalState = .bottomToTop
} else if pan.y > 0 && pan.x < pan.y && verticalState <> topToBottom{
horizontalState = .none
verticalState = .topToBottom
}
switch horizontalState
{
case .leftToRight:
sprites.removeActionForKey("movement")
self.player.removeActionForKey("movement")
sprites.run(SKAction.repeatForever(SKAction.moveBy(x: -40, y:0, duration: 0.5), forKey:"movement"))
self.player.run(SKAction(named: "WalkRight")!, forKey:"movement")
case .rightToLeft:
sprites.removeActionForKey("movement")
self.player.removeActionForKey("movement")
sprites.run(SKAction.repeatForever(SKAction.moveBy(x: 40, y:0,, duration: 0.5), forKey:"movement"))
self.player.run(SKAction(named: "WalkLeft")!, forKey:"movement")
case .none:
break
}
switch verticalState
{
case .topToBottom:
sprites.removeActionForKey("movement")
self.player.removeActionForKey("movement")
sprites.run(SKAction.repeatForever(SKAction.moveBy(x: 0, y:-30, duration: 0.5), forKey:"movement"))
self.player.run(SKAction(named: "WalkUp")!, forKey:"movement")
case .bottomToTop:
sprites.removeActionForKey("movement")
self.player.removeActionForKey("movement")
sprites.run(SKAction.repeatForever(SKAction.moveBy(x: 0, y:30,, duration: 0.5), forKey:"movement"))
self.player.run(SKAction(named: "WalkDown")!, forKey:"movement")
case .none:
break
}
}
}else if gesture.state == .ended {
sprites.removeActionForKey("movement")
self.player.removeActionForKey("movement")
}
}

Related

Dragging UIView

I need some help with dragging UIView to reveal, menu view underneath main view.
I have two UIViews.
menuView - includes menu buttons and labels
and mainView - located over menuView.
I want to drag the main view from left edge to show menu items and snap the main view to a specific position. I am able to get the dragging to the right working but I cannot set it back to original position when dragging left.
here is my codes I have been playing around with but no success.
I also wanted to make mainView smaller as I drag it to right.
any help will be greatly appreciated.
Note: PanGesture is attached to mainView.
#IBAction func dragMenu(_ sender: UIPanGestureRecognizer) {
let mview = sender.view!
let originalCenter = CGPoint(x: self.mainView.bounds.width/2, y: self.mainView.bounds.height/2)
switch sender.state {
case .changed:
if let mview = sender.view {
mview.center.x = mview.center.x + sender.translation(in: view).x
sender.setTranslation(CGPoint.zero, in: view)
}
case .ended:
let DraggedHalfWayRight = mview.center.x > view.center.x
if DraggedHalfWayRight {
//dragginToRight
showMenu = !showMenu
self.mainViewRight.constant = 200
self.mainTop.constant = 50
self.mainBottom.constant = 50
self.mainLeft.constant = 200
} else //dragging left and set it back to original position.
{
mview.center = originalCenter
showMenu = !showMenu
}
default:
break
}
}
I'd suggest a few things:
When you're done dragging the menu off, make sure to set its alpha to zero (so that, if you were in portrait and go to landscape, you don't suddenly see the menu sitting there).
I personally just adjust the transform of the dragged view and avoid resetting the translation of the gesture back to zero all the time. That's a matter of personal preference.
I'd make the view controller the delegate for the gesture and implement gestureRecognizerShouldBegin (because if the menu is hidden, you don't want to recognize swipes to try to hide it again; and if it's not hidden, you don't want to recognize swipes to show it).
When figuring out whether to complete the gesture or not, I also consider the velocity of the gesture (e.g. so a little flick will dismiss/show the animated view).
Thus:
class ViewController: UIViewController {
#IBOutlet weak var menuView: UIView!
var isMenuVisible = true
#IBAction func dragMenu(_ gesture: UIPanGestureRecognizer) {
let translationX = gesture.translation(in: gesture.view!).x
switch gesture.state {
case .began:
// if the menu is not visible, make sure it's off screen and then make it visible
if !isMenuVisible {
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
menuView.alpha = 1
}
fallthrough
case .changed:
if isMenuVisible {
menuView.transform = CGAffineTransform(translationX: translationX, y: 0)
} else {
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width + translationX, y: 0)
}
case .ended:
let shouldComplete: Bool
if isMenuVisible {
shouldComplete = translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x > 0
} else {
shouldComplete = -translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x < 0
}
UIView.animate(withDuration: 0.25, animations: {
if self.isMenuVisible && shouldComplete || !self.isMenuVisible && !shouldComplete {
self.menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
} else {
self.menuView.transform = .identity
}
if shouldComplete{
self.isMenuVisible = !self.isMenuVisible
}
}, completion: { _ in
self.menuView.alpha = self.isMenuVisible ? 1 : 0
})
default:
break
}
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
guard let gesture = gesture as? UIPanGestureRecognizer else { return true }
let translationX = gesture.translation(in: gesture.view!).x
return isMenuVisible && translationX > 0 || !isMenuVisible && translationX < 0
}
}
Personally, I prefer to use a separate screen edge gesture recognizer to pull them menu back on screen (you don't really want it to recognize gestures anywhere, but just on that right edge). Another virtue of this approach is that by keeping "show" and "hide" in different functions, the code is a lot more readable (IMHO):
class ViewController: UIViewController {
#IBOutlet weak var menuView: UIView!
var isMenuVisible = true
#IBAction func handleScreenEdgeGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
let translationX = gesture.translation(in: gesture.view!).x
switch gesture.state {
case .began:
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
menuView.alpha = 1
fallthrough
case .changed:
menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width + translationX, y: 0)
case .ended:
let shouldComplete = -translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x < 0
UIView.animate(withDuration: 0.25, delay:0, options: .curveEaseOut, animations: {
if shouldComplete {
self.menuView.transform = .identity
self.isMenuVisible = !self.isMenuVisible
} else {
self.menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
}
}, completion: { _ in
self.menuView.alpha = self.isMenuVisible ? 1 : 0
})
default:
break
}
}
#IBAction func dragMenu(_ gesture: UIPanGestureRecognizer) {
let translationX = gesture.translation(in: gesture.view!).x
switch gesture.state {
case .began, .changed:
menuView.transform = CGAffineTransform(translationX: translationX, y: 0)
case .ended:
let shouldComplete = translationX > gesture.view!.bounds.width / 2 || gesture.velocity(in: gesture.view!).x > 0
UIView.animate(withDuration: 0.25, delay:0, options: .curveEaseOut, animations: {
if shouldComplete {
self.menuView.transform = CGAffineTransform(translationX: gesture.view!.bounds.width, y: 0)
self.isMenuVisible = !self.isMenuVisible
} else {
self.menuView.transform = .identity
}
}, completion: { _ in
self.menuView.alpha = self.isMenuVisible ? 1 : 0
})
default:
break
}
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIScreenEdgePanGestureRecognizer {
return !isMenuVisible
} else if let gesture = gestureRecognizer as? UIPanGestureRecognizer {
let translationX = gesture.translation(in: gesture.view!).x
return isMenuVisible && translationX > 0
}
return true
}
}

require(toFail) not working for PanGestureRecognizer and PageViewController

Here's the problem I'm having:
I have a PageViewController that navigates between 4 views horizontally and a PanGestureRecognizer on a UIView that brings it up and down vertically. The issue is that the UIView can be panned up, but then it is stuck there. The user can only swipe horizontally with the PageViewController.
The goal:
Be able to pan the UIView up and down AND swipe left and right on the PageViewController. Basically, make the UIView not get stuck on screen.
This is what I have now:
I have a PanGestureRecognizer function in the UIView file that looks
like this:
func handlePan(recognizer: UIPanGestureRecognizer) {
let vel = recognizer.velocity(in: self)
print("HANDLING PAN")
switch recognizer.state {
case .began:
if vel.y < 0 && !scanIsUp {
animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut, animations: {
self.frame = self.frame.offsetBy(dx: 0, dy: -603)
})
scanIsUp = true
isScanVisible = true
} else if vel.y > 0 && scanIsUp {
print("vel.y hehehe")
animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut, animations: {
self.frame = self.frame.offsetBy(dx: 0, dy: 603)
})
scanIsUp = false
isScanVisible = false
}
animator?.pauseAnimation()
print("paused?")
case .changed:
let translation = recognizer.translation(in: backgroundImage)
if vel.y < 0 && scanIsUp {
animator?.fractionComplete = translation.y / -603
} else if vel.y > 0 && !scanIsUp {
print("vel.y hehehe pt 2")
animator?.fractionComplete = translation.y / 603
}
case .ended:
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
case .possible: break
default: break
}
}
I also have an array of gestureRecognizers that control the PageViewController. I cycle through them and require them to fail so that the user can pan the UIView up and down like this:
if #available(iOS 10.0, *) {
self.panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(PFScan.handlePan))
self.panGestureRecognizer!.maximumNumberOfTouches = 1
self.panGestureRecognizer!.cancelsTouchesInView = false
self.addGestureRecognizer(self.panGestureRecognizer!)
for recognizer in self.scrollViewGestureRecognizers {
recognizer.require(toFail: panGestureRecognizer!)
}
}
Finally, I initialize the UIView on the main view controller part of the PageViewController like this:
func setUpScanPage(scrollViewGestureRecognizers: [UIGestureRecognizer]) {
scanPage = PFScan(scrollViewGestureRecognizers: scrollViewGestureRecognizers)
self.view.addSubview(scanPage)
}
Does anyone know why the UIView gets stuck?? I have tried everything I know of. Any help would be immensely appreciated.
Thanks a ton in advanced. Cheers,
Theo

Cutting diagonal movement from pan gesture

I'm trying to lock my image I'm dragging (objectDragging) to the x or y center, depending which way I'm moving or influencing it to move. If I'm influencing it to the right, I want it to lock on its initial Y position, and only allow it to go right (or back left, on a straight line). If I'm influencing it upward, lock its x position and allow it to move straight up or down on the exact line. I'm using a switch for my movement handling, with lots of code in the .end case, so I didn't want to include a lot of code. Just hoping for an idea — wasn't able to find a lot.
switch(recognizer.state) {
objectDragging = (recognizer.view?.tag)!
case .began:
initPosx = Int(recognizer.view!.center.x)
initPosy = Int(recognizer.view!.center.y)
case .changed:
...
After the suggestion of using velocity from Paulw11 I've had success limiting the direction of the image I'm moving by deciding which way we are initially moving the image (in .began) and then locking the x or y (in .changed). Incase anyone comes across this with a similar issue, heres the gist of my solution that is working well for my application:
switch(recognizer.state) {
case .began:
let velocity = recognizer.velocity(in: self.view)
if (velocity.x > velocity.y) && (velocity.x > 0) {
print("moving right")
myBlocks[objectDragging].center.y = CGFloat(initPosy)
movingRight = true
}
else if ((abs(velocity.x)) > velocity.y) && (velocity.x < 0) {
print("moving left")
myBlocks[objectDragging].center.y = CGFloat(initPosy)
movingLeft = true
}
else if (velocity.y > velocity.x) && (velocity.y > 0) {
print("moving down")
myBlocks[objectDragging].center.x = CGFloat(initPosx)
movingDown = true
}
else if ((abs(velocity.y)) > velocity.x) && (velocity.y < 0){
print("moving up")
myBlocks[objectDragging].center.x = CGFloat(initPosx)
print(abs(velocity.y))
movingUp = true
}
case .changed:
if (movingRight == true) {
myBlocks[objectDragging].center.y = CGFloat(initPosy)
}
else if (movingLeft == true) {
myBlocks[objectDragging].center.y = CGFloat(initPosy)
}
else if (movingDown == true) {
myBlocks[objectDragging].center.x = CGFloat(initPosx)
}
else if (movingUp == true) {
myBlocks[objectDragging].center.x = CGFloat(initPosx)
}
case .ended:
movingUp = false
movingLeft = false
movingRight = false
movingDown = false

How to Properly Space SKSpriteNodes?

I'm working on a game where nodes are continuously scrolling down the screen. I have them set up to spawn at a certain position and everything looks even:
The issue occurs when the user pauses the game repeatedly, the nodes get misaligned like so:
I believe that when I pause the game my timing is thrown off. Here are my updateCurrentTime() functions. I have it set so it adds a new random row every 0.6 seconds.
func updateWithTimeSinceLastUpdate (_ timeSinceLastUpdate:CFTimeInterval) {
lastYieldTimeInterval += timeSinceLastUpdate
if lastYieldTimeInterval > 0.6 {
lastYieldTimeInterval = 0
addRandomRow()
}
}
override func update(_ currentTime: TimeInterval) {
if screenIsPaused == 1{
pauseScreen()
}else if screenIsPaused == 0{
unPauseScreen()
screenIsPaused = 2
} else if screenIsPaused == 3{
gameLayer.isPaused = true
}
var timeSinceLastUpdate = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime
if timeSinceLastUpdate > 1 {
timeSinceLastUpdate = 1/60
lastUpdateTimeInterval = currentTime
}
updateWithTimeSinceLastUpdate(timeSinceLastUpdate)
}
Here is the code that I use to pause my game:
func pauseScreen(){
overlay.color = UIColor.black
overlay.alpha = 0.6
overlay.size.height = self.size.height
overlay.size.width = self.size.width
overlay.position = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
overlay.zPosition = 100
addChild(overlay)
gameLayer.isPaused = true
screenIsPaused = 3
label.text = "Press screen to play!"
label.fontColor = SKColor.white
label.fontSize = 50
label.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
addChild(label)
let Fade = SKAction.sequence([SKAction.fadeIn(withDuration: 1), SKAction.fadeOut(withDuration: 1)])
label.run(SKAction.repeatForever(Fade))
}
func unPauseScreen(){
label.removeFromParent()
overlay.removeFromParent()
screenIsPaused = 2
gameLayer.isPaused = false
}
The pause function is called in touchesEnded() and the un-pause function is called in touchesBegan.
Please let me know if there is anything I can try or if you require more information. Thanks!
I finally found a solution. I'm not sure if it is the best answer; however, it works. I modified my function updateWithTimeSinceLastUpdate here it is now:
func updateWithTimeSinceLastUpdate (_ timeSinceLastUpdate:CFTimeInterval) {
if lastYieldTimeInterval > 0.6 {
lastYieldTimeInterval = 0
addRandomRow()
}
if screenIsPaused == 1{
pauseScreen()
lastYieldTimeInterval += timeSinceLastUpdate
}else if screenIsPaused == 0{
unPauseScreen()
lastYieldTimeInterval += timeSinceLastUpdate
screenIsPaused = 2
} else if screenIsPaused == 3{
gameLayer.isPaused = true
}else if screenIsPaused == 2{
lastYieldTimeInterval += timeSinceLastUpdate
}
}
I moved that screenIsPaused if statements here instead of my update(_ currentTime: TimeInterval). In addition, I added lastYieldTimeInterval += timeSinceLastUpdate into each of the if statements. Now every time the game pauses or un-pauses the time is updated.

Detect both falling and jumping in sprite kit game

I am working on a sprite kit/swift game. I want the ball to do different things when it falls and jumps. Now if it falls it also uses the code at the bottom (the code for jumping)
Here is the code I am using:
override func collisionWithBall(ball: SKNode) -> Bool {
if ball.physicsBody?.velocity.dy < 0 {
ball.physicsBody?.velocity = CGVector(dx: ball.physicsBody!.velocity.dx, dy: 500.0)
if platformType == .Normal {
self.removeFromParent()
}
if platformType == .Break {
self.removeFromParent()
ball.physicsBody?.velocity = CGVector(dx: ball.physicsBody!.velocity.dx, dy: 800.0)
}
}
if ball.physicsBody?.velocity.dy > 0 {
if platformType == .Break {
NSUserDefaults.standardUserDefaults().setInteger(1, forKey: "nextValue")
self.removeFromParent()
ball.physicsBody?.velocity = CGVector(dx: ball.physicsBody!.velocity.dx, dy: -800.0)
}
}
return false
}
If it jumps I want it to only use this if ball.physicsBody?.velocity.dy > 0 And when it falls I want to only use this if ball.physicsBody?.velocity.dy < 0.
What can I do to make it working?
You'll have do change the if statements to use an else if for the second condition. Like this:
if ball.physicsBody?.velocity.dy < 0 {
// Handle falling here...
} else if if ball.physicsBody?.velocity.dy > 0 {
// Handle jumping here...
}

Resources