Create a path to move the player from the current position to a new position using the "circle rotation",

I’ve been trying to wave my brain for a couple of days, trying to come up with a way to move the player from the current position to a new position using Swift and SpriteKit. Sounds relatively easy.

Now I know that I can use CGPath and SKAction to move the player along the path, but I need to know how to create a path to move the player.

I need the player to move through a given radius, when he first turns to a new point, when he moves, let me demonstrate ...

Turn circle

So, the red circle is the player and their current orientation, the big circle is the turning radius, and the red crosses are the possible points that the player wants to go to (obviously, you would only have one at any time, but the idea shows the difference in movement between one possible point and another)

In addition, the player can move left or right, depending on which path is always the shortest for the target point.

What I tried (sorry, the list is short) ...

Basically, I know the current position / orientation of the player; I know the radius of the turning circle, and I know what I want to move towards. I need to calculate the arc through which the player will first have to move in order to navigate to a new point (the snap on CGPathAddLineToPoint to the end of the arc should be trivial)

In addition, spending a lot of time browsing documents, Google, reading blog posts and tutorials, I also tried a cycle through a series of angles from the start angle to a given iteration level (for example, +/- 0.5 degrees) and calculating the angle between the current point and the next point on the circle and comparing it with the angle of the current point to the target point and basically choosing the angle with the smallest difference / delta ...

Delta angles

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 say that although this might work, I’m kind of terrified of this idea and hope that it will be possible to find a better / quick solution.

I'm not sure if something like CGPathAddArcToPoint will help, as this will create an arc from the current position of the players to the target point, and then allow the player to move around the rotation circle.

As soon as the player leaves the turn of the circle, I don’t feel much fuss if the movement is in a straight line or not (i.e. they can tilt slightly towards the target point), but currently I am focusing on trying to calculate what is required to start the game.

Sorry, my math is pretty poor, so please be nice.

The code "now" looks something like this (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-computing points on a circle of a given radius with a given offset, which is just awful, and if I had the time right now, he would rework it so that the point was dynamically created (or I could cache the values ​​with which this way and just translate them), but, as I said, it was such a terrible idea that I really wanted to find another way and abandon this approach.

I'm pretty sure that the current code doesn’t take players into the current orientation, and it should provide the starting angle and direction (counter / clockwise) that you need to iterate over, but I got to the point I'd like to see if they just the best solution is to try to fix more problems with it.

+5
source share
1 answer

Funny, I actually have movement in my game almost the way you described, except that instead of always going clockwise when on the right side and counterclockwise when on the left, it will choose more close way.

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

However, my implementation does not use SKActions and paths to move. Everything happens dynamically in real time, which allows you to make collisions with moving objects and improve motion control. However, if you absolutely must use the paths with SKActions, let me know and I will try to come up with a solution. Essentially, it comes down to finding the arc at tangent points (which the code already does to some extent).

Physical calculations come from my two respondents here and here .

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

Below is the code for 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 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 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 } } 


enter image description here

Note that the circle you see is another node that I added to the scene to make the movement more noticeable; You can easily remove it. When debugging, you may also find it useful to add nodes to tangent points. The tangentPair tuple inside the calculateMaxAngularDistanceOfBestTangent function contains two tangent points.

Also, note that find the tangent points / corners of the road, but this only happens every time you need to move to a new point. If, however, the game requires a constant transition to a new point, using this algorithm multiple times on many nodes can be expensive (always profile before taking this, though). Another way to check when to move from a centripetal motion to a linear motion is to check if the velocity vector is approaching its final position, as shown below. This is less accurate, but allows you to completely remove the calculateMaxAngularDistanceOfBestTangent function.

 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 } 


Finally, let me know if you need to use paths with SKActions, regardless of whether I think I will update this last part, show how it is done (if someone does not defeat me! And, as I mentioned earlier, the code I posted does this to a certain extent.) I don’t have the time right now, but I hope I get a chance soon! Hope something mentioned in this answer helps you. Good luck with your game.

Update including SKActions

The code below shows the exact same effect, except for this time, using SKActions to animate CGPath to the tangential angle and then to the final destination. This is much simpler since there is no longer any manual calculation of centripetal and linear motion, however, since this is an animation, you lose the dynamic control of the motion in real time, which provides the above solution.

 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 } } 
+3
source

All Articles