How to create a slingshot mechanism in Swift, SpriteKit - swift

I am trying to create a Sling shot mechanism where I rotate a node with an object attached at the end. The slingshot starts off with a touch and hold interaction to rotate the object and on release, launches the object attached at the end - A similar effect is seen here
This is my code for the rotation which has worked. Not sure how to start the joining and slinging part.
var touchingScreen = false
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
touchingScreen = true
println("Screen Touched")
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
super.touchesCancelled(touches, withEvent: event)
touchingScreen = false
println("Screen Not Touched")
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesEnded(touches, withEvent: event)
touchingScreen = false
println("Screen Not Touched")
}
override func update(currentTime: CFTimeInterval) {
if touchingScreen {
var RotatingAngle = CGFloat(M_PI)
var rotationDuration = 20.0
var rotateCanon = SKAction.rotateByAngle(CGFloat(RotatingAngle), duration: rotationDuration)
canon.runAction(rotateCanon)
} else if !touchingScreen {
var RotatingAngleTwo = CGFloat(0)
var rotationDurationTwo = 0.0
var rotateCanon = SKAction.rotateByAngle(CGFloat(RotatingAngleTwo), duration: rotationDurationTwo)
canon.runAction(rotateCanon)
}
}
Thanks for the help.

Some thoughts...
It's fairly straightforward to implement a slingshot as shown in the video if you break it down into smaller steps
If you represent the ball's position on the circle with polar coordinates, you can rotate the ball by simply incrementing the angle over time and then converting from polar to cartesian coordinates
When launched, the ball's trajectory should be tangent to the ball's position on the circle and its speed should be the same as its angular velocity.
and some code...
enum State {
case Stopped
case Rotating
case Launched
}
let two_pi = CGFloat(M_PI*2.0)
let pi = CGFloat(M_PI)
// These are useful vector/point operators
func * (left:CGPoint, right:CGFloat) -> CGPoint {
return CGPointMake(left.x*right, left.y*right)
}
func += (inout left:CGPoint, right:CGPoint) {
left = CGPointMake(left.x+right.x, left.y+right.y)
}
func * (left:CGVector, right:CGFloat) -> CGVector {
return CGVectorMake(left.dx*right, left.dy*right)
}
func / (left:CGVector, right:CGFloat) -> CGVector {
return CGVectorMake(left.dx/right, left.dy/right)
}
class GameScene: SKScene {
let shape = SKShapeNode(circleOfRadius: 7)
let radius:CGFloat = 30
var center = CGPointZero
var currentAngle = -pi/2
let angleIncr = two_pi / 60.0
var state:State = .Stopped
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
// Set the center of the sling
center = CGPointMake (CGRectGetMidX(view.frame),CGRectGetMidY(view.frame))
addBall()
let circle = SKShapeNode(circleOfRadius: radius)
circle.position = CGPointMake (CGRectGetMidX(view.frame),CGRectGetMidY(view.frame))
addChild(circle)
}
// Adds a circle shape node at the bottom of the sling
func addBall() {
currentAngle = -pi/2
shape.fillColor = SKColor.blueColor()
shape.position = CGPointMake (center.x, center.y-radius)
shape.physicsBody = SKPhysicsBody(circleOfRadius: 7)
shape.physicsBody?.affectedByGravity = false
shape.physicsBody?.mass = 0.5
shape.zPosition = 1
addChild(shape)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
if let touch = touches.first as? UITouch {
let location = touch.locationInNode(self)
if (state == .Stopped) {
// Start rotating the ball around the sling
state = .Rotating
}
}
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if (state == .Rotating) {
// Launch the ball on a vector tangent to its current position on the circle
state = .Launched
// Normal vector
var normal = CGVectorMake(shape.position.x-center.x, shape.position.y-center.y)
normal = normal / magnitude(normal)
// Tangent vector
let vector = CGVectorMake(normal.dy, -normal.dx)
// Convert angular to linear speed
let speed = angleIncr * 60.0 * radius
shape.physicsBody?.velocity = vector*speed
runAction(SKAction.waitForDuration(1.0)) {
self.shape.removeFromParent()
self.state = .Stopped
self.addBall()
}
}
}
override func update(currentTime: CFTimeInterval) {
switch (state) {
case .Rotating:
var point = angleToPoint(currentAngle) * radius
point += center
shape.position = point
currentAngle -= angleIncr
// Wrap at 2 pi
currentAngle %= two_pi
default:
break
}
}
func angleToPoint(angle:CGFloat) -> CGPoint {
return CGPointMake(cos(angle), sin(angle))
}
func magnitude(v1:CGVector) -> CGFloat {
return sqrt(v1.dx*v1.dx+v1.dy*v1.dy)
}
}
and a video...

Related

Sprite follows finger when dragged on screen Spritekit

