I'm looking to have a scrollable background in Sprite Kit. I've had a go with some of the other solutions available online, but they were implementing infinite scrolling backgrounds, and I haven't been able to adapt the code to my needs.
Here is some sample code which I've got to try and get the background moving (without the detection of reaching the end of the background) - but it's very choppy and not smooth at all.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touchLocation = touches.first?.location(in: self), let node = nodes(at: touchLocation).first {
if node.name != nil {
if node.name == "background" {
background.position = touchLocation
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touchLocation = touches.first?.location(in: self), let node = nodes(at: touchLocation).first {
if node.name != nil {
if node.name == "background" {
background.position = touchLocation
}
}
}
}
The image below demonstrates what I'm trying to achieve - I want the code to detect when you've reached the end of the background, and to prevent you from moving it any further.
So, taking #KnightOfDragon's comment into account about needing to set maximum and minimum X coordinate values for the background, I was able to solve my own question. I already had swipe left/right recognisers in my code (for another purpose in my game), and I was able to reuse these to fulfil my needs. Code is as follows:
In didMove():
swipeRightRec.addTarget(self, action: #selector(self.swipedRight) )
swipeRightRec.direction = .right
self.view!.addGestureRecognizer(swipeRightRec)
swipeLeftRec.addTarget(self, action: #selector(self.swipedLeft) )
swipeLeftRec.direction = .left
self.view!.addGestureRecognizer(swipeLeftRec)
And then these functions:
#objc func swipedRight() {
if background.position.x + 250 > maxBackgroundX {
let moveAction = SKAction.moveTo(x: maxBackgroundX, duration: 0.3)
background.run(moveAction)
} else {
let moveAction = SKAction.moveTo(x: background.position.x + 250, duration: 0.3)
background.run(moveAction)
}
}
#objc func swipedLeft() {
if background.position.x - 250 < minBackgroundX {
let moveAction = SKAction.moveTo(x: minBackgroundX, duration: 0.3)
background.run(moveAction)
} else {
let moveAction = SKAction.moveTo(x: background.position.x - 250, duration: 0.3)
background.run(moveAction)
}
}
Yes this means that the background moves a set amount each time you swipe, no matter how big the swipe is, but it is exactly what I required for my game. I hope this helps someone else who needs the same thing!
Related
I`m here because after weeks of trying different solutions and don't come with the right answer and functional in-app I am exhausted.
I need to track the time of finger on-screen and if a finger is on screen longer than 1 sec I need to call function. But also if now user is performing gestures like a pan or pinch function must be not called.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let touch = touches.first
guard let touchLocation = touch?.location(in: self) else {return }
let tile: Tile?
switch atPoint(touchLocation){
case let targetNode as Tile:
tile = targetNode
case let targetNode as SKLabelNode:
tile = targetNode.parent as? Tile
case let targetNode as SKSpriteNode:
tile = targetNode.parent as? Tile
default:
return
}
guard let tile = tile else {return }
paint(tile: tile)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
}
private func paint(tile: Tile){
let col = tile.column
let row = tile.row
let colorPixel = gridAT(column: col, row: row)
if !tile.isTouchable {
}else {
if !selectedColor.isEmpty {
if tile.mainColor == selectedColor {
tile.background.alpha = 1
tile.background.color = UIColor(hexString:selectedColor)
tile.text.text = ""
tile.backgroundStroke.color = UIColor(hexString:selectedColor)
uniqueColorsCount[selectedColor]?.currentNumber += 1
didPaintCorrect(uniqueColorsCount[selectedColor]?.progress ?? 0)
colorPixel?.currentState = .filledCorrectly
tile.isTouchable = false
}
else if tile.mainColor != selectedColor {
tile.background.color = UIColor(hexString:selectedColor)
tile.background.alpha = 0.5
colorPixel?.currentState = .filledIncorrectly
}
}
}
}
Here is a small example I created for you with my suggestion of using UILongPressGestureRecognizer as it seems easier to manage for your situation than processing touchesBegin and touchesEnded
You can give it the minimum time the user needs to tap so it seems perfect for your requirement.
You can read more about it here
First I just set up a basic UIView inside my UIViewController with this code and add a long tap gesture recognizer to it:
override func viewDidLoad() {
super.viewDidLoad()
// Create a basic UIView
longTapView = UIView(frame: CGRect(x: 15, y: 30, width: 300, height: 300))
longTapView.backgroundColor = .blue
view.addSubview(longTapView)
// Initialize UILongPressGestureRecognizer
let longTapGestureRecognizer = UILongPressGestureRecognizer(target: self,
action: #selector(self.handleLongTap(_:)))
// Configure gesture recognizer to trigger action after 2 seconds
longTapGestureRecognizer.minimumPressDuration = 2
// Add gesture recognizer to the view created above
view.addGestureRecognizer(longTapGestureRecognizer)
}
This gives me something like this:
Next, to get the location of the tap, the main question to ask yourself is - Where did the user tap in relation to what view ?
For example, let's say the user taps here:
Now we can ask what is location of the tap in relation to
The blue UIView - It is approx x = 0, y = 0
The ViewController - It is approx x = 15, y = 30
The UIView - It is approx x = 15, y = 120
So based on your application, you need to decide, in relation to which view do you want the touch.
So here is how you can get the touch based on the view:
#objc
private func handleLongTap(_ sender: UITapGestureRecognizer)
{
let tapLocationInLongTapView = sender.location(in: longTapView)
let tapLocationInViewController = sender.location(in: view)
let tapLocationInWindow = sender.location(in: view.window)
print("Tap point in blue view: \(tapLocationInLongTapView)")
print("Tap point in view controller: \(tapLocationInViewController)")
print("Tap point in window: \(tapLocationInWindow)")
// do your work and function here
}
For same touch as above image, I get the following output printed out:
Tap point in blue view: (6.5, 4.5)
Tap point in view controller: (21.5, 34.5)
Tap point in window: (21.5, 98.5)
I have a sprite node in game that when touched makes an action of the node being pressed and when the touches began event is called and then goes back to the normal size when the touches ended event is called. my problem is when i press down on the node and then move my finger outside of the node it doesn't go back to its original size after I take my finger off the screen.
I tried using multiple things in the touches moved section of the code to try and get it to go back to its original size after i've moved my finger outside of the node while holding the touch down but it didn't work. My code is below
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let node = self.atPoint(touch.location(in: self))
let pushdown = SKAction.scale(to: 0.8, duration: 0.1)
if node == mainMenu.settingsButton {
node.run(pushdown)
} else if node == mainMenu.viewMapButton {
node.run(pushdown)
}else if node == mainMenu.shopButton {
node.run(pushdown)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
var location: CGPoint = touch.location(in: self)
let node: SKNode = atPoint(location)
let pushUp = SKAction.scale(to: 1.0, duration: 0.2)
if node != mainMenu.settingsButton {
//node.run(pushUp)
} else if touch.phase == .moved || touch.phase == .cancelled {
node.run(pushUp)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let node = self.atPoint(touch.location(in: self))
let pushUp = SKAction.scale(to: 1.0, duration: 0.2)
if node == mainMenu.settingsButton {
node.run(pushUp)
//Run Sound Here
let scene = SettingsMenu(fileNamed:"SettingsMenu")
scene?.scaleMode = .aspectFill
//scene?.backgroundColor = UIColor.lightGray
let transition = SKTransition.crossFade(withDuration: 0.5)
self.scene?.view?.presentScene(scene!, transition: transition)
} else if node == mainMenu.viewMapButton {
node.run(pushUp)
}
}
How can i get it to go back to the original size after i've moved my finger outside of the nodes location while holding the touch down?
In touchesBegan, touchesMoved, touchesEnded you are referring always the current node at point. let node = self.atPoint(touch.location(in: self))
Why not to keep a reference to the initial node which was detected in touchesBegan to scale it back if the current node atPoint is not equal to the initial one. I assume this would solve your issue.
EDIT:
To capture the node on first touch ...
private var currentTouchedNode : SKNode?
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
let touch: UITouch = touches.first!
currentTouchedNode = self.atPoint(touch.location(in: self))
let pushdown = SKAction.scale(to: 0.8, duration: 0.1)
if currentTouchedNode == mainMenu.settingsButton {
currentTouchedNode.run(pushdown)
} else if currentTouchedNode == mainMenu.viewMapButton {
currentTouchedNode.run(pushdown)
}else if currentTouchedNode == mainMenu.shopButton {
currentTouchedNode.run(pushdown)
}
}
In touchesMoved, touchesEnded you would compare on if the current node is equal currentTouchedNode and resize it if needed
I am making a game with Sprite Kit where the user has tap balls that pass through the screen. The balls are spawned every 1 second. However, if two balls have spawned and the user taps the first ball only the second (and any that have spawned after that) will be removed/recorded and not the one the user actually tapped.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = self.nodes(at: location).first
if node?.name == "BALL" {
currentScore += ballValue
player?.removeFromParent()
}
else {
gameOver()
}
}
}
override func didMove(to view: SKView) {
setupTracks()
createHUD()
self.run(SKAction.repeatForever(SKAction.sequence([SKAction.run {
self.createBall(forTrack: self.track)
}, SKAction.wait(forDuration: 2)])))
}
func createBall(forTrack track: Int) {
setLevel()
player?.name = "BALL"
player?.size = CGSize(width: 100, height: 100)
ballValue = 1
let ballPosition = trackArray?[track].position
player?.position = CGPoint(x: (ballPosition?.x)!, y: (ballPosition?.y)!)
player?.position.y = (ballPosition?.y)!
player?.zPosition = 1
if ballDirection == "right" {
player?.position.x = 0
moveRight()
}
else {
player?.position.x = (self.view?.frame.size.height)!
moveLeft()
}
}
I’m pretty sure it’s because you do:
player?.removeFromParent()
no matter which sprite is touched, but player is always the last sprite spawned. You’ve already assigned the node that was touched to node, so I think you need to do:
node.removeFromParent()
instead.
I have a UILabel object which is being animated, moving up and down. I am coding in Xcode 8.3 with Swift 3. A user can pan this object and drag around to one location to get some points. I am handling the pan/drag using touchesXXX gesture. When I tap and let it go immediately, however, this object jumps vertically above its location for some reason and I am unable to figure this out why for a week now...
When I enabled some debugging, I can only see touchesBegan was invoked (touchesMoved was not called as expected, neither touchesEnded which appears unexpected to me). If I disable animation on the object manually, it works as expected and the object is able to be panned effortlessly and there is obviously no object jumps.
Here is an extract of the relevant code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
//pauseLayer(self.optOneLbl.layer) <<<<< comment #1
//optOneLbl.translatesAutoresizingMaskIntoConstraints = true <<<<< comment #2
optOneLbl.center = touchLocation
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
var sender: UILabel
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
self.optOneLbl.center = touchLocation
sender = self.optOneLbl
}
// identify which letter was overlapped
var overlappedView : UILabel
if (sender.frame.contains(letter1.center)) {
...
}
else {
return
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
}
After reading responses to other SO questions, I thought disabling the label animation programmatically when touched as per above comment #1 in touchesBegan might help, but the issue persisted. Also, I thought may be Auto Layout is causing this weird jump. So, I enabled translatesAutoresizingMaskIntoConstraints as per comment #2, but it too didn't help.
Anyone is able to see where I am handling this incorrectly?
Thank you for reading!
EDIT:
As per #agibson007 request, I am adding the animation code extract for reference:
UIView.animate(withDuration: 12.0, delay: 0.0, options: [ .allowUserInteraction, .curveLinear, .autoreverse, .repeat ], animations: {
self.optOneLbl.center = CGPoint(x: self.optOneLbl.center.x, y: screenSize.midY*1.1)
})
You need to remove/reset the animation after you change the location of the label. The animation does not know you updated the values and is trying to stay in the same range of byValue from the beginning. Here is a working example of updating the animation.
import UIKit
class ViewController: UIViewController {
var optOneLbl = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
optOneLbl = UILabel(frame: CGRect(x: 20, y: 50, width: self.view.bounds.width - 40, height: 40))
optOneLbl.textAlignment = .center
optOneLbl.text = "I Will Be Moving Up and Down"
optOneLbl.textColor = .white
optOneLbl.backgroundColor = .blue
self.view.addSubview(optOneLbl)
//starts it in the beginnning with a y
fireAnimation(toY: self.view.bounds.midY*1.1)
}
func fireAnimation(toY:CGFloat) {
UIView.animate(withDuration: 12.0, delay: 0.0, options: [ .allowUserInteraction, .curveLinear, .autoreverse, .repeat ], animations: {
self.optOneLbl.center = CGPoint(x: self.optOneLbl.center.x, y:toY)
})
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
//re
optOneLbl.layer.removeAllAnimations()
optOneLbl.center = touchLocation
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self.view)
var sender: UILabel
if self.optOneLbl.layer.presentation()!.hitTest(touchLocation) != nil {
self.optOneLbl.center = touchLocation
sender = self.optOneLbl
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
//restart animation after finished and change the Y if you want.
// you could change duration or whatever
fireAnimation(toY: self.view.bounds.height - optOneLbl.bounds.height)
}
}
You can achieve the same using UIPanGestureRecognizer. Add pan gesture to your label and move the label on pan as
#IBAction func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.view)
yourLabel!.center = CGPoint(x: yourLabel!.center.x + translation.x, y: yourLabel!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
}
}
You can add gesturerecognizer to your UILabel from storyboard or programmatically in viewDidLoad as
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
yourLabel.addGestureRecognizer(gestureRecognizer)
Using Pan gesture as advantage that you dont have to follow up touchesXX method. To why your view is moving up, I think you might be resetting your label frame some where else too. There are also chances for gesture conflicting with touches. As both the cases are not evident in your code provided we need more code to confirm the same.
I am making a game where as the main sprite/player moves constantly, he/she needs to jump through barriers.
I need help with how to set a constant velocity for my moving sprite. When I try and do this in the SpriteKit update function, I can’t apply an impulse to jump whenever the user taps the screen.
Here is my code. I commented the places where I am having trouble:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
if (gameStarted == false) {
gameStarted = true
mainSprite.physicsBody?.affectedByGravity = true
mainSprite.physicsBody?.allowsRotation = true
let spawn = SKAction.runBlock({
() in
self.createWalls()
})
let delay = SKAction.waitForDuration(1.5)
let spawnDelay = SKAction.sequence([spawn, delay])
let spawnDelayForever = SKAction.repeatActionForever(spawnDelay)
self.runAction(spawnDelayForever)
let distance = CGFloat(self.frame.height + wallPair.frame.height)
let movePipes = SKAction.moveByX(0, y: -distance - 50, duration: NSTimeInterval(0.009 * distance)) // Speed up pipes
let removePipes = SKAction.removeFromParent()
moveAndRemove = SKAction.sequence([movePipes, removePipes])
} else {
if died == true {
}
else {
mainSprite.physicsBody?.applyImpulse(CGVectorMake(0, 20)) // TRYING TO APPLY AN IMPULSE TO MY SPRITE SO IT CAN JUMP AS IT MOVES
}
}
for touch in touches {
let location = touch.locationInNode(self)
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
updateSpritePosition()
mainSprite.physicsBody?.velocity = CGVectorMake(400, 0) // SETS A CONSTANT VELOCITY, HOWEVER I CAN NOT APPLY AN IMPULSE.
}
The problem is that you're overwriting the velocity in the update method. So even though you added an impulse, it gets immediately overwritten by code in the update. Try overwriting just the dx part of the velocity.
override func update(currentTime: CFTimeInterval) {
updateSpritePosition()
mainSprite.physicsBody?.velocity = CGVectorMake(400, mainSprite.physicsBody?.velocity.dy)
}