I have a demo app for testing physics in SpriteKit. I was able to simulate inertia after throwing sprite with pan gesture. When I release finger bouncing works great.
Problem is: How to simulate bouncing during dragging? I would like to red SKSpriteNode bounce off when I hit it with dragging SKSpriteNode.
I tried to set self.selectedNode?.physicsBody?.isDynamic = true but then sprite moves away under finger even when I set correct position.
Here is my repo. Feel free to copy or experiment.
Crucial code:
func handlePan(panGestureRecognizer recognizer:UIPanGestureRecognizer) {
let touchLocationView = recognizer.location(in: recognizer.view)
let touchLocationScene = self.convertPoint(fromView: touchLocationView)
switch recognizer.state {
case .began:
self.showMoved = false
let canditateNode = self.touchedNode(touchLocationScene)
if let name = canditateNode.name, name.contains(kMovableNode) {
self.selectedNode = canditateNode
self.selectedNode?.physicsBody?.isDynamic = false;
}
case .changed:
let translation = recognizer.translation(in: recognizer.view)
if let position = self.selectedNode?.position {
self.selectedNode?.position = CGPoint(x: position.x + translation.x, y: position.y - translation.y)
recognizer.setTranslation(CGPoint.zero, in: recognizer.view)
}
case .ended:
self.selectedNode?.physicsBody?.isDynamic = true;
let velocity = recognizer.velocity(in: recognizer.view)
self.selectedNode?.physicsBody?.applyImpulse(CGVector(dx: velocity.x, dy: -velocity.y))
self.selectedNode = nil
self.showMoved = true
default:
break
}
}
Related
I'm having a problem when trying to drag a view using a pan gesture recognizer. The view is a collectionViewCell and the dragging code is working, except when the drag starts the view jumps up and to the left. My code is below.
In the collectionViewCell:
override func awakeFromNib() {
super.awakeFromNib()
let panRecognizer = UIPanGestureRecognizer(target:self, action:#selector(detectPan))
self.gestureRecognizers = [panRecognizer]
}
var firstLocation = CGPoint(x: 0, y: 0)
var lastLocation = CGPoint(x: 0, y: 0)
#objc func detectPan(_ recognizer:UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
firstLocation = recognizer.translation(in: self.superview)
lastLocation = recognizer.translation(in: self.superview)
case .changed:
let translation = recognizer.translation(in: self.superview)
self.center = CGPoint(x: lastLocation.x + translation.x, y: lastLocation.y + translation.y)
default:
UIView.animate(withDuration: 0.1) {
self.center = self.firstLocation
}
}
}
The first image is before the drag starts, the second is what happens when dragging up.
You're using self.center instead of using self.frame.origin.x and self.frame.origin.y then later you're setting your translation and adding it to the lastLocation.
Effectively what's happening is that your view is calculating the position changed from the center of the view, as if you were perfectly dragging from that location and then translating + lastLocation. I'm sure just by reading that you're aware of the issue.
The fix is simple.
self.frame.origin.x = translation.x
self.frame.origin.y = translation.y
The difference is the starting calculation with the translation. Origin will grab the x/y position based on where the touch event begins. Whereas the .center always goes from the center.
I'm developing a card view like in Tinder. When cards X origin is bigger than a value which I declare, It moves out the screen. Otherwise, It sticks to center again. I'm doing all of these things inside UIPanGestureRecognizer function. I can move the view in Change state. However, It sometimes doesn't get into end state so card is neither moves out of the screen or stick to center again. It just stays in some weird place.
So My problem is that card should go out of the screen like in below screenshot or stick into center.
I tried solutions in below post but nothing worked:
UIPanGestureRecognizer not calling End state
UIPanGestureRecognizer does not switch to state "End" or "Cancel" if user panned x and y in negative direction
/// This method handles the swiping gesture on each card and shows the appropriate emoji based on the card's center.
#objc func handleCardPan(sender: UIPanGestureRecognizer) {
// Ensure it's a horizontal drag
let velocity = sender.velocity(in: self.view)
if abs(velocity.y) > abs(velocity.x) {
return
}
// if we're in the process of hiding a card, don't let the user interace with the cards yet
if cardIsHiding { return }
// change this to your discretion - it represents how far the user must pan up or down to change the option
// distance user must pan right or left to trigger an option
let requiredOffsetFromCenter: CGFloat = 80
let panLocationInView = sender.location(in: view)
let panLocationInCard = sender.location(in: cards[0])
switch sender.state {
case .began:
dynamicAnimator.removeAllBehaviors()
let offset = UIOffsetMake(cards[0].bounds.midX, panLocationInCard.y)
// card is attached to center
cardAttachmentBehavior = UIAttachmentBehavior(item: cards[0], offsetFromCenter: offset, attachedToAnchor: panLocationInView)
//dynamicAnimator.addBehavior(cardAttachmentBehavior)
let translation = sender.translation(in: self.view)
print(sender.view!.center.x)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .changed:
//cardAttachmentBehavior.anchorPoint = panLocationInView
let translation = sender.translation(in: self.view)
print(sender.view!.center.x)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y)
sender.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
case .ended:
dynamicAnimator.removeAllBehaviors()
if !(cards[0].center.x > (self.view.center.x + requiredOffsetFromCenter) || cards[0].center.x < (self.view.center.x - requiredOffsetFromCenter)) {
// snap to center
let snapBehavior = UISnapBehavior(item: cards[0], snapTo: CGPoint(x: self.view.frame.midX, y: self.view.frame.midY + 23))
dynamicAnimator.addBehavior(snapBehavior)
} else {
let velocity = sender.velocity(in: self.view)
let pushBehavior = UIPushBehavior(items: [cards[0]], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x/10, dy: velocity.y/10)
pushBehavior.magnitude = 175
dynamicAnimator.addBehavior(pushBehavior)
// spin after throwing
var angular = CGFloat.pi / 2 // angular velocity of spin
let currentAngle: Double = atan2(Double(cards[0].transform.b), Double(cards[0].transform.a))
if currentAngle > 0 {
angular = angular * 1
} else {
angular = angular * -1
}
let itemBehavior = UIDynamicItemBehavior(items: [cards[0]])
itemBehavior.friction = 0.2
itemBehavior.allowsRotation = true
itemBehavior.addAngularVelocity(CGFloat(angular), for: cards[0])
dynamicAnimator.addBehavior(itemBehavior)
showNextCard()
hideFrontCard()
}
default:
break
}
}
I was checking If I'm swiping in horizontal with:
let velocity = sender.velocity(in: self.view)
if abs(velocity.y) > abs(velocity.x) {
return
}
For some reason, It was getting in to return while I'm swiping horizontal. When I comment this block of code, everything started to work :)
I have a SCNNode that I've set at a position of SCNVector3(0,0,-1). The following code moves the node forward or backward along the Z-axis, however, the initial pan gesture moves the node to (0,0,0) and then moves the node in line with the pan gesture.
let currentPositionDepth = CGPoint()
#objc func handlePan(sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: sceneView)
var newPos = CGPoint(x: CGFloat(translation.x), y: CGFloat(translation.y))
newPos.x += currentPositionDepth.x
newPos.y += currentPositionDepth.y
node.position.x = Float(newPos.x)
node.position.z = Float(newPos.y)
if(sender.state == .ended) { currentPositionDepth = newPos }
}
I'd like the node to move from it's set position of (0,0,-1). I've tried setting currentPositionDepth.y to -1, however it does not achieve the desired effect. How can I achieve this?
Try something like this:
var previousLoc = CGPoint.init(x: 0, y: 0)
#objc func panAirShip(sender: UIPanGestureRecognizer){
var delta = sender.translation(in: self.view)
let loc = sender.location(in: self.view)
if sender.state == .changed {
delta = CGPoint.init(x: 2 * (loc.x - previousLoc.x), y: 2 * (loc.y - previousLoc.y))
airshipNode.position = SCNVector3.init(airshipNode.position.x + Float(delta.x * 0.02), airshipNode.position.y + Float(-delta.y * (0.02)), 0)
previousLoc = loc
}
previousLoc = loc
}
I have multiplied the 0.02 factor to make the translation smoother and in turn easier for the end user. You may change that factor to anything else you like.
I have set up some custom camera controls in my SceneKit game. I am having a problem with my pan gesture auto-adapting based on the cameras y euler angle. The pan gesture I have works by panning the camera on the x and z axis (by using the gestures translation) The problem is, despite the cameras rotation, the camera will continue to pan on the x and z axis. I want it so that the camera pans on the axis its facing.
here are my gestures I am using to pan/rotate:
panning:
var previousTranslation = SCNVector3(x: 0.0,y: 15,z: 0.0)
var lastWidthRatio:Float = 0
var angle:Float = 0
#objc func pan(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
gesture.maximumNumberOfTouches = 1
if gesture.numberOfTouches == 1 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "Node", recursively: false)
let secondNode = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translation = gesture.translation(in: view)
let constant: Float = 30.0
angle = secondNode!.eulerAngles.y
//these were the previous values I was using to handle panning, they worked but provided really jittery movement. You can change the direction they rotate by multiplying the sine/cosine .pi values by any integer.
//var translateX = Float(translation.y) * sin(.pi) / cos(.pi) - Float(translation.x) * cos(.pi)
//var translateY = Float(translation.y) * cos(.pi) / cos(.pi) + Float(translation.x) * sin(.pi)
//these ones work a lot smoother
var translateX = Float(translation.x) * Float(Double.pi / 180)
var translateY = Float(translation.y) * Float(Double.pi / 180)
translateX = translateX * constant
translateY = translateY * constant
switch gesture.state {
case .began:
previousTranslation = node!.position
break;
case .changed:
node!.position = SCNVector3Make((previousTranslation.x + translateX), previousTranslation.y, (previousTranslation.z + translateY))
break
default: break
}
}
}
rotation:
#objc func rotate(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 2
gesture.maximumNumberOfTouches = 2
if gesture.numberOfTouches == 2 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translate = gesture.translation(in: view)
var widthRatio:Float = 0
widthRatio = Float(translate.x / 10) * Float(Double.pi / 180)
switch gesture.state {
case .began:
lastWidthRatio = node!.eulerAngles.y
break
case .changed:
node!.eulerAngles.y = lastWidthRatio + widthRatio
print(node!.eulerAngles.y)
break
default: break
}
}
}
the CameraHandler Node is the parent node of the Camera Node. It all works, it just doesnt work like I want it to. Hopefully this is clear enough for you guys to understand.
In Objective C. The key part is the last three lines and specifically the order of multiplication of the matrices in the last line (causing the movement to happen in local space). If the transmat and cammat are switched it would behave again like you have now (moving in world space). The refactor part is just something that works for my specific situation where both perspective and orthographic camera is possible.
-(void)panCamera :(CGPoint)location {
CGFloat dx = _prevlocation.x - location.x;
CGFloat dy = location.y - _prevlocation.y;
_prevlocation = location;
//refactor dx and dy based on camera distance or orthoscale
if (cameraNode.camera.usesOrthographicProjection) {
dx = dx / 416 * cameraNode.camera.orthographicScale;
dy = dy / 416 * cameraNode.camera.orthographicScale;
} else {
dx = dx / 720 * cameraNode.position.z;
dy = dy / 720 * cameraNode.position.z;
}
SCNMatrix4 cammat = self.cameraNode.transform;
SCNMatrix4 transmat = SCNMatrix4MakeTranslation(dx, 0, dy);
self.cameraNode.transform = SCNMatrix4Mult(transmat, cammat);
}
I figured it out, based off of what Xartec answered. I translated it into swift and retro-fitted it to work with what I needed. I'm not truly happy with it because the movement is not smooth. I will work on smoothing it out later today.
pan gesture: This gesture pans the cameras parent node around the scene in the direction that the camera is rotated. It works exactly like I wanted.
#objc func pan(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
gesture.maximumNumberOfTouches = 1
if gesture.numberOfTouches == 1 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translation = gesture.translation(in: view)
var dx = previousTranslation.x - translation.x
var dy = previousTranslation.y - translation.y
dx = dx / 100
dy = dy / 100
print(dx,dy)
let cammat = node!.transform
let transmat = SCNMatrix4MakeTranslation(Float(dx), 0, Float(dy))
switch gesture.state {
case .began:
previousTranslation = translation
break;
case .changed:
node!.transform = SCNMatrix4Mult(transmat, cammat)
break
default: break
}
}
}
rotation gesture: This gesture rotates the camera with two fingers.
#objc func rotate(gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 2
gesture.maximumNumberOfTouches = 2
if gesture.numberOfTouches == 2 {
let view = self.view as! SCNView
let node = view.scene!.rootNode.childNode(withName: "CameraHandler", recursively: false)
let translate = gesture.translation(in: view)
var widthRatio:Float = 0
widthRatio = Float(translate.x / 10) * Float(Double.pi / 180)
switch gesture.state {
case .began:
lastWidthRatio = node!.eulerAngles.y
break
case .changed:
node!.eulerAngles.y = lastWidthRatio + widthRatio
break
default: break
}
}
}
To get the same functionality that I have, you need to attach the cameraNode to a parent node. Like so:
//create and add a camera to the scene
let cameraNode = SCNNode()
//cameraHandler is declared outside viewDidLoad.
let cameraHandler = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.name = "Camera"
cameraNode.position = SCNVector3(x: 0.0,y: 10.0,z: 20.0)
//This euler angle only rotates the camera downward a little bit. It is not neccessary
cameraNode.eulerAngles = SCNVector3(x: -0.6, y: 0, z: 0)
cameraHandler.addChildNode(cameraNode)
cameraHandler.name = "CameraHandler"
scene.rootNode.addChildNode(cameraHandler)
I am working on an augmented reality app and I would like to be able to drag an object in the space. The problem with the solutions I find here in SO, the ones that suggest using projectPoint/unprojectPoint, is that they produce movement along the XY plane.
I was trying to use the fingers movement on the screen as an offset for x and z coordinates of the node. The problem is that there is a lot of stuff to take in consideration (camera's position, node's position, node's rotation, etc..)
Is there a simpler way of doing this?
I have updated #Alok answer as in my case it is drgging in x plane only from above solution. So i have added y coordinates, working for me.
var PCoordx: Float = 0.0
var PCoordy: Float = 0.0
var PCoordz: Float = 0.0
#objc func handleDragGesture(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
let hitNode = self.sceneView.hitTest(sender.location(in: self.sceneView),
options: nil)
self.PCoordx = (hitNode.first?.worldCoordinates.x)!
self.PCoordy = (hitNode.first?.worldCoordinates.y)!
self.PCoordz = (hitNode.first?.worldCoordinates.z)!
case .changed:
// when you start to pan in screen with your finger
// hittest gives new coordinates of touched location in sceneView
// coord-pcoord gives distance to move or distance paned in sceneview
let hitNode = sceneView.hitTest(sender.location(in: sceneView), options: nil)
if let coordx = hitNode.first?.worldCoordinates.x,
let coordy = hitNode.first?.worldCoordinates.y,
let coordz = hitNode.first?.worldCoordinates.z {
let action = SCNAction.moveBy(x: CGFloat(coordx - PCoordx),
y: CGFloat(coordy - PCoordy),
z: CGFloat(coordz - PCoordz),
duration: 0.0)
self.photoNode.runAction(action)
self.PCoordx = coordx
self.PCoordy = coordy
self.PCoordz = coordz
}
sender.setTranslation(CGPoint.zero, in: self.sceneView)
case .ended:
self.PCoordx = 0.0
self.PCoordy = 0.0
self.PCoordz = 0.0
default:
break
}
}
first you need to create floor or very large plane few meters (i have 10) below origin. This makes sure your hittest always returns value. Then using pan gesture :
//store previous coordinates from hittest to compare with current ones
var PCoordx: Float = 0.0
var PCoordz: Float = 0.0
#objc func move(_ gestureRecognizer: UIPanGestureRecognizer){
if gestureRecognizer.state == .began{
let hitNode = sceneView.hitTest(gestureRecognizer.location(in: sceneView), options: nil)
PCoordx = (hitNode.first?.worldCoordinates.x)!
PCoordz = (hitNode.first?.worldCoordinates.z)!
}
// when you start to pan in screen with your finger
// hittest gives new coordinates of touched location in sceneView
// coord-pcoord gives distance to move or distance paned in sceneview
if gestureRecognizer.state == .changed {
let hitNode = sceneView.hitTest(gestureRecognizer.location(in: sceneView), options: nil)
if let coordx = hitNode.first?.worldCoordinates.x{
if let coordz = hitNode.first?.worldCoordinates.z{
let action = SCNAction.moveBy(x: CGFloat(coordx-PCoordx), y: 0, z: CGFloat(coordz-PCoordz), duration: 0.1)
node.runAction(action)
PCoordx = coordx
PCoordz = coordz
}
}
gestureRecognizer.setTranslation(CGPoint.zero, in: sceneView)
}
if gestureRecognizer.state == .ended{
PCoordx = 0
PCoordz = 0
}
}
In my case there is only one node so i have not checked if required node is taped or not. You can always check for it if you have many nodes.
If I understand you correctly, I do this using a UIPanGestureRecognizer added to the ARSCNView.
In my case I want to check if pan was started on a given virtual object and keep track of which it was because I can have multiple, but if you have just one object you may not need the targetNode variable.
The 700 constant I use to divide I got it by trial and error to make the translation smoother, you may need to change it for your case.
Moving finger up, moves the object further away from camera and moving it down moves it nearer. Horizontal movement of fingers moves object left/right.
#objc func onTranslate(_ sender: UIPanGestureRecognizer) {
let position = sender.location(in: scnView)
let state = sender.state
if (state == .failed || state == .cancelled) {
return
}
if (state == .began) {
// Check pan began on a virtual object
if let objectNode = virtualObject(at: position).node {
targetNode = objectNode
latestTranslatePos = position
}
}
else if let _ = targetNode {
// Translate virtual object
let deltaX = Float(position.x - latestTranslatePos!.x)/700
let deltaY = Float(position.y - latestTranslatePos!.y)/700
targetNode!.localTranslate(by: SCNVector3Make(deltaX, 0.0, deltaY))
latestTranslatePos = position
if (state == .ended) {
targetNode = nil
}
}