Making a "Joy stick" swift - ios

What I have been trying to do is create a "Joy stick" that moves a player around. Here is what I have so far:
import UIKit
import SpriteKit
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var isTracking = false
var firstTrackingLocation = CGPoint.zero
var trackingVelocity = CGPoint.zero
var trackingDistance : CGFloat = 0.0
var previousTime : NSTimeInterval = 0.0
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/level.scn")!
let scnView = self.view as! SCNView
scnView.delegate = self
scnView.scene = scene
scnView.showsStatistics = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking == false {
for touch in touches {
isTracking = true
let location = touch.locationInView(self.view)
firstTrackingLocation = location
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking {
trackingVelocity = touches.first!.locationInView(self.view)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
isTracking = false
trackingVelocity = CGPoint.zero
}
func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
if isTracking == true {
let scnView = self.view as! SCNView
let character = scnView.scene!.rootNode.childNodeWithName("person", recursively: true)
let deltaTime = time - previousTime
let pointsPerSecond: CGFloat = 1.0 * CGFloat(deltaTime)
var xResult:CGFloat = 0.0
var yResult:CGFloat = 0.0
let point = firstTrackingLocation
let endPoint = trackingVelocity
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
if direction.x > direction.y {
let movePerSecond = pointsPerSecond/direction.x
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
} else {
let movePerSecond = pointsPerSecond/direction.y
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
}
character!.position = SCNVector3(CGFloat(character!.position.x) + (xResult), CGFloat(character!.position.y), CGFloat(character!.position.z) + (yResult))
let camera = scnView.scene?.rootNode.childNodeWithName("camera", recursively: true)
camera?.position = SCNVector3(CGFloat(camera!.position.x) + (xResult), CGFloat(camera!.position.y), CGFloat(camera!.position.z) + (yResult))
}
previousTime = time
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Landscape
}
}
Now this works except if you drag your finger to the other side of the phone the character moves 10 times faster then it would if you barely moved your finger. So what I would like to have, is a Joy stick that moves the character the same speed if you drag a little bit or to the other side of the screen. And I would also like if you changed direction at the other side of the screen that the character would move the other way. So, my guess is that there needs to be a lastPoint saved then when the touchesMoved gets called that somehow we calculate a direction from lastPoint to the currentPoint and then move the character in renderer. I understand that most of this code is probably rubbish, but thanks in advance.

