I am currently working with SpriteKit and I want to run a code block in the update-loop as long as the user is touching a certain SpriteNode. I tried to achieve this by using a Boolean, that gets set to true, when the touchesBegan() method recognises a touch on this node and gets set to false, when the touchesEnded() method recognises a touch ending on this node. However, when the user touches the node and then moves his finger outside of the boundaries, the touchesEnded() method does not recognise this.
Is there a simple way to check if a touch, that began in this node, but then moved outside of it, still exists? Or can I check in general if a UITouch instance still exists?
It's not clear what behavior you want, but in general you probably want to use touch identity to track what's happening.
For example, if you're handling touches in the scene containing the node, and if the desire is simply to have the action start when the node is touched and stop when that touch ends, then something like:
// Whatever touch initiated the action
var activeTouch: UITouch?
// Checked by update loop
var doingSomething = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// Ignore new touches if one is already active
guard activeTouch == nil else { return }
let location = touch.location(in: self)
let touchedNodes = self.nodes(at: location)
for node in touchedNodes {
if <some test for the interesting node> {
activeTouch = touch
doingSomething = true
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if touch == activeTouch {
// Finished
activeTouch = nil
doingSomething = false
}
}
}
If you also want the action to stop if the user moves their finger off the node and restart if they move back on, then also override touchesMoved, e.g.:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
if touch == activeTouch {
let location = touch.location(in: self)
let touchedNodes = self.nodes(at: location)
// Assume they moved off the node
doingSomething = false
for node in touchedNodes {
if <some test for the interesting node> {
// Nope, still touching it
doingSomething = true
}
}
}
}
}
(You should also handle touchesCancelled in some appropriate way, maybe stopping the action and clearing activeTouch as in touchesEnded)
Perhaps you have some other behavior in mind for the case when there are multiple touches active. You might need to keep track of all the active touches and their status of being on or off the node, and then set doingSomething = true if any active touch is on the node. Or maybe you want touchesMoved to discard an active touch as soon as it moves off the node, so moving that touch back on again won't reactivate.
The main point is that keeping track of touch identity gives you a lot of flexibility, but you have to decide on how you want the game to react.
Related
I am creating a game where the user can move some fruits around in the scene. I want to user to be able to move only the fruits and not any other SKSpriteNode in the scene, so I wrote the code below to implement it. However the code doesn't work properly as I can't seem to be able to drag any of my sprites around, but rather they change position only when I stop touching the screen and they don't move by much anyway.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in: self){
let nodesTouched = nodes(at: location)
for node in (nodesTouched) {
if node is Fruit{
for t in touches {
let locationMoved = t.location(in: self)
node.position.x = locationMoved.x
node.position.y = locationMoved.y
}
}
}
}
}
Anyone knows what's wrong with it?
Thanks in advance!
I found a solution to this, which was to basically set the physicsBody.affectedByGravity property to false for that specific Fruit instance every time I touch it, and then set it back to true as soon as I stop touching it. that makes it possible to drag all the fruits everywhere I want.
I have a few button with amorphous look. (the Rect of the buttons are intersecting) First I evaluated the BeganTouch in the GameScene. Then I get several touches.
Since I have in my buttons still child nodes, they have swallowed the touchs. Ok, I have made with the help of here a subclass of the SpriteNodes and processed the touch inside the subclass. Now I have the problem that the first button does not pass the touch to the underlying sprites.
But now I would like top ignore the transparent areas of the buttons, how can I do that?
I have read that one can work with physicsbody, I have tried, gravitiy = 0 but since I move and scale the buttons via actions there were violent effects.
Why can I check for the alpha in the touch location and pass the touch to the next sprite.
by the way: how can I get a reference to the view? to get the global loc.
let loc = touch.location(in: view)
with the global touch location I could check all sprites under this point for the alpha!
you can try passing the touch to it's parent (presumably the scene) from your subclass.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch! {
//handle whatever code you want your subclass to do
...
//pass the touch event to the parent
super.touchesBegan(touches, with: event)
}
}
and then in your scene, cycle through the your buttons (in this example I have the buttons in a button array)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch! {
let touchLocation = touch.location(in: self)
for button in buttons {
if button.contains(touchLocation) {
//handle all other buttons
...
}
}
}
}
although this seems a little redundant to do the touches in two different locations. #Knight0fDragon is correct, it seems odd to have a button have transparent areas.
ok, it's simple, all buttons are from the same subclass. this subclass delegates to an method in the GameScene. here I can check with
allNodes = nodes(at: globalLocation)
now I can check for the name of the node, calculate the point of the pixel inside each node and get the alpha value.
thanxs all
I have looked around at many articles, but none of them seem to make sense or are relevant to my issue. I want to give the user a set time to press a node. If they succeed in pressing the node within the set time the timer should reset, if they fail to press the node within the set time it will be game over.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
if square.contains(touch.location(in: self)) {
moveSquare()
GameScene.score+=1
scoreLabel.text = "\(GameScene.score)"
}
} else {
gameOverScene()
}
}
so my question is how do I set the timer up to achieve those requirements, and where do I put the code?
Use SKActions instead of timers:
let countdown = SKAction.sequence([SKAction.wait(forDuration: 5),
SKAction.perform(#selector(gameOver), onTarget: Self)])
run(countdown, withKey: "gameOverTimer")
(this assumes that the function gameOver is the one you want to call if the correct button is not touched in time)
and in touchesBegan, if the correct node is touched:
removeAction(forKey: "gameOverTimer")
I have several buttons that move a main character. (Left, right, jump, etc.) But, when I touch more than one button at a time, the previous one is ended. My question is, how do I keep both touches alive for the duration of their touches? As an example, this would allow the character to move forward and jump at the same time. I have set multipleTouchEnabled to true. I've read that using a dictionary to track touches would help, but I can't seem to wrap my head around the implementation.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInView(nil)
if location.x < self.size.width / 2 && location.y > self.size.height / 2 {
movingLeft = true
}
if location.x > self.size.width / 2 && location.y > self.size.height / 2 {
movingRight = true
}
if location.x < self.size.width / 2 && location.y < self.size.height / 2 {
jump()
}
if location.x > self.size.width / 2 && location.y < self.size.height / 2 {
jump()
}
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
movingLeft = false
movingRight = false
}
func jump() {
mainCharacter.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
mainCharacter.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 400))
}
The logic
You should create a dictionary of active touches.
private var activeTouches = [UITouch:String]()
Every time a touch begins you save it into the dictionary and assign to it a label.
activeTouches[touch] = "left"
So when the touch does end you can search for it into your dictionary and find the related label. Now you know which button has been released by the user.
let button = activeTouches[touch]
if button == "left" { ... }
And don't forget to remove it from the dictionary.
activeTouches[touch] = nil
The implementation
class GameScene: SKScene {
private var activeTouches = [UITouch:String]()
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let button = findButtonName(from:touch)
activeTouches[touch] = button
tapBegin(on: button)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
guard let button = activeTouches[touch] else { fatalError("Touch just ended but not found into activeTouches")}
activeTouches[touch] = nil
tapEnd(on: button)
}
}
private func tapBegin(on button: String) {
print("Begin press \(button)")
// your custom logic goes here
}
private func tapEnd(on button:String) {
print("End press \(button)")
// your custom logic goes here
}
private func findButtonName(from touch: UITouch) -> String {
// replace this with your custom logic to detect a button location
let location = touch.locationInView(self.view)
if location.x > self.view?.frame.midX {
return "right"
} else {
return "left"
}
}
}
In the code above you should put your own code into
tapBegin: this method receive the label of a button and start some action.
E.g. start running.
tapEnd: this method receive the label of a button and stop some action.
E.g. stop running.
findButtonName: this method receives a UITouch and returns the label of the button pressed by the user.
Test
I tested the previous code on my iPhone. I performed the following actions.
started pressing the right of the screen
started pressing the left of the screen
removed finger from the right of the screen
removed finger from the left of the screen
As you can see in the following log the code is capable of recognizing different touches
Begin press right
Begin press left
End press right
End press left
Conclusion
I hope I made myself clear. Let me know if something is not.
Issue
For me it was a bit foolish but there's an option that I miss to enable in my storyboard in order to handle multiple touch in my GameScene of type SKScene.
When one of the overwritten functions is being called, you should have more than 1 touch contained in touches.
Those functions are indeed:
touchesBegan
touchesMoved
touchesEnded
touchesCancelled
... and the way to retrieve the touches is indeed to browse through them. However, if you are only able to retrieve one touch only, follow the steps bellow.
Debugging
Here's what you can do to see if you have multiple touches in your set of touches:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for (i, t) in touches.enumerated() {
print(i, t.location(in: self))
self.touchMoved(toPoint: t.location(in: self))
}
}
Output:
There you should find a 0 for the first touch and a 1 when you touch with 2 fingers... and so on.
Solution
I case you encounter the same problem than me and have only the touch t at index 0 but no other touch, you should see upper in the hierarchy of views if all of them are enabled for multi-touch. In my case it was the view containing the GameScene of type SKScene that did not have the option Multiple Touch enabled. See below:
I'm building a list with a checkbox function, where the check symbol is a child of the empty check box. In touchesBegan:, the checkbox should go from hidden to unhidden, and back, with each touch of the checkbox. The code looks like this:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
location = touch.locationInNode(self)
for node in self.nodesAtPoint(location) {
if node.childNodeWithName("checkSymbol")?.hidden == true {
node.childNodeWithName("checkSymbol")?.hidden = false
} else if childNodeWithName("checkSymbol")?.hidden == false {
node.childNodeWithName("checkSymbol")?.hidden = true
}
}
}
}
The problem is the childNode (the checkSymbol node) is not being picked up by the .nodesAtPoint() method when it's hidden. That's the reason I've tried to workaround using the .childNodeWithName() approach above, but the box remains checked for subsequent touches. Has anyone a suggestion for how to fix this?
If you wish for hidden nodes to be picked up by the nodesAtPoint you can always change your implementation of hidden.
You could alternatively use node.zPosition = -1, presuming you have a background node with zPosition 0 to hide behind.