Calculating Angle Between Two Points on Edge of Circle Swift Spritekit

calculating angle between two points on edge of circle Swift SpriteKit

Given points p1, p2 on a circle with center center,
you would compute the difference vectors first:

let v1 = CGVector(dx: p1.x - center.x, dy: p1.y - center.y)
let v2 = CGVector(dx: p2.x - center.x, dy: p2.y - center.y)

Then

let angle = atan2(v2.dy, v2.dx) - atan2(v1.dy, v1.dx)

is the (directed) angle between those vectors in radians, and

var deg = angle * CGFloat(180.0 / M_PI)

the angle in degrees. The computed value can be in the range -360 .. 360, so you
might want to normalize it to the range 0 <= deg < 360 with

if deg < 0 { deg += 360.0 }

Get angle from 2 positions

Does this other answer help?

How to map atan2() to degrees 0-360

I've written it like this:

- (CGFloat) pointPairToBearingDegrees:(CGPoint)startingPoint secondPoint:(CGPoint) endingPoint
{
CGPoint originPoint = CGPointMake(endingPoint.x - startingPoint.x, endingPoint.y - startingPoint.y); // get origin point to origin by subtracting end from start
float bearingRadians = atan2f(originPoint.y, originPoint.x); // get bearing in radians
float bearingDegrees = bearingRadians * (180.0 / M_PI); // convert to degrees
bearingDegrees = (bearingDegrees > 0.0 ? bearingDegrees : (360.0 + bearingDegrees)); // correct discontinuity
return bearingDegrees;
}

Running the code:

CGPoint p1 = CGPointMake(10, 10);
CGPoint p2 = CGPointMake(20,20);

CGFloat f = [self pointPairToBearingDegrees:p1 secondPoint:p2];

And this returns 45.

Hope this helps.

How to calculate third point of a triangle from two points, angle and edge length?

Simple : use the law of Sines

You have two known angles and a known edge. The unknown angle is presumably opposite the known edge. Using the property that the internal angles of a triangle always sum to 180 degrees, you can calculate the unknown angle. Then, sine(angle)/known edge = sine(known angle)/unknown edge. Solve for both unknown edges and then solve for your point.

Create path to move player from current position to new position through a turn circle

Funny, I actually have motion in my game almost exactly as you described except that instead of always going clock-wise when on the right side and counter-clock when on the left, it will pick the closer path.

So I grabbed some of the code and modified it sightly to fit your description. It will move left when the target point is to the left of the player, else it will move right. You can also set the speed of the node, as well as the radius and position of the "orbit."

My implementation however does not use SKActions and paths to move. Everything is done dynamically in real-time which allows for collisions with the moving objects and greater motion control. However if you absolutely need to use paths with SKActions let me know and I'll try to come up with a solution. Essentially what it comes down to is finding the arc to the tangent points (which the code already does to an extent).

The physics calculations come from my two answerers here, and here.

The way the implementation works is that it first determines the final destination point, as well as the angular distance to the best tangent point using a secondary circle to find the tangent points. Then using centripetal motion, the node moves along the path to the tangent point and then switches to linear motion to finish moving to the end destination.

Below is the code for the GameScene:

import SpriteKit

enum MotionState { case None, Linear, Centripetal }