Your joystick should be a value from 0 to 1, you need to determine the radius of your joystick, then calculate distance of (point touched to center of control) and the angle of the control with arc tan.
Now we need to ensure we never go past maxRadius, so if our distance is > maxRadius, we just set it to max radius, then we divide this value by our maxRadius to get out distance ratio.
Then we just take the cos and sin of our angle, and multiply it by our distance ratio, and get the x and y ratio values. (Should be between 0 and 1)
Finally, take this x and y value, and multiply it to the speed at which your object should be moving at.
let maxRadius = 100 //Your allowable radius
let xDist = (p2.x - p1.x)
let yDist = (p2.y - p1.y)
let distance = (sqrt((xDist * xDist) + (yDist * yDist))
let angle = atan2(yDist , xDist )
let controlDistanceRatio = (distance > maxRadius) ? 1 : distance / maxRadius
let controllerX = cos(angle) * controlDistanceRatio
let controllerY = sin(angle) * controlDistanceRatio

This seems like a good case to use a custom UIGestureRecognizer. See Apple API Reference.
In this particular case you would create a continuous gesture. The resultant CGVector would be calculated from the center (origin) point of your joystick on screen. The recognizer would fail if the joystick node isn't selected and end if unselected (deselected). The resultant CGVector will be updated while the gesture's state is moved.
Now the tricky part to figure out would be moving the node image in such a way that allows the user to have the feeling of a joystick. For this you may need to update the node texture and make slight adjustments to the node position to give the appearance of moving a node around.
See if this helps you: Single Rotation Gesture Recognizer
Let me know if this points you in the right direction.

Your stated problem is the character moves at a variable rate depending on the how far from the start point the user drags their finger. The crucial point in the code seems to be this
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
The difference between endPoint and point is variable and so you are getting a variable magnitude in your direction. To simplify, you could just put in a constant value like
let direction = CGPoint(x: 10, y: 10)
That gets the character moving at a constant speed when the user presses the joystick, but the character is always moving the same direction.
So somehow you've got to bracket in the variable directional values. The first thing that comes to mind is using min and max
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
direction.x = min(direction.x, 10)
direction.x = max(direction.x, -10)
direction.y = min(direction.y, 10)
direction.y = max(direction.y, -10)
That seems like it would keep the magnitude of the direction values between -10 and 10. That doesn't make the speed completely constant, and it allows faster travel along diagonal lines than travel parallel to the x or y axis, but maybe it is closer to what you want.

Related

How do I change the texture of my node when Im moving the joystick to the left or right?

I have this node that is controlled by a joystick and when I move the joystick to the left I would like to change the image of the node and the same thing when I move the joystick to the right. How would I do that? Here is the code I have:
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
if stickActive == true {
//EDIT Code to change texture when moving joystick to left or right.
if joyStick.position.x < base.position.x {
plane.texture = SKTexture(imageNamed: "planeleft")
} else {
plane.texture = SKTexture(imageNamed: "planeright")
}
plane.removeActionForKey("stopaction")
plane.physicsBody?.affectedByGravity = false
xJoystickDelta = location.x - base.position.x
yJoystickDelta = location.y - base.position.y
let v = CGVector(dx: location.x - base.position.x, dy: location.y - base.position.x)
let angle = atan2(v.dy, v.dx)
let length: CGFloat = base.frame.size.height / 2
let xDist: CGFloat = sin(angle - 1.57079633) * length
let yDist: CGFloat = cos(angle - 1.57079633) * length
if (CGRectContainsPoint(base.frame, location)) {
joyStick.position = location
}else {
base.alpha = 0.5 //sets the opacity to 0.5 when the joystick is touched
joyStick.position = CGPointMake(base.position.x - xDist, base.position.y + yDist)
}
}
}
}
override func update(currentTime: CFTimeInterval) {
let xScale = 0.08
let yScale = 0.08
let xAdd = self.xJoystickDelta * CGFloat(xScale)
let yAdd = self.yJoystickDelta * CGFloat(yScale)
self.plane.position.x += xAdd
self.plane.position.y += yAdd
}
I'm not familiar with this, but I know the logic you should use for this.
Complicated:
To detect if the joystick is on the left, you should use a range of the furthest left the joystick can go to the centre of the joystick when it's in the middle. The right side would be from the centre to the furthest right it can go.
Simple:
For the left, make it when the X value if the joystick is less than the default centre position. The right would be an X position greater than the middle.
These would be if statements inside the update function so it's constantly checking for this. In the statements would be something like this:
plane.texture = SKTexture(imageNamed: "TextureName")
Note: This all assumes that the joystick is in a fixed position and has a set centre
Hope this helps!

Detect the degrees in a circle with users touch

Im looking to create a swift function to return the degrees (Float) of the users touch on an image (SKSpritenode). In touchesBegan, I know how to detect the x & y positions of my image. The idea is to create a function that takes in these positions and returns the degrees.
Amended - The following code now works:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.anchorPoint = CGPointMake(0.5, 0.5)
myNode.position = CGPointMake(0, -myNode.frame.height / 2)
self.addChild(myNode)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
if myNode.containsPoint(location) {
print("tapped!")
let origin = myNode.position
let touch = touch.locationInNode(myNode.parent!)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
print("degrees = \(degrees)")
}
}
}
You need to compare the user's touch position to an origin point, which might be the centre of your sprite node for example. Here's some code to get you started:
let origin = CGPoint(x: 0, y: 0)
let touch = CGPoint(x: 100, y: 100)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
That last value – degrees – is the one you want if you want to show users information. If you want to do more calculations, you should probably stick with radians.

Balancing a sprite

