Swift-Setting a Physics Body Velocity by Angle

Swift-Making an SKNode simply move forward on an angle

To do this, you just need to use some trigonometry!

When zRotation is between 0 and 90, you don't need to do anything to the angle except converting it to radians. Just call tan(radianAngle). Now tan will return how much should the node move in the y axis when it moves by 1 in the x axis. If you learned trigonometry before, you should understand what I'm saying. If you have not learned trigonometry, learn it. :)

Let's say the node's zRotation is 60 degrees, which is π/3 radians. Pass that into tan and you get √3. This means your dx and dy parameters in SKAction must be in the ratio of 1 : √3 in order to make the node move in the direction of 60 degrees.

When zRotation is between 90 and 180, you need to first subtract the angle from 180, then convert it to radians. Again, pass that to tan and the return value is how much your node should move in the y direction when it moves by -1 in the x axis. The dx : dy ratio is now -1 : tan(angleInRadians).

When zRotation is between 180 and 270, subtract 180 from that angle and convert it to radians. The dx : dy ratio is -1 : -tan(angleInRadians).

Lastly, a zRotation bewteen 270 and 360, subtract the angle from 360 and convert it to radians. The dx : dy ratio is 1 : -tan(angleInRadians).

Just before you convert the angle to radians, check if the angle is 90 degrees. If it is, please hard code the dx and dy because tan(M_PI / 2) is undefined.

How to give a physics body to an arc?

You may want to avoid using an SKCropNode to build your arcs. From Apple's documentation,

Use clipping and effect nodes sparingly. Both are very powerful, but can be expensive, especially when nested
together within the node tree.

Alternatively, you can construct an arc-shaped core graphics path and then create a shape node from the path. You can then create a physics body using the CG path.

To build the arc-shaped path,

  1. Add an inner arc from the starting angle to the ending angle
  2. Add a line from the inner arc's ending point to the outer arc's ending point
  3. Add the outer arc from the ending angle to the starting angle
  4. Close the path (which connects the arcs starting points)

Extending CGPath to construct the arc path is not necessary, but often it's more convenient to use. Once extended, the new class method can be called from anywhere in your code. Here's an example:

extension CGPath {
static func arcWithWidth(arcWidth:CGFloat, start:CGFloat, end:CGFloat, radius:CGFloat, clockwise:Bool) -> CGPath {
// The radius parameter specifies the middle of the arc; adjust this as needed
let innerRadius:CGFloat = radius - arcWidth / 2.0
let outerRadius:CGFloat = radius + arcWidth / 2.0

// Note the arc is upside down because CGPath uses UIKit coordinates
let path = UIBezierPath()
// Add inner ring.
path.addArcWithCenter(CGPointZero, radius: innerRadius, startAngle: start, endAngle: end, clockwise: clockwise)
let x = outerRadius * cos(end)
let y = outerRadius * sin(end)

// Connect the inner to the outer ring
path.addLineToPoint(CGPointMake(x, y))

// Add outer ring
path.addArcWithCenter(CGPointZero, radius: outerRadius, startAngle: end, endAngle: start, clockwise: !clockwise)

path.closePath()

return path.CGPath
}
}

With the extension, you can create the top and bottom arcs:

    // Top arc
var path = CGPath.arcWithWidth(20, start:0, end: CGFloat(M_PI), radius: 100, clockwise: true)

let topArc = SKShapeNode(path: path)
topArc.position = view.center
topArc.fillColor = SKColor.redColor()
topArc.strokeColor = SKColor.clearColor()

// Add a physics body to the top half
topArc.physicsBody = SKPhysicsBody(polygonFromPath: path)
topArc.physicsBody?.affectedByGravity = false

addChild(topArc)

// Bottom arc
path = CGPath.arcWithWidth(20, start:0, end: CGFloat(M_PI), radius: 100, clockwise: false)
let bottomArc = SKShapeNode(path: path)
bottomArc.position = view.center
bottomArc.fillColor = SKColor.blueColor()
bottomArc.strokeColor = SKColor.clearColor()

addChild(bottomArc)

how to set physics properties for a circle so it follows given path

how to set physics properties for a circle so it follows given path

So essentially you are looking to move a node to a particular point using real-time motion. I have an answer here showing how to do this, however given the number of up votes this question has received, I will provide a more detailed answer.

What the answer I linked to doesn't provide is traversing a path of points. So below I have provided a solution showing how this can be done below. It simply just moves to each point in the path, and each time the node reaches a point, we increment the index to move to the next point. I also added a few variables for travel speed, rate (to make the motion more smooth or static) and whether or not the node should repeat the path. You could further expand upon this solution to better meet the needs of your game. I would definitely consider subclassing a node and building this behavior into it so you can re-use this motion for multiple nodes.

One final note, you may notice the calculation for the impulse varies between my solution below and the answer I linked to above. This is because I am avoiding using angle calculation because they are very expensive. Instead I am calculating a normal so that the calculation is more computationally efficient.

One final note, my answer here explains the use of the rate factor to smooth the motion and leave room for motion distortions.

import SpriteKit