class GameScene: SKScene {
var node: SKShapeNode!
var circle: SKShapeNode!
var angularDistance: CGFloat = 0
var maxAngularDistance: CGFloat = 0
let dt: CGFloat = 1.0/60.0 //Delta Time
var centripetalPoint = CGPoint() //Point to orbit.
let centripetalRadius: CGFloat = 60 //Radius of orbit.
var motionState: MotionState = .None
var invert: CGFloat = 1
var travelPoint: CGPoint = CGPoint() //The point to travel to.
let travelSpeed:CGFloat = 200 //The speed at which to travel.

override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
circle = SKShapeNode(circleOfRadius: centripetalRadius)
circle.strokeColor = SKColor.redColor()
circle.hidden = true
self.addChild(circle)
}
func moveToPoint(point: CGPoint) {
travelPoint = point
motionState = .Centripetal

//Assume clockwise when point is to the right. Else counter-clockwise
if point.x > node.position.x {
invert = -1
//Assume orbit point is always one x radius right from node's position.
centripetalPoint = CGPoint(x: node.position.x + centripetalRadius, y: node.position.y)
angularDistance = CGFloat(M_PI)
} else {
invert = 1
//Assume orbit point is always one x radius left from node's position.
centripetalPoint = CGPoint(x: node.position.x - centripetalRadius, y: node.position.y)
angularDistance = 0
}
}

final func calculateCentripetalVelocity() {
let normal = CGVector(dx:centripetalPoint.x + CGFloat(cos(self.angularDistance))*centripetalRadius,dy:centripetalPoint.y + CGFloat(sin(self.angularDistance))*centripetalRadius);
let period = (CGFloat(M_PI)*2.0)*centripetalRadius/(travelSpeed*invert)
self.angularDistance += (CGFloat(M_PI)*2.0)/period*dt;
if (self.angularDistance>CGFloat(M_PI)*2)
{
self.angularDistance = 0
}
if (self.angularDistance < 0) {
self.angularDistance = CGFloat(M_PI)*2
}
node.physicsBody!.velocity = CGVector(dx:(normal.dx-node.position.x)/dt ,dy:(normal.dy-node.position.y)/dt)

//Here we check if we are at the tangent angle. Assume 4 degree threshold for error.
if abs(maxAngularDistance-angularDistance) < CGFloat(4*M_PI/180) {
motionState = .Linear
}
}

final func calculateLinearVelocity() {
let disp = CGVector(dx: travelPoint.x-node.position.x, dy: travelPoint.y-node.position.y)
let angle = atan2(disp.dy, disp.dx)
node.physicsBody!.velocity = CGVector(dx: cos(angle)*travelSpeed, dy: sin(angle)*travelSpeed)

//Here we check if we are at the travel point. Assume 15 point threshold for error.
if sqrt(disp.dx*disp.dx+disp.dy*disp.dy) < 15 {
//We made it to the final position! Code that happens after reaching the point should go here.
motionState = .None
println("Node finished moving to point!")
}
}

override func update(currentTime: NSTimeInterval) {
if motionState == .Centripetal {
calculateCentripetalVelocity()
} else if motionState == .Linear {
calculateLinearVelocity()
}
}

