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,
- Add an inner arc from the starting angle to the ending angle
- Add a line from the inner arc's ending point to the outer arc's ending point
- Add the outer arc from the ending angle to the starting angle
- 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
- Save the sprite that the user touched
- 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
- If a sprite was selected in touchesBegan, move the sprite to the new location
- 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
- Calculate the differences in x and y from the last touch and the current touch
- Find the time difference between touch events
- Calculate and keep a sum of the velocity components in x and y
- Create a velocity vector and apply it to the sprite
- 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
Swift - Getting Only Alphanumeric Characters from String
Swift: Reflecting Properties of Subclass of Nsmanagedobject
Can My Class Override Protocol Property Type in Swift
Swift Language Statically or Dynamically Dispatched
Why 'Self.Self' Compiles and Run in Swift
Migration from Swift 3 to Swift 4 - Cannot Convert String to Expected String.Element
Generic Method Override Not Working in Swift
Fullscreen for Swift Playgrounds on iPad
How to Trigger Updateuiview of a Uiviewrepresentable
Send Mail with File Attachment
Checking User Location Permission Status on iOS 14
How to Import Googleanalytics Header into a Library Framework
How to Sort Dates in a Dictionary
Xcode Build Perfect Failure -- Copenssl Not Found
Textfield in Swiftui Loses Focus When I Enter a Character
Xcframework Issue, a Library with the Identifier "Ios-Armv7_Arm64" Already Exists