So my problem regards the rotation of a sprite. I want it look like it's balancing. When you tap left - the sprite moves left, when you tap right - the sprite moves right. To get a better idea, put a pencil on it's end and try balance it... Yeah... that (but only in x axis).
Currently I am rotating a sprite either clockwise or anti-clockwise dependent upon whether I tap the left or right side of the screen. This is all done using SKActions.
The problem with this is that it results in a very 'jerky' and not particularly realistic motion.
I assume that I want to use physics body and something similar to velocity but my questions are:
the use of velocity is right... isn't it?
would i better off with a volume based sprite (and get it to take into account it's volume and mass) or just a simple edge based sprite?
Thanks in advance!
-- Code -
This is how I'm currently rotating and moving my sprite:
import SpriteKit
enum rotationDirection{
case clockwise
case counterClockwise
case none
}
// Creates GameScene and initialises Sprites in Scene //
class GameScene: SKScene, SKPhysicsContactDelegate {
// Rotation direction variable for ship motion (rotation and movement) //
var currentRotationDirection = rotationDirection.none
var xVelocity: CGFloat = 0
var sprite = SKSpriteNode()
// Setup Scene here //
override func didMoveToView(view: SKView) {
// Background colour //
self.backgroundColor = SKColor.whiteColor()
// sprite Physics + add's sprite //
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: ship.size)
sprite.physicsBody?.affectedByGravity = true
sprite.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
self.addSprite()
// Initialises sprite node and it's properties //
func addSprite() {
// Sprite dimension properities //
sprite.name = "sprite"
sprite = SKSpriteNode(imageNamed: "sprite")
sprite.setScale(0.5)
sprite.position = CGPointMake(frame.midX, 220)
sprite.anchorPoint = CGPoint(x: 0.5, y: 0.25)
sprite.zPosition = 1;
// sprite Physics properties //
sprite.physicsBody = SKPhysicsBody(rectangleOfSize: ship.size)
sprite.physicsBody?.categoryBitMask = UInt32(shipCategory)
sprite.physicsBody?.dynamic = true
sprite.physicsBody?.contactTestBitMask = UInt32(obstacleCategory)
sprite.physicsBody?.collisionBitMask = 0
self.addChild(sprite)
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
// Defines UI Touch //
let touch = touches.first as UITouch!
let touchPosition = touch.locationInNode(self)
// Sets up Inital Rotation Direction //
let newRotationDirection : rotationDirection = touchPosition.x < CGRectGetMidX(self.frame) ? .clockwise : .counterClockwise
// Left or Right movement based on touch //
if touchPosition.x < CGRectGetMidX(self.frame) {xVelocity = -75}
else {xVelocity = 75}
// Clockwise or anticlockwise rotation based on touch //
if currentRotationDirection != newRotationDirection && currentRotationDirection != .none {
reverseRotation()
currentRotationDirection = newRotationDirection
}
else if (currentRotationDirection == .none) {
setupRotationWith(direction: newRotationDirection)
currentRotationDirection = newRotationDirection
}
}
func reverseRotation() {
let oldRotateAction = sprite.actionForKey("rotate")
let newRotateAction = SKAction.reversedAction(oldRotateAction!)
sprite.runAction(newRotateAction(), withKey: "rotate")
}
func stopRotation() {
sprite.removeActionForKey("rotate")
}
func setupRotationWith(direction direction: rotationDirection){
let angle : CGFloat = (direction == .clockwise) ? CGFloat(M_PI) : -CGFloat(M_PI)
let rotate = SKAction.rotateByAngle(angle, duration: 2)
let repeatAction = SKAction.repeatActionForever(rotate)
sprite.runAction(repeatAction, withKey: "rotate")
}
override func update(currentTime: CFTimeInterval) {
let rate: CGFloat = 0.3; //Controls rate of motion. 1.0 instantaneous, 0.0 none.
let relativeVelocity: CGVector = CGVector(dx:xVelocity-sprite.physicsBody!.velocity.dx, dy:0);
sprite.physicsBody!.velocity=CGVector(dx:sprite.physicsBody!.velocity.dx+relativeVelocity.dx*rate, dy:0);
}
I think what your are going to want to look into is acceleration. Right now you are adjusting verlocityX with a static amount using:
if touchPosition.x < CGRectGetMidX(self.frame) {
xVelocity = -75
}
else {
xVelocity = 75
}
Doing this will result in your jerky motion. Which is actually just linear. Using acceleration you can avoid this.
For this you'll need two more variables for maxVelocity and acceleration. You want a maxVelocity probably to limit the velocity. Each frame you'll need to increase the velocity with the acceleration amount.
I suggest you try something like this:
velocity = 0 // No movement
acceleration = 2 // Increase velocity with 2 each frame
maxVelocity = 75 // The maximum velocity
if touchPosition.x < CGRectGetMidX(self.frame) {
xVelocity += acceleration
if xVelocity <= -maxVelocity {
xVelocity = -maxVelocity
}
}
else {
xVelocity -= acceleration
if xVelocity >= maxSpeed {
xVelocity = maxSpeed
}
}
Increasing your velocity like this will result with a rotation speed of 2 in the first frame, than 4 in the second and 6 in the thirds etc. So it will have an increasing effect. While at the same time, when trying to reverse it you wont have a jerky effect either. Say the velocity is at 50, you first decrease it to 48, 46, 44, etc. Not until the 0 point you will actually start rotating the other way.

(SpriteKit + Swift 2) - Remove sprite when user tapped on it

I am trying to make a simple bubble tapped game. I have a created a SKSpriteKid node in my code. The problem is I want to sprite to disappear when user tap on it. I can't seem to removeparent() the node. How do I tackle this?
I have created a sprite with bubble1 as the name.
func bubblePressed(bubble1: SKSpriteNode) {
print("Tapped")
bubble1.removeFromParent()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let touchLocation = touch.locationInNode(self)
let touchedNode = self.nodeAtPoint(touchLocation)
if (touchedNode.name == "bubble1") {
touchedNode.removeFromParent()
print("hit")
}
}
}
//the node's creation.
func bubblesinView() {
//create the sprite
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
//spawn in the X axis min is size, max is the total height minus size bubble
let width_bubble_1 = random(min: bubble1.size.width/2, max: size.width - bubble1.size.height/2)
//y: size.height * X, X is 0 at bottom page to max
//spawn position, let x be random
bubble1.position = CGPoint(x: width_bubble_1, y: size.height)
// Add the monster to the scene
addChild(bubble1)
// Determine speed of the monster from start to end
let bubblespeed = random(min: CGFloat(1.1), max: CGFloat(4.0))
//this tell the bubble to move to the given position, changeble x must be a random value
let moveAction = SKAction.moveTo(CGPoint(x: width_bubble_1, y: size.height * 0.1), duration: NSTimeInterval(bubblespeed))
let actionMoveDone = SKAction.removeFromParent()
bubble1.runAction(SKAction.sequence([moveAction, actionMoveDone]))
}
I am really trying to make this work. Thanks in advance!
If you want to use the name property of the node, you should set it to something. The runtime cannot figure out the variable name as the name. So in bubblesInView function after
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
you should do
bubble1.name = "bubble1"
You just forgot to name the node :
//create the sprite
let bubble1 = SKSpriteNode(imageNamed: "bubble_green.png")
bubble1.name = "bubble1"

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

I've been wracking my brain for a couple of days trying to come up with a way to move a player from there current position to new position using Swift and SpriteKit. Sounds relatively easy.
Now, I know I can use a CGPath and a SKAction to move the player along a path, but what I need to know is how to create the path for the player to move along.
I need the player to move through a predetermined radius as it turns towards the new point first as it moves, let me demonstrate...
So, the red circle is the player and their current orientation, the large circle is the turn radius and the red crosses are possible points where the player wants to move to (obviously you'd only have one at any point in time, but the idea is demonstrate the difference in movement between one possible point and another)
Also, the player could move left or right depending in which ever path is shortest to the target point.
What I tried (sorry the list is kind of short)...
Basically, I know the current position/orientation of the player; I know the radius of the turn circle and I know the point I want to move to. I need to calculate the arc through which the player will need to initially move through to orientate themselves to the new point (tacking on a CGPathAddLineToPoint to the end of the arc should be trivial)
Other then spending copious amounts of time reading through the docs, Googling, reading blog posts and tutorials, I also tried looping through a series of angles from a start angle through a given iteration level (+/-0.5 degrees for example) and calculating the angle between the current point and next point on the circle and comparing that to the angle of the current point to the target point and basically selecting the angle with the lowest difference/delta ...
So, the two red circles represent two points on the circle, the blue line represents the angle between them, the green line represents the angle from the first point to the target point.
Let's just say, that while that might possibly work, I'm kind of horrified at the idea and hope that it might be possible to come up with a better/faster solution.
I'm not sure if something like CGPathAddArcToPoint would help, as it would create an arc from my players current position to the target point, rather then allow the player to move through a turning circle.
Once the player leaves the turning circle, I'm not particular fussed if the move in a straight line or not (ie they could curve slightly to the target point), but I'm currently focused on trying to calculate the required arc needed to get the player started.
Sorry, my maths is pretty poor, so, please, be nice
The code "currently" looks something like (a complete mess)
func pointTowards(point thePoint: CGPoint) {
// Need to calculate the direction of the turn
//let angle = atan2(thePoint.y - self.position.y, thePoint.x - self.position.x) - CGFloat(180.0.toRadians());
let angle = angleBetween(startPoint: self.position, endPoint: thePoint) - CGFloat(180.0.toRadians())
if (self.zRotation < 0) {
// self.zRotation
// self.zRotation = self.zRotation + M_PI * 2;
}
let rotateTo: SKAction = SKAction.rotateToAngle(angle, duration: 1, shortestUnitArc: true)
rotateTo.timingMode = SKActionTimingMode.EaseInEaseOut
self.runAction(rotateTo)
let offset = CGPoint(x: rotorBlur.position.x, y: rotorBlur.position.y + (rotorBlur.size.width / 2))
let radius = rotorBlur.size.width / 2.0
var points: [AnglesAndPoints] = self.pointsOnCircleOf(
radius: radius,
offset: offset);
let centerPoint = CGPoint(x: offset.x + radius, y: offset.y + radius)
var minAngle = CGFloat.max
var minDelta = CGFloat.max
for var p: Int = 1; p < points.count; p++ {
let p1 = points[p - 1].point
let p2 = points[p].point
let point = angleBetween(startPoint: p1, endPoint: p2) - CGFloat(180.0.toRadians())
let target = angleBetween(startPoint: p1, endPoint: thePoint) - CGFloat(180.0.toRadians())
let delta = target - point
if delta < minDelta {
minDelta = delta
minAngle = points[p - 1].angle
}
}
println("projected: \(minAngle); delta = \(minDelta)")
if let pathNode = pathNode {
pathNode.removeFromParent()
}
//points = self.pointsOnCircleOf(
// radius: rotorBlur.size.width / 2.0,
// offset: CGPoint(x: 0, y: rotorBlur.size.width / 2));
let path = CGPathCreateMutable()
CGPathAddArc(
path,
nil,
0,
rotorBlur.size.width / 2,
rotorBlur.size.width / 2,
CGFloat(-180.0.toRadians()),
minAngle,
true)
pathNode = SKShapeNode()
pathNode?.path = path
pathNode?.lineWidth = 1.0
pathNode?.strokeColor = .lightGrayColor()
addChild(pathNode!)
}
func pointsOnCircleOf(radius r : CGFloat, offset os: CGPoint) -> [AnglesAndPoints] {
var points: [AnglesAndPoints] = []
let numPoints = 360.0 * 2.0
let delta = 360.0 / numPoints
for var degrees: Double = 0; degrees < numPoints; degrees += delta {
var point: CGPoint = pointOnCircle(angle: CGFloat(degrees.toRadians()), radius: r)
point = CGPoint(x: point.x + os.x, y: point.y + os.y)
points.append(AnglesAndPoints(angle: CGFloat(degrees.toRadians()), point: point))
}
return points
}
func pointOnCircle(angle radians:CGFloat, radius theRadius:CGFloat) -> CGPoint {
return CGPointMake((cos(radians) * theRadius),
(sin(radians) * theRadius));
}
func angleBetween(startPoint p1: CGPoint, endPoint p2: CGPoint) -> CGFloat {
return atan2(p2.y - p1.y, p2.x - p1.x) //- CGFloat(180.0.toRadians());
}
Basically, I went about pre-calculating the points on a circle of a given radius with a given offset, which is just horrible and if I had the time right now, would re-work it so that the point was dynamically created (or I could cache the values some how and simply translate them), but as I said, this was such a horrible idea I really wanted to find a different way and abandon this approach
I'm pretty sure that the current code doesn't take into the players current orientation and it should be supplying a start angle and direction (counter/clockwise) in which to iterate, but I've gotten to the point I'd like to see if their is simply a better solution then this before trying to fix any more issues with it
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
}
}
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
}
}

Resources