func calculateMaxAngularDistanceOfBestTangent() {
let disp = CGVector(dx: centripetalPoint.x - travelPoint.x, dy: centripetalPoint.y - travelPoint.y)
let specialCirclePos = CGPoint(x: (travelPoint.x+centripetalPoint.x)/2.0, y: (travelPoint.y+centripetalPoint.y)/2.0)
let specialCircleRadius = sqrt(disp.dx*disp.dx+disp.dy*disp.dy)/2.0
let tangentPair = getPairPointsFromCircleOnCircle(centripetalPoint, radiusA: centripetalRadius, pointB: specialCirclePos, radiusB: specialCircleRadius)
let tangentAngle1 = (atan2(tangentPair.0.y - centripetalPoint.y,tangentPair.0.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
let tangentAngle2 = (atan2(tangentPair.1.y - centripetalPoint.y,tangentPair.1.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
if invert == -1 {
maxAngularDistance = tangentAngle2
} else {
maxAngularDistance = tangentAngle1
}
}

//Not mine, modified algorithm from https://stackoverflow.com/q/3349125/2158465
func getPairPointsFromCircleOnCircle(pointA: CGPoint, radiusA: CGFloat, pointB: CGPoint, radiusB: CGFloat) -> (CGPoint,CGPoint) {
let dX = (pointA.x - pointB.x)*(pointA.x - pointB.x)
let dY = (pointA.y - pointB.y)*(pointA.y - pointB.y)
let d = sqrt(dX+dY)
let a = (radiusA*radiusA - radiusB*radiusB + d*d)/(2.0*d);
let h = sqrt(radiusA*radiusA - a*a);
let pointCSub = CGPoint(x:pointB.x-pointA.x,y:pointB.y-pointA.y)
let pointCScale = CGPoint(x: pointCSub.x*(a/d), y: pointCSub.y*(a/d))
let pointC = CGPoint(x: pointCScale.x+pointA.x, y: pointCScale.y+pointA.y)
let x3 = pointC.x + h*(pointB.y - pointA.y)/d;
let y3 = pointC.y - h*(pointB.x - pointA.x)/d;
let x4 = pointC.x - h*(pointB.y - pointA.y)/d;
let y4 = pointC.y + h*(pointB.x - pointA.x)/d;
return (CGPoint(x:x3, y:y3), CGPoint(x:x4, y:y4));

}

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touchPos = (touches.first! as! UITouch).locationInNode(self)
node = SKShapeNode(circleOfRadius: 10)
node.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
node.physicsBody = SKPhysicsBody(circleOfRadius: 10)
self.addChild(node)

moveToPoint(touchPos)
calculateMaxAngularDistanceOfBestTangent() //Expensive!

circle.hidden = false
circle.position = centripetalPoint
}
}


Sample Image

Note that the circle you see is another node I added to the scene to make the motion more visible; you can easily just remove it. When debugging you might also find it useful to add nodes at the tangent points. The tangentPair tuple inside the calculateMaxAngularDistanceOfBestTangent function contains the two tangent points.

Additionally note that finding the tangent points/angles is expensive but it only happens each time you need to move to a new point. If however you game requires constantly moving to a new point, using this algorithm repeatedly on many nodes can be costly (always profile before assuming this though). Another way to check when to move from centripetal motion to linear motion is to check if the velocity vector is approaching the end position as shown below. This is less accurate but allows you to remove the calculateMaxAngularDistanceOfBestTangent function entirely.

let velAngle = atan2(node.physicsBody!.velocity.dy,node.physicsBody!.velocity.dx)
let disp = CGVector(dx: travelPoint.x-node.position.x, dy: travelPoint.y-node.position.y)
let dispAngle = atan2(disp.dy,disp.dx)

//Here we check if we are at the tangent angle. Assume 4 degree threshold for error.
if velAngle != 0 && abs(velAngle - dispAngle) < CGFloat(4*M_PI/180) {
motionState = .Linear
}


Lastly let me know if you need to use paths with SKActions, regardless I think I will update this last part showing how this is done (unless someone beats me to it! And as I mentioned earlier the code I posted does this to an extent.) I don't have time to right now but hopefully I get a chance to soon! I hope something mentioned in this answer helps you. Good luck with your game.

Update including SKActions

The code below shows getting the same exact effect except this time using SKActions to animate a CGPath to the tangent angle then to the final destination point. It is much simpler as there is no longer a manual calculation of centripetal and linear motion, however because it is an animation you lose the dynamic real-time motion control that the solution above provides.

class GameScene: SKScene {

var centripetalPoint = CGPoint() //Point to orbit.
let centripetalRadius: CGFloat = 60 //Radius of orbit.
var travelPoint: CGPoint = CGPoint() //The point to travel to.
var travelDuration: NSTimeInterval = 1.0 //The duration of action.
var node: SKShapeNode!
var circle: SKShapeNode!

override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
circle = SKShapeNode(circleOfRadius: centripetalRadius)
circle.strokeColor = SKColor.redColor()
circle.hidden = true
self.addChild(circle)
}
//Not mine, modified algorithm from https://stackoverflow.com/q/3349125/2158465
func getPairPointsFromCircleOnCircle(pointA: CGPoint, radiusA: CGFloat, pointB: CGPoint, radiusB: CGFloat) -> (CGPoint,CGPoint) {
let dX = (pointA.x - pointB.x)*(pointA.x - pointB.x)
let dY = (pointA.y - pointB.y)*(pointA.y - pointB.y)
let d = sqrt(dX+dY)
let a = (radiusA*radiusA - radiusB*radiusB + d*d)/(2.0*d);
let h = sqrt(radiusA*radiusA - a*a);
let pointCSub = CGPoint(x:pointB.x-pointA.x,y:pointB.y-pointA.y)
let pointCScale = CGPoint(x: pointCSub.x*(a/d), y: pointCSub.y*(a/d))
let pointC = CGPoint(x: pointCScale.x+pointA.x, y: pointCScale.y+pointA.y)
let x3 = pointC.x + h*(pointB.y - pointA.y)/d;
let y3 = pointC.y - h*(pointB.x - pointA.x)/d;
let x4 = pointC.x - h*(pointB.y - pointA.y)/d;
let y4 = pointC.y + h*(pointB.x - pointA.x)/d;
return (CGPoint(x:x3, y:y3), CGPoint(x:x4, y:y4));
}

func moveToPoint(point: CGPoint) {
travelPoint = point

//Assume clockwise when point is to the right. Else counter-clockwise
if point.x > node.position.x {
centripetalPoint = CGPoint(x: node.position.x + centripetalRadius, y: node.position.y)
} else {
centripetalPoint = CGPoint(x: node.position.x - centripetalRadius, y: node.position.y)
}

let disp = CGVector(dx: centripetalPoint.x - travelPoint.x, dy: centripetalPoint.y - travelPoint.y)
let specialCirclePos = CGPoint(x: (travelPoint.x+centripetalPoint.x)/2.0, y: (travelPoint.y+centripetalPoint.y)/2.0)
let specialCircleRadius = sqrt(disp.dx*disp.dx+disp.dy*disp.dy)/2.0
let tangentPair = getPairPointsFromCircleOnCircle(centripetalPoint, radiusA: centripetalRadius, pointB: specialCirclePos, radiusB: specialCircleRadius)
let tangentAngle1 = (atan2(tangentPair.0.y - centripetalPoint.y,tangentPair.0.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)
let tangentAngle2 = (atan2(tangentPair.1.y - centripetalPoint.y,tangentPair.1.x - centripetalPoint.x)+CGFloat(2*M_PI))%CGFloat(2*M_PI)

let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, node.position.x, node.position.y)
if travelPoint.x > node.position.x {
CGPathAddArc(path, nil, node.position.x+centripetalRadius, node.position.y, centripetalRadius, CGFloat(M_PI), tangentAngle2, true)
} else {
CGPathAddArc(path, nil, node.position.x-centripetalRadius, node.position.y, centripetalRadius, 0, tangentAngle1, false)
}
CGPathAddLineToPoint(path, nil, travelPoint.x, travelPoint.y)

let action = SKAction.followPath(path, asOffset: false, orientToPath: false, duration: travelDuration)
node.runAction(action)
}

override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touchPos = (touches.first! as! UITouch).locationInNode(self)
node = SKShapeNode(circleOfRadius: 10)
node.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
self.addChild(node)

moveToPoint(touchPos)

circle.hidden = false
circle.position = centripetalPoint
}
}