class GameScene: SKScene {
var node: SKShapeNode! //The node.
let path: [CGPoint] = [CGPoint(x: 100, y: 100),CGPoint(x: 100, y: 300),CGPoint(x: 300, y: 300),CGPoint(x: 300, y: 100)] //The path of points to travel.
let repeats: Bool = true //Whether to repeat the path.
var pathIndex = 0 //The index of the current point to travel.
let pointRadius: CGFloat = 10 //How close the node must be to reach the destination point.
let travelSpeed: CGFloat = 200 //Speed the node will travel at.
let rate: CGFloat = 0.5 //Motion smoothing.

override func didMoveToView(view: SKView) {
node = SKShapeNode(circleOfRadius: 10)
node.physicsBody = SKPhysicsBody(circleOfRadius: 10)
node.physicsBody!.affectedByGravity = false
self.addChild(node)
}

final func didReachPoint() {
//We reached a point!
pathIndex++

if pathIndex >= path.count && repeats {
pathIndex = 0
}
}

override func update(currentTime: NSTimeInterval) {
if pathIndex >= 0 && pathIndex < path.count {
let destination = path[pathIndex]
let displacement = CGVector(dx: destination.x-node.position.x, dy: destination.y-node.position.y)
let radius = sqrt(displacement.dx*displacement.dx+displacement.dy*displacement.dy)
let normal = CGVector(dx: displacement.dx/radius, dy: displacement.dy/radius)
let impulse = CGVector(dx: normal.dx*travelSpeed, dy: normal.dy*travelSpeed)
let relativeVelocity = CGVector(dx:impulse.dx-node.physicsBody!.velocity.dx, dy:impulse.dy-node.physicsBody!.velocity.dy);
node.physicsBody!.velocity=CGVectorMake(node.physicsBody!.velocity.dx+relativeVelocity.dx*rate, node.physicsBody!.velocity.dy+relativeVelocity.dy*rate);
if radius < pointRadius {
didReachPoint()
}
}
}
}

I did this pretty quickly so I apologize if there is a mistake. I don't have time now but I will add a gif showing the solution later.


A note about collisions

To fix the erratic movement during a collision, after the 2 bodies collide set the "rate" property to 0 or preferably a very low number to reduce the travel velocity impulse which will give you more room for motion distortion. Then at some point in the future (maybe some time after the collision occurs or preferably when the body is moving slow again) set the rate back to its initial value. If you really want a nice effect, you can actually ramp up the rate value over time from 0 to the initial value to give yourself a smooth and gradual acceleration.

How to drag and release an object with velocity

Dragging and releasing a sprite with velocity can be accomplished by

First, a struct to store touch data

struct TouchInfo {
var location:CGPoint
var time:NSTimeInterval
}

Declare SKScene subclass properties

var selectedNode:SKSpriteNode?
var history:[TouchInfo]?

In touchesBegan

  1. Save the sprite that the user touched
  2. Save touch event data

Swift code:

let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self)
let node = self.nodeAtPoint(location)
if (node.name == "player") {
// Step 1
selectedNode = node as? SKSpriteNode;
// Stop the sprite
selectedNode?.physicsBody?.velocity = CGVectorMake(0,0)
// Step 2: save information about the touch
history = [TouchInfo(location:location, time:touch.timestamp)]
}

In touchesMoved

  1. If a sprite was selected in touchesBegan, move the sprite to the new location
  2. Save the touch event data

Swift code:

let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self)
if (selectedNode != nil) {
// Step 1. update sprite's position
selectedNode?.position = location
// Step 2. save touch data at index 0
history?.insert(TouchInfo(location:location, time:touch.timestamp),atIndex:0)
}

In touchesEnded

  1. Calculate the differences in x and y from the last touch and the current touch
  2. Find the time difference between touch events
  3. Calculate and keep a sum of the velocity components in x and y
  4. Create a velocity vector and apply it to the sprite
  5. Unselect the node

Swift code:

if let history = history, history.count > 1 && selectedNode != nil {
var vx:CGFloat = 0.0
var vy:CGFloat = 0.0
var previousTouchInfo:TouchInfo?
// Adjust this value as needed
let maxIterations = 3
var numElts:Int = min(history.count, maxIterations)
// Loop over touch history
for index in 0..<numElts {
let touchInfo = history[index]
let location = touchInfo.location
if let previousTouch = previousTouchInfo {
// Step 1
let dx = location.x - previousTouch.location.x
let dy = location.y - previousTouch.location.y
// Step 2
let dt = CGFloat(touchInfo.time - previousTouch.time)
// Step 3
vx += dx / dt
vy += dy / dt
}
previousTouchInfo = touchInfo
}
let count = CGFloat(numElts-1)
// Step 4
let velocity = CGVectorMake(vx/count,vy/count)
selectedNode?.physicsBody?.velocity = velocity
}
// Step 5
selectedNode = nil
history = nil

How to get direction of a PhysicsBody SKNode in spritekit

Try this:

 yourPhysicsBody.velocity

This should return the velocity CGVector of the physics body at any given time.



Related Topics



Leave a reply



Submit