I am attempting to create a walking animation where the sprite moves towards a touch location that can be dragged around the screen. I have managed to get Xcode to print updated X and Y coordinates where the finger is dragged but the sprite will barely move and I haven't been able to solve this issue. The touchesMoved has correctly printed the x and y coordinates but the moveGuy function will not perform correctly.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches { self.touchMoved(toPoint: touch.location(in: self))
let location = touch.location(in: self)
print(location)
moveGuy(location: location)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guyMoveEnded()
}
func moveGuy(location: CGPoint) {
var multiplierForDirection: CGFloat
// 2
let guySpeed = frame.size.width / 3.0
// 3
let moveDifference = CGPoint(x: location.x - guy.position.x, y: location.y - guy.position.y)
let distanceToMove = sqrt(moveDifference.x * moveDifference.x + moveDifference.y * moveDifference.y)
// 4
let moveDuration = distanceToMove / guySpeed
// 5
if moveDifference.x < 0 {
multiplierForDirection = -1.0
} else {
multiplierForDirection = 1.0
}
guy.xScale = abs(guy.xScale) * multiplierForDirection
// 1
if guy.action(forKey: "walkingInPlaceguy") == nil {
// if legs are not moving, start them
animateguy()
}
if guy.position == (location) {
guyMoveEnded()
}
// 2
let moveAction = SKAction.move(to: location, duration:(TimeInterval(moveDuration)))
// 3
let doneAction = SKAction.run({ [weak self] in
self?.guyMoveEnded()
})
// 4
let moveActionWithDone = SKAction.sequence([moveAction, doneAction])
guy.run(moveActionWithDone, withKey:"guyMoving")
}

Apply impulse depending on rotation - Swift