swift shooting objects from circle shape

Have you tried switching isDynamic from "false" to "true"? Also, try adding GCVector(dx: 1.0 * dx, dy: 1.0 * dy)

SpriteKit origination and angle of gesture

There is always another way of doing something--this is programming! But, as Rube Goldberg demonstrates below, another way may not be the best / easiest way:

Sample Image

Using touchesEnded you are making sort of like a 'fling' gesture, which can be somewhat limited in it's capabilities.

Example, If you want the object to continuously gain velocity at some f of x / make small path changes (without necessarily having to lift the finger), you can use touchesMoved. Fill it with whatever calculations / functions you like.

Example: drag a node with your touch:

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
//-Main touch processoereor
for touch in touches {

let TPOINT = touch.locationInNode(self)
my_super_cool_node!.position = TPOINT

// Put math stuff here, etc.

}
}

This gives you MUCH more control over what happens to your sprite from point A to point B.

Here is a good tutorial:

http://www.globalnerdy.com/2016/02/02/writing-ios-games-with-swift-and-sprite-kit-part-2-responding-to-touches/

And Apple's stuff:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIResponder_Class/#//apple_ref/occ/instm/UIResponder/touchesMoved:withEvent:

Unfortunately, the best things in SpriteKit are going to require some clever implementations / math, and can't all be done with the Scene Editor and and nodes.

