In a game I'm building a user is able to move the camera around by dragging around the screen similar to a scrollview. This is all handled in the main scene file by the touchesBegan and touchesMoved method. However I also want the ability for a user to drag from an SKSpriteNode (not actually moving it) to another SKSpriteNode - I then perform an action based on the 2 nodes. I can do both of these fine separately. But when a user is dragging from one node to another, I don't want the camera to pan.
Again I've managed to resolve this using a boolean flag that's set on touchesBegan if they have touched a node. However the screen then doesn't pan if a user starts touching on one of the sprites but finishes off of one e.g they actually quickly performed a pan that happened to start on a node. Does anyone have any good solutions for this?
One thought I had was to store all the touch events in touchesMoved, and then loop through them in touchesEnded performing the same movement logic, provided they didn't start and end on a sprite. e.g
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
movingTouches.append(touch)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if startedOnSprite && !endedOnSprite || !(startedOnSprite && endedOnSprite) {
let location = movingTouches.last().location(in: self)
let previousLocation = movingTouches.first().location(in: self)
let deltaY = location.y - previousLocation.y
let deltax = location.x - previousLocation.x
gameCamera?.position.y -= deltaY
view.frame.size.height
gameCamera?.position.y = (gameCamera?.position.y)! > viewHeight/2 ? viewHeight/2 : (gameCamera?.position.y)!
gameCamera?.position.y = (gameCamera?.position.y)! < -(viewHeight/2) ? -(viewHeight/2) : (gameCamera?.position.y)!
gameCamera?.position.x -= deltax
gameCamera?.position.x = (gameCamera?.position.x)! < -(viewWidth/2) ? -(viewWidth/2) : (gameCamera?.position.x)!
gameCamera?.position.x = (gameCamera?.position.x)! > (viewWidth/2) ? (viewWidth/2) : (gameCamera?.position.x)!
}
movingTouches = [UITouch]()
endedOnSprite = false
startedOnSprite = false
}
To complicate matters I also want the Sprites to have a tap event on them as well as dragging. I'm struggling to find a good way to do all this in SpriteKit
Unfortunately the above isn't very smooth at all, and in think it appears to not even scroll the distance I would expect either (the exact same code does scroll correctly if I'm not bothered about whether or not it starts on a Sprite)
So to be clear, I want scrolling behaviour, provided a user isn't actually using a finger to 'draw' from one sprite to another
Gesture recognisors on your scene would do what you need, keeping taps and swipes separated.
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 sprite node on the screen, that the player is able to move left and right. I know how to make it move left and right using touchesMoved, however, if the player touches a location the node snaps to that location.
For example, if the node was on the left side of the screen, and the player touched the right side of the screen, the node would immediately move to the touch location. I don't want this behavior. I only want to the node to move when the player is moving their finger left or right, not when they touch a location. Below is the code I currently use. There isn't any code that would affect the playerNode x position in the touchesBegan or touchesEnded function.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches
{
if game.state == .game
{
let position = touch.location(in: playableArea)
game.playerNode.position.x = position.x
}
}
}
How can I stop this behavior from happening, and only have the node move when the players finger is moving left or right?
I ended up resolving my issue. I knew that I had to essentially move the node relative to the touch location, but I wasn't sure how to do that. This is how I got it working.
First I created a variable that would be equal to the players touch location. This variable gets set in touchesBegan.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
if game.state == .game
{
let position = touch.location(in: playableArea)
playerTouchPosition = position
}
}
I use this variable as the starting point of the touch. When the player moves their finger, it calculates how much the player moved their finger relative to the starting point. For example, if the player touched at the X value of 100, and moved their finger to the X value of 120, it would calculate that the player moved their finger by 20. After finding that value, I set the playerTouchPosition to the position of touchesMoved. This is to make the next move be relative to the previous touch. Then I move the players node by the amount the player moved their finger.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches
{
if game.state == .game
{
let position = touch.location(in: playableArea)
let newX : CGFloat!
if position.x > playerTouchPosition.x
{
newX = position.x - playerTouchPosition.x
}else
{
newX = -(playerTouchPosition.x - position.x)
}
playerTouchPosition = position
game.playerNode.position.x = game.playerNode.position.x + newX
}
}
}
This stopped the players node from immediately moving to where ever the touch locations X value was.
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 recently send out a version of my game to TestFlight for internal testing.
My friend noticed that on his iPhone 6S Plus the controls to shoot do not work correctly.
The controls are as follow
1) left half of screen = jump
2) right half of screen = shoot
3) Slide and hold on right half of screen = start slow motion
Now in my touches began method I have a slight delay for the shooting, so if touches moved is called it cancels the shooting and starts slow mo.
This seems to be causing the problem on 3D Touch enabled phones.
Digging further into this I came across this post
iOS 9 Spritekit Double Tap Not Working on iPhone 6S
where it basically states that on 3D Touch enabled devices the touches moved method gets called automatically.
On 3D Touch capable devices, touch pressure changes cause
touchesMoved:withEvent: to be called for all apps running on iOS
9.0. When running iOS 9.1, the method is called only for apps
linked with the iOS 9.0 (or later) SDK.
Apps should be prepared to receive touch move events with no
change in the x/y coordinates. "
This seems to make sense of why my friend has problems, if touches moved is always called it will cancel the shooting and start slow mo, even though he is not sliding on the screen.
So my questions is
1) Can you disable 3D Touch in SpriteKit games?
2) Is this a bug?
3) any other way to use touches moved correctly on 3D Touch devices?
4) alternatively can I use gesture recognisers to simulate a slide and hold feature that I am currently doing in touches moved.
Thanks for any help
After trying various other things that didnt work, including UISwipeGestureRecognizer subclasses, I finally figured it out.
The answer was actually staring me right in the face in that same stackOverflow post I linked in my question
iOS 9 Spritekit Double Tap Not Working on iPhone 6S
The solution is to create a property
var startingTouchLocation: CGPoint?
than in touches Began you set that property
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
/// Set starting touch. this is for 3d touch enabled devices, see touchesMoved method below
startingTouchLocation = location
....
}
}
and than add a check in touchesMoved to return from the method until the touch has actually moved.
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
/// WAY 1 - Check if touch moved because on 3d touch devices this method gets called on a force touch.
guard location != startingTouchLocation else { return }
// Your code
...
/// WAY 2 - Add a min distance for the finger to travel in any direction before calling touches moved code
let minValue: CGFloat = 30
guard let startingLocation = startingLocation else { return }
guard currentLocation.y < startingLocation.y - minValue ||
currentLocation.y > startingLocation.y + minValue ||
currentLocation.x < startingLocation.x - minValue ||
currentLocation.x > startingLocation.x + minValue else { return false }
// Your code
....
}
}
I am trying to implement an overriden touchesMoved func to enable SKSpriteNodes to be moved around by the user. However, I have to move the node very slowly for it to follow my touches when I drag. In addition, there are three SKSpriteNodes in the background (which you will see I explicitly set to .userInteractionEnabled = false) and these nodes will occasionally respond to the touches. Any help would be greatly appreciated. Let me know if there are any other parts of the code you need.
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
var positionInScene = CGPoint(x: 375.0, y: 400.0) //sets a default position
titleLabel.userInteractionEnabled = false
drawingBoard.userInteractionEnabled = false
sideBar.userInteractionEnabled = false
for touch in touches {
positionInScene = touch.locationInNode(self)
if self.nodeAtPoint(positionInScene) is SKSpriteNode {
if (self.nodeAtPoint(positionInScene)).name == movableNodeName { //movableNodeName is the name assigned to all SKSpriteNodes that should be draggable
//I know this might be a strange way of doing it
(self.nodeAtPoint(touch.previousLocationInNode(self))).position = positionInScene
}
}
}
}
I've managed to fix this issue by just setting the coordinates of the node that shouldn't move back to what they were originally whenever it is moved. This seems to work as I haven't been able to replicate the bug again in testing. If anyone could come up with a better solution though, I would love to hear it.
Your problem is you are using touch.locationInNode(self), where self is your SKSpriteNode. This means that your touch move code will only respond to what is going on inside of your SKSpriteNode. What you need to do, is use either the parent of the sprite, or the scene, based on how you want to apply this logic.