I am currently creating a 2D game in which I have a cannon staying at the bottom of the screen that fires projectiles to some targets in the upper part of the screen. The user can rotate the cannon in order to aim for the targets. I want the projectile to be fired to the location that the canon faces. I have no idea how to do that and I need your help... Heres my code ->
import SpriteKit
class GameScene: SKScene {
var canon = SKSpriteNode()
var projectile = SKSpriteNode()
var touching = false
var locked = false
var location = CGPoint()
override func didMoveToView(view: SKView) {
/* Setup your scene here */
projectile.position = canon.position
projectile.color = UIColor.whiteColor()
projectile.size = CGSize(width: 50, height: 50)
projectile.physicsBody?.affectedByGravity = false
projectile.physicsBody?.linearDamping = 1
projectile.physicsBody?.dynamic = true
addChild(projectile)
canon.zPosition = 2
canon = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: 50, height: 200))
canon.position = CGPoint(x: self.size.width/2.0, y: 10)
addChild(canon)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
touching = true
let touch = touches.first as UITouch!
location = touch.locationInView(self.view)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
rotateCanon(location.x)
projectile.zRotation = canon.zRotation
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
touching = false
}
func rotateCanon(locationx:CGFloat) {
if locationx <= frame.width/5 && touching {
print(1)
canon.runAction(SKAction.rotateByAngle(0.01, duration: 0.1))
}
if locationx > frame.width/5 && touching {
print(2)
canon.runAction(SKAction.rotateByAngle(-0.01, duration: 0.1))
}
}
}`
Thanks a lot if you can help that would be awesome :D

Continous minimum velocity tracking of a finger on a sprite in spritekit/ios

What is a good way to continuously track the velocity of a user moving their finger up and down on a sprite without lifting the finger? And if they don't go fast enough(set a minimum) to perform an action.
This is some code you can use to drag a sprite across the screen at different speeds depending on your finger acceleration. You can add your own code to detect the magnitude of change and perform whichever action you want..
class GameScene: SKScene {
// time values
var delta:NSTimeInterval = NSTimeInterval(0)
var last_update_time:NSTimeInterval = NSTimeInterval(0)
// our sprite
let sprite = SKSpriteNode(color: SKColor.redColor(), size: CGSize(width: 20, height: 30))
// tracking difference between touches
var currentTouchY: CGFloat = CGFloat(0)
var lastTouchY: CGFloat = CGFloat(0)
// the magnitude of change between current and last touch
var forceMag: CGFloat = CGFloat(0)
// max mag so our sprite doesnt travel at crazy speed
let forceMagMax: CGFloat = CGFloat(7)
// direction of our sprites travel
var forceDir: CGFloat = CGFloat(0)
// arbitrary speed we want sprite to move
let velocity: CGFloat = CGFloat(70)
// we use this to stop moving our sprite when we let go
var intervalsWithoutChange = 0
override func didMoveToView(view: SKView) {
sprite.position.x = 100
sprite.position.y = size.width/2
self.addChild(sprite)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let yCoord = location.y
lastTouchY = yCoord
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let yCoord = location.y
intervalsWithoutChange = 0
currentTouchY = yCoord
let diff = currentTouchY - lastTouchY
forceMag = abs(diff)
forceMag = forceMag > forceMagMax ? forceMagMax : forceMag
forceDir = diff < 0 ? -1 : 1
lastTouchY = yCoord
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
intervalsWithoutChange = 1
currentTouchY = 0
lastTouchY = 0
}
override func update(currentTime: NSTimeInterval) {
if last_update_time == 0.0 {
delta = 0
} else {
delta = currentTime - last_update_time
}
last_update_time = currentTime
if intervalsWithoutChange > 0 {
forceMag -= CGFloat(15) * CGFloat(delta)
}
if forceMag < 0.01 {
forceMag = 0
}
let change = forceDir * pow(forceMag, 1.29)
sprite.position.y += change * CGFloat(delta) * velocity
}
}

Q: How to make it so player can jump to wherever I tap, but not keep moving up when touch is held?

Im creating a game like Doodle jump, and I want it to be so that my hero does jump to wherever I tap, but when I hold that touch on the screen, it just keeps moving up until it is off screen, which isn't what I'm aiming for. I just want it to be so that no matter how much I tap and hold, my hero does not keep moving up.
Here is my code for my touches functions:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
player.animatePlayer()
let touch = touches.first as? UITouch
let touchLocation = touch!.locationInNode(self)
lastTouch = touchLocation
player.physicsBody?.categoryBitMask = snowCategory
player.physicsBody?.contactTestBitMask = playerCategory
player.physicsBody?.affectedByGravity = true
player.physicsBody?.collisionBitMask = playerCategory
player.physicsBody?.usesPreciseCollisionDetection = true
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
lastTouch = nil
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
lastTouch = nil
}
override func update(currentTime: NSTimeInterval) {
if let touch = lastTouch {
var xForce: CGFloat = 0.0
var yForce: CGFloat = 0.0
let xTouchOffset = (touch.x - player.position.x)
let yTouchOffset = (touch.y - player.position.y)
if xTouchOffset > 0.0 {
xForce = xPlayerForce
} else if xTouchOffset < 0.0 {
xForce = -xPlayerForce
} // else we do nothing
if yTouchOffset > 0.0 {
yForce = yPlayerForce
} else if xTouchOffset > 0.0 {
yForce = -yPlayerForce
}
// here you can choose whether you want it to push
// the player node down, using similar code from the
// above if statement
let impulseVector = CGVector(dx: xForce, dy: yForce)
player.physicsBody?.applyImpulse(impulseVector)
}
}
It seems your object movement is activated when you lift your finger. Instead of doing this, I would say put your code in touches began, or at least have some way of stopping the object when it gets where you want. Right now you have it in an update method that continues to run after you lift your finger, so that's why it keeps going.

Drag Rotate a Node around a fixed point

I'm trying to create a spinable node similar to the "prize wheel" in this question. So far I have the flinging capability, adding angular impulses on a physics body using a UIPanGestureRecognizer that works really well. I can also stop the spinning using a touch.
Now I'm trying to allow fine adjustment of the wheel using a drag or swipe gesture so if the player isn't happy with what they end up with they can manually spin/drag/rotate it to their favoured rotation.
Currently I save the location of the touch in the touchesBegan and try to increment the zRotation of my node in the update loop.
The rotation doesn't follow my finger and is jerky. I'm not sure if I'm getting an accurate enough read on the finger movement or if the change position of the finger isn't being accurately translated into radians. I suspect detecting the touch and then dealing with it in the update isn't a great solution.
Here's my code.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent?) {
if let touch = touches.first as? UITouch {
var location = touch.locationInView(self.view)
location = self.convertPointFromView(location)
mostRecentTouchLocation = location
let node = nodeAtPoint(location)
if node.name == Optional("left") && node.physicsBody?.angularVelocity != 0
{
node.physicsBody = SKPhysicsBody(circleOfRadius:150)
node.physicsBody?.applyAngularImpulse(0)
node.physicsBody?.pinned = true
}
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if mostRecentTouchLocation != CGPointZero{
let node = nodeAtPoint(mostRecentTouchLocation)
if node.name == Optional("left")
{
var positionInScene:CGPoint = mostRecentTouchLocation
let deltaX:Float = Float(positionInScene.x) - Float(node.position.x)
let deltaY:Float = Float(positionInScene.y) - Float(node.position.y)
let angle:CGFloat = CGFloat(atan2f(deltaY, deltaX))
let maths:CGFloat = angle - (CGFloat(90) * (CGFloat(M_PI) / 180.0))
node.zRotation += maths
mostRecentTouchLocation = CGPointZero
}
}
}
I've spread some of the maths across multiple lines in the update to make debugging a bit easier.
I can add the PanGestureRecognizer code if needed but I'll try to keep it short for now.
EDIT
Here is my latest code based on GilderMan's recommendation. I think it's working better but the rotation is far from smooth. It's jumping in large increments and not following the finger well. Does this mean there's something wrong with my angle calculation?
override func didSimulatePhysics() {
if mostRecentTouchLocation != CGPointZero {
let node = nodeAtPoint(mostRecentTouchLocation)
if node.name == Optional("left")
{
var positionInScene:CGPoint = mostRecentTouchLocation
let deltaX:Float = Float(positionInScene.x) - Float(node.position.x)
let deltaY:Float = Float(positionInScene.y) - Float(node.position.y)
let angle:CGFloat = CGFloat(atan2f(deltaY, deltaX))
node.zRotation += angle
println(angle)
mostRecentTouchLocation = CGPointZero
}
}
}
The following code simulates a prize wheel that spins based on touch. As the user's finger moves, the wheel rotates proportionately to the speed of the finger. When the user swipes on the wheel, the wheel will spin proportionately to the velocity of the swipe. You can change the angularDamping property of the physics body to slow or increase the rate at which the wheel comes to a stop.
class GameScene: SKScene {
var startingAngle:CGFloat?
var startingTime:TimeInterval?
override func didMove(to view: SKView) {
let wheel = SKSpriteNode(imageNamed: "Spaceship")
wheel.name = "wheel"
wheel.setScale(0.5)
wheel.physicsBody = SKPhysicsBody(circleOfRadius: wheel.size.width/2)
// Change this property as needed (increase it to slow faster)
wheel.physicsBody!.angularDamping = 0.25
wheel.physicsBody?.pinned = true
wheel.physicsBody?.affectedByGravity = false
addChild(wheel)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in:self)
let node = atPoint(location)
if node.name == "wheel" {
let dx = location.x - node.position.x
let dy = location.y - node.position.y
// Store angle and current time
startingAngle = atan2(dy, dx)
startingTime = touch.timestamp
node.physicsBody?.angularVelocity = 0
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let location = touch.location(in:self)
let node = atPoint(location)
if node.name == "wheel" {
let dx = location.x - node.position.x
let dy = location.y - node.position.y
let angle = atan2(dy, dx)
// Calculate angular velocity; handle wrap at pi/-pi
var deltaAngle = angle - startingAngle!
if abs(deltaAngle) > CGFloat.pi {
if (deltaAngle > 0) {
deltaAngle = deltaAngle - CGFloat.pi * 2
}
else {
deltaAngle = deltaAngle + CGFloat.pi * 2
}
}
let dt = CGFloat(touch.timestamp - startingTime!)
let velocity = deltaAngle / dt
node.physicsBody?.angularVelocity = velocity
// Update angle and time
startingAngle = angle
startingTime = touch.timestamp
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startingAngle = nil
startingTime = nil
}
}
The zRotation of a SKNode is in radians. You can remove your conversion to degrees.
You may wish to do the angle adjustment in the didSimulatePhysics in order to compute the zRotation after physics have been applied. (This may not apply directly to this situation, but is good practice further down the line.)
I tried two set of coodes below.
Code Sample 1
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
let dx = location.x
let dy = location.y
startingAngle = atan2(dx, dy)
startingTime = touch.timestamp
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
if !isRotating {
let dx = location.x
let dy = location.y
let angle = atan2(dx, dy)
wheel.zRotation = -angle
//update angle and time
startingAngle = angle
startingTime = touch.timestamp
}
}
}
}
Code Sample 2
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
let dx = location.x
let dy = location.y
startingAngle = atan2(dx, dy)
startingTime = touch.timestamp
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
guard let safeNode = node.parent else { break }
if safeNode.name == "wheel" {
let wheel = node.parent!
if !isRotating {
let dx = location.x
let dy = location.y
let angle = atan2(dx, dy)
let deltaAngle = angle - startingAngle!
wheel.zRotation -= deltaAngle
//update angle and time
startingAngle = angle
startingTime = touch.timestamp
}
}
}
}
The only difference between Code Sample 1 & 2 is the method of finding zRotation of the wheel.
Code Sample 1 uses wheel.zRotation = -angle. It ensures wheel follows the finger position exactly. But there is a bug that the wheel will jump position when you touch two distant locations in wheel then move your fingure. I am still unable to fix this bug.
Code Sample 2 uses wheel.zRotation -= deltaAngle to stack the change in angle of each finger movement. It supposes to generate the same result of code sample 1 but it does not exactly follow the finger movement.
I found out the reason by comparing the zPosition of Code Sample 1 and 2 with same finger movement. The deltaAngle in Code Sample 2 is slightly less (up to 8 decimal places)so the wheel does not exactly follow the fingure movement.