What you are talking about doing is a fairly routine (math) function, the distance formula, so it's pretty advantageous to make an actual func out of it, so that way you can customize it and not have to manually write a ton of a code for each thing you want to do.
SK makes things super easy already. Doing even the basic hello world with spinny spaceships would have been a thousand lines of code (or more) back in the day with C++ Direct3d / OpenGL (and tons of advanced math).

The SK editor is great, and with a little bit of programming on the backend, you can actually make full games from just the editor.

Draw circle with ten or more control points in swift

One way you could approach this problem

  • create a circle from several curve segments (using addQuadCurve() or addCurve() of UIBezierPath class)
  • addQuadCurve() adds a curve with one control point while addCurve() adds a curve with 2 control points (the video you showed seems using paths with 2 control points, so it would be better using addCurve())
  • Then user needs to be able to move any of start/end and control points of these curves.
  • For each these change, you have to redraw the curves

I have created a sample playground with this idea. In this playground, I have created a red circle (not a perfect circle) by four curves using addQuadCurve(). This circle has 8 points you could use to alter the shape. If you use 4 curves with addCurve(), then you will have 12 points to alter the shape.

Then I changed a single point of the red circle and added the updated shape in green color below the original red circle.

import UIKit
import PlaygroundSupport

let container = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 700))

let view1 = UIView(frame: CGRect(x: 50, y: 50, width: 500, height: 350))

let layer1 = CAShapeLayer()
layer1.fillColor = UIColor.clear.cgColor
layer1.lineWidth = 5
layer1.strokeColor = UIColor.red.cgColor

//create a circle wich has 8 points to change it's shape (4 control points and 4 start/end points of curves)
let originalPath = UIBezierPath()
originalPath.move(to: CGPoint(x: 100, y: 0))
originalPath.addQuadCurve(to: CGPoint(x: 200, y: 100), controlPoint: CGPoint(x: 190, y: 10))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))

//add this path to the layer1
layer1.path = originalPath.cgPath

//suppose user move the CGPoint(x: 200, y: 100) to CGPoint(x: 220, y: 100)
//then we can redraw the 4 curves again

let view2 = UIView(frame: CGRect(x: 50, y: 350, width: 500, height: 350))

let layer2 = CAShapeLayer()
layer2.fillColor = UIColor.clear.cgColor
layer2.lineWidth = 5
layer2.strokeColor = UIColor.green.cgColor

//changedPath is almost same as originalPath except CGPoint(x: 250, y: 100)
let changedPath = UIBezierPath()
changedPath.move(to: CGPoint(x: 100, y: 0))
changedPath.addQuadCurve(to: CGPoint(x: 250, y: 100), controlPoint: CGPoint(x: 190, y: 10)) // <---- user has moved point CGPoint(x: 200, y: 100) to CGPoint(x: 250, y: 100). So add this curve to the new point
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))

//adding changed path to layer2
layer2.path = changedPath.cgPath

view1.layer.addSublayer(layer1)
view2.layer.addSublayer(layer2)

container.addSubview(view1)
container.addSubview(view2)

PlaygroundPage.current.liveView = container


Related Topics



Leave a reply



Submit