I've been asked to simplify this question, so that's what I'm doing.
I'm struggling in SpriteKit's physic joints (and possibly physic body properties). I tried every single subclass and many configurations but seams like nothing works or I'm doing something wrong.
I'm developing Snake game. User controls head of snake which should move at constant speed ahead and user can turn it clockwise or anticlockwise. All the remaining snake's pieces should follow the head - they should travel exactly the same path that head was some time ago.
I think for this game the Pin joint should be the answer, which anchor point is exactly in the centre between elements.
Unfortunately the result is not perfect. The structure should make the perfect circle, but it doesn't. I'm attaching the code, and gif showing the current effect. Is anyone experience enough to give me any suggestion what properties of physic body and or joints should are apply here for desired effect?
My code:
class GameScene: SKScene {
private var elements = [SKNode]()
override func didMove(to view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
let dummyTurnNode = SKNode()
dummyTurnNode.position = CGPoint(x: size.width / 2 - 50, y: size.height / 2)
let dummyTurnBody = SKPhysicsBody(circleOfRadius: 1)
dummyTurnBody.isDynamic = false
dummyTurnNode.physicsBody = dummyTurnBody
addChild(dummyTurnNode)
for index in 0..<5 {
let element = SKShapeNode(circleOfRadius: 10)
let body = SKPhysicsBody(circleOfRadius: 10)
body.linearDamping = 0
// body.mass = 0
element.physicsBody = body
element.position = CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index))
elements.append(element)
addChild(element)
let label = SKLabelNode(text: "A")
label.fontSize = 10
label.fontName = "Helvetica-Bold"
element.addChild(label)
if index == 0 {
element.fillColor = UIColor.blue()
body.velocity = CGVector(dx: 0, dy: 30)
let dummyTurnJoint = SKPhysicsJointPin.joint(withBodyA: dummyTurnBody, bodyB: body, anchor: dummyTurnNode.position)
physicsWorld.add(dummyTurnJoint)
} else {
body.linearDamping = 1
element.fillColor = UIColor.red()
let previousElement = elements[index - 1]
let connectingJoint = SKPhysicsJointPin.joint(withBodyA: previousElement.physicsBody!, bodyB: body, anchor: CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index) + CGFloat(15)))
physicsWorld.add(connectingJoint)
}
}
}
override func update(_ currentTime: TimeInterval) {
let head = elements.first!.physicsBody!
var velocity = head.velocity
velocity.normalize()
velocity.multiply(30)
head.velocity = velocity
}
}
extension CGVector {
var rwLength: CGFloat {
let xSq = pow(dx, 2)
let ySq = pow(dy, 2)
return sqrt(xSq + ySq)
}
mutating func normalize() {
dx /= rwLength
dy /= rwLength
}
mutating func multiply(_ factor: CGFloat) {
dx *= factor
dy *= factor
}
}
"All the remaining snake's pieces should follow the head - they should travel exactly the same path that head was some time ago."
You should note that with Physics joints you are likely going to have variance no matter what you do. Even if you have it close to perfect you'll have rounding errors under the hood making the path not exact.
If all the tail parts are equal you can also use a different approach, this is something I've done for a comet tail. Basically the idea is that you have an array of tail objects and per-frame move move the last tail-object always to the same position as the head-object. If the head-object has a higher z-position the tail is drawn below it.
If you need to keep your tail in order you could vary the approach by storing an array of head-positions (per-frame path) and then place the tail objects along that path in your per-frame update call to the snake.
See my code below for example:
These are you head-object variables:
var tails = [SKEmitterNode]()
var tailIndex = 0
In your head init function instantiate the tail objects:
for _ in 0...MAX_TAIL_INDEX
{
if let remnant = SKEmitterNode(fileNamed: "FireTail.sks")
{
p.tails.append(remnant)
}
}
Call the below per-frame:
func drawTail()
{
if tails.count > tailIndex
{
tails[tailIndex].resetSimulation()
tails[tailIndex].particleSpeed = velocity() / 4
tails[tailIndex].emissionAngle = zRotation - CGFloat(M_PI_2) // opposite direction
tails[tailIndex].position = position
tailIndex = tailIndex < MAX_TAIL_INDEX ? tailIndex + 1 : 0
}
}
The resulting effect is actually really smooth when you call it from the scene update() function.
Related
I'd like to display two lines when a SKSpriteNode is being bounced around the screen — one of where the node is going towards, and second which is in what direction the node is going to bounce off of other sprite nodes.
Essentially, I'd like to do something like this:
I can create the first line fairly easily using the velocity of the ball, and the second line would be fairly easy as well, but only if I know where the line is going to collide with another SKSpriteNode.
For the first line, I'm creating a SKShapeNode and setting its path to the same line as the velocity of the ball, but I'm not sure how to find the path the ball will take after it bounces off of the nearest obstacle.
In short — how can I find the point at which a moving object will collide with another object, and then find the direction in which it will move after colliding with the object? This isn't an issue if I know what object it's going to collide with, but short of doing some insanely inefficient brute-forcing, I'm not sure how to find what object the ball is going to collide with and where.
I'm looking for something like this:
let line: SKSpriteNode // this is the first line
let scene: SKScene // the scene that contains all of the objects
// this doesn't exist, but I'm looking for something that would do this
let collisionPoint: CGPoint = line.firstCollision(in: scene)
For my purposes, there are no environmental factors on the sprite (no gravity, air resistance, etc.), it's moving at a constant velocity, and doesn't lose any velocity in rebounds, but it would be useful if the answer could include a way of calculating the the nth rebound of a sprite in any environment.
This is the result we are going to build
Ray Casting
The technique used here is called Ray Casting and you can find more details in this similar answer I have already written.
Basically we use the SpriteKit physics engine to determine draw a line and get the first point of intersection between that line and a physics body.
Code
Of course you need to
create an empty Xcode project based on SpriteKit
add a circular sprite we'll call ball
add a physics body to the ball and give it some speed
Here's the full code you'll need
Extensions
Add these 2 extensions to your project
extension CGVector {
init(angle: CGFloat) {
self.init(dx: cos(angle), dy: sin(angle))
}
func normalized() -> CGVector {
let len = length()
return len>0 ? self / len : CGVector.zero
}
func length() -> CGFloat {
return sqrt(dx*dx + dy*dy)
}
static func / (vector: CGVector, scalar: CGFloat) -> CGVector {
return CGVector(dx: vector.dx / scalar, dy: vector.dy / scalar)
}
func bounced(withNormal normal: CGVector) -> CGVector {
let dotProduct = self.normalized() * normal.normalized()
let dx = self.dx - 2 * (dotProduct) * normal.dx
let dy = self.dy - 2 * (dotProduct) * normal.dy
return CGVector(dx: dx, dy: dy)
}
init(from:CGPoint, to:CGPoint) {
self = CGVector(dx: to.x - from.x, dy: to.y - from.y)
}
static func * (left: CGVector, right: CGVector) -> CGFloat {
return (left.dx * right.dx) + (left.dy * right.dy)
}
}
extension CGPoint {
func length() -> CGFloat {
return sqrt(x*x + y*y)
}
func distanceTo(_ point: CGPoint) -> CGFloat {
return (self - point).length()
}
static func -(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
}
Scene
Now replace all the code you have in Scene.swift with this one
class GameScene: SKScene {
lazy var ball: SKSpriteNode = {
return childNode(withName: "ball") as! SKSpriteNode
}()
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
override func update(_ currentTime: TimeInterval) {
self.enumerateChildNodes(withName: "line") { (node, _) in
node.removeFromParent()
}
guard let collision = rayCast(start: ball.position, direction: ball.physicsBody!.velocity.normalized()) else { return }
let line = CGVector(from: ball.position, to: collision.destination)
drawVector(point: ball.position, vector: line, color: .green)
var nextVector = line.normalized().bounced(withNormal: collision.normal.normalized()).normalized()
nextVector.dx *= 200
nextVector.dy *= 200
drawVector(point: collision.destination, vector: nextVector, color: .yellow)
}
private func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)? {
let endVector = CGVector(
dx: start.x + direction.normalized().dx * 4000,
dy: start.y + direction.normalized().dy * 4000
)
let endPoint = CGPoint(x: endVector.dx, y: endVector.dy)
var closestPoint: CGPoint?
var normal: CGVector?
physicsWorld.enumerateBodies(alongRayStart: start, end: endPoint) {
(physicsBody:SKPhysicsBody,
point:CGPoint,
normalVector:CGVector,
stop:UnsafeMutablePointer<ObjCBool>) in
guard start.distanceTo(point) > 1 else {
return
}
guard let newClosestPoint = closestPoint else {
closestPoint = point
normal = normalVector
return
}
guard start.distanceTo(point) < start.distanceTo(newClosestPoint) else {
return
}
normal = normalVector
}
guard let p = closestPoint, let n = normal else { return nil }
return (p, n)
}
private func drawVector(point: CGPoint, vector: CGVector, color: SKColor) {
let start = point
let destX = (start.x + vector.dx)
let destY = (start.y + vector.dy)
let to = CGPoint(x: destX, y: destY)
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: to)
path.closeSubpath()
let line = SKShapeNode(path: path)
line.strokeColor = color
line.lineWidth = 6
line.name = "line"
addChild(line)
}
}
How does it work?
This method
func drawVector(point: CGPoint, vector: CGVector, color: SKColor)
simply draws a vector from a starting point a with a given color.
This one
func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)?
uses the Ray Casting technique discussed above to find the point of intersection and the related normal vector.
Finally in the
func update(_ currentTime: TimeInterval)
we are going to
remove the lines drawn during the previous frame
get the collision point using the raycasting
draw the line between the ball and the collision point
calculate the direction the ball is going to bounce
draw a line that goes from the collision using the direction value got in the previous step
That's it
I currently have two SKSpriteNodes that I have added SKPhysicsBodies to. When they have no SKJoint attached, they collide as expected. As soon as I add the SKPhysicsJoint, they just pass right through each other. Any joint I add functions properly, but the SKPhysicsJointLimit only limits the extent to which the nodes can travel apart from each other, not how close they can get. How can I fix this?
Here is code I am using for the joint:
let joint = SKPhysicsJointLimit.joint(withBodyA: object1.physicsBody!, bodyB: object2.physicsBody!, anchorA: CGPoint(x: object1.position.x + iconController.position.x, y: object1.position.y + iconController.position.y), anchorB: CGPoint(x: object2.position.x + iconController.position.x, y: object2.position.y + iconController.position.y))
joint.maxLength = screen.height * 0.4
physicsWorld.add(joint)
PhysicsBody of both nodes:
self.physicsBody = SKPhysicsBody(circleOfRadius: self.size.width / 2)
self.physicsBody?.allowsRotation = false
self.physicsBody?.friction = 0
self.physicsBody?.mass = 0.1
I have tested it with different values for the above modifications of the SKPhysicsBody and it performs the same.
An SKPhysicsJoint object connects two physics bodies so that they are simulated together by the physics world.
You can use also SKPhysicJointPin:
A pin joint allows the two bodies to independently rotate around the
anchor point as if pinned together.
If your objects work well before the SKPhysicsJoint addition with the physic engine so they fired the didBeginContact as you wish and as you have setted, I think your problem is simply a wrong anchor. Try to add:
let skView = self.view as! SKView
skView.showsPhysics = true
to your scene initialization code: you will see an outline of the physic bodies and maybe you'll see the issue immediatly.
To help you I'll try to make an example of elements configured to collide each other:
enum CollisionTypes: UInt32 {
case Boundaries = 1
case Element = 2
}
class GameScene: SKScene,SKPhysicsContactDelegate {
private var elements = [SKNode]()
override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.physicsWorld.contactDelegate = self
let boundariesFrame = CGRectMake(20, 20, 200, 400)
let boundaries = SKShapeNode.init(rect: boundariesFrame)
boundaries.position = CGPointMake(350,150)
let boundariesBody = SKPhysicsBody.init(edgeLoopFromRect: boundariesFrame)
boundariesBody.dynamic = false
boundariesBody.categoryBitMask = CollisionTypes.Boundaries.rawValue
boundariesBody.contactTestBitMask = CollisionTypes.Element.rawValue
boundaries.physicsBody = boundariesBody
addChild(boundaries)
for index in 0..<5 {
let element = SKShapeNode(circleOfRadius: 10)
let body = SKPhysicsBody(circleOfRadius: 10)
body.linearDamping = 0
// body.mass = 0
body.dynamic = true
body.categoryBitMask = CollisionTypes.Element.rawValue
body.contactTestBitMask = CollisionTypes.Boundaries.rawValue | CollisionTypes.Element.rawValue
body.collisionBitMask = CollisionTypes.Boundaries.rawValue | CollisionTypes.Element.rawValue
element.physicsBody = body
element.position = CGPoint(x: size.width / 2, y: size.height / 2 - 30 * CGFloat(index))
elements.append(element)
addChild(element)
}
}
}
Hope it can help you to find your issue.
Using Swift and Sprite-Kit, I am trying to create a rope with realistic physics between the location of a static SKSpriteNode called "pin", and wherever the user touches the screen. I am doing this by adding individual SKSpriteNodes called ropeNodes, and linking them up with a series of SKPhysicsJointPins. The physics works just fine, however when I try to rotate each individual piece so that they are oriented properly, the ropeNodes no longer form a straight line, nor do they rotate to the correct angle. When I remove the SKPhysicsJoints however, the rotation works as intended for each separate Node. Moving around the anchorPoint for each individual ropeNode only seemed to jumble things up worse. Why does this happen, and how could I go about fixing it? Thanks in advance (:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
dx = pin.position.x - location.x
dy = pin.position.y - location.y
let length = sqrt(pow(dx!, 2) + pow(dy!, 2))
let distanceBetweenRopeNodes = 40
let numberOfPieces = Int(length)/distanceBetweenRopeNodes
var ropeNodes = [SKSpriteNode]()
//adds the pieces to the array and the scene at respective locations
for var index = 0; index < numberOfPieces; ++index{
let point = CGPoint(x: pin.position.x + CGFloat((index) * distanceBetweenRopeNodes) * sin(atan2(dy!, -dx!) + 1.5707), y: pin.position.y + CGFloat((index) * distanceBetweenRopeNodes) * cos(atan2(dy!, -dx!) + 1.5707))
let piece = createRopeNode(point)
piece.runAction(SKAction.rotateByAngle(atan2(-dx!, dy!), duration: 0))
ropeNodes.append(piece)
self.addChild(ropeNodes[index])
}
//Adds an SKPhysicsJointPin between each pair of ropeNodes
self.physicsWorld.addJoint(SKPhysicsJointPin.jointWithBodyA(ropeNodes[0].physicsBody, bodyB: pin.physicsBody, anchor:
CGPoint(x: (ropeNodes[0].position.x + pin.position.x)/2, y: (ropeNodes[0].position.y + pin.position.y)/2)))
for var i = 1; i < ropeNodes.count; ++i{
let nodeA = ropeNodes[i - 1]
let nodeB = ropeNodes[i]
let middlePoint = CGPoint(x: (nodeA.position.x + nodeB.position.x)/2, y: (nodeA.position.y + nodeB.position.y)/2)
let joint = SKPhysicsJointPin.jointWithBodyA(nodeA.physicsBody, bodyB: nodeB.physicsBody, anchor: middlePoint)
self.physicsWorld.addJoint(joint)
}
}
}
func createRopeNode(location: CGPoint) -> SKSpriteNode{
let ropeNode = SKSpriteNode(imageNamed: "RopeTexture")
ropeNode.physicsBody = SKPhysicsBody(rectangleOfSize: ropeNode.size)
ropeNode.physicsBody?.affectedByGravity = false
ropeNode.physicsBody?.collisionBitMask = 0
ropeNode.position = location
ropeNode.name = "RopePiece"
return ropeNode
}
This is an image of what happens when I try to rotate each individual ropeNode
After some thought, I think adding the presentation node as child is a better idea, since you don't need to keep track of the extra nodes in a separate array. To set the rotation of the child, simply unwind the parent's rotation and then add the new rotation angle:
node.zRotation = -node.parent!.zRotation + newRotation
If the rope segments are in a container SKNode, you can iterate over them by
for rope in ropes.children {
if let node = rope.children.first {
let newRotation = ...
node.zRotation = -rope.zRotation + newRotation
}
}
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
}
}
I've been racking my brain and searching here and all over to try to find out how to generate a random position on screen to spawn a circle. I'm hoping someone here can help me because I'm completely stumped. Basically, I'm trying to create a shape that always spawns in a random spot on screen when the user touches.
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenHeight = screenSize.height
let screenWidth = screenSize.width
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.position = CGPointMake(CGFloat(arc4random_uniform(UInt32(Float(screenWidth)))), CGFloat(arc4random_uniform(UInt32(Float(screenHeight)))))
self.removeAllChildren()
self.addChild(currentBall)
}
If you all need more of my code, there really isn't any more. But thank you for whatever help you can give! (Just to reiterate, this code kind of works... But a majority of the spawned balls seem to spawn offscreen)
The problem there is that you scene is bigger than your screen bounds
let viewMidX = view!.bounds.midX
let viewMidY = view!.bounds.midY
print(viewMidX)
print(viewMidY)
let sceneHeight = view!.scene!.frame.height
let sceneWidth = view!.scene!.frame.width
print(sceneWidth)
print(sceneHeight)
let currentBall = SKShapeNode(circleOfRadius: 100)
currentBall.fillColor = .green
let x = view!.scene!.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
let y = view!.scene!.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))
print(x)
print(y)
currentBall.position = CGPoint(x: x, y: y)
view?.scene?.addChild(currentBall)
self.removeAllChildren()
self.addChild(currentBall)
First: Determine the area that will be valid. It might not be the frame of the superview because perhaps the ball (let's call it ballView) might be cut off. The area will likely be (in pseudocode):
CGSize( Width of the superview - width of ballView , Height of the superview - height of ballView)
Once you have a view of that size, just place it on screen with the origin 0, 0.
Secondly: Now you have a range of valid coordinates. Just use a random function (like the one you are using) to select one of them.
Create a swift file with the following:
extension Int
{
static func random(range: Range<Int>) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
And now you can specify a random number as follows:
Int.random(1...1000) //generate a random number integer somewhere from 1 to 1000.
You can generate the values for the x and y coordinates now using this function.
Given the following random generators:
public extension CGFloat {
public static var random: CGFloat { return CGFloat(arc4random()) / CGFloat(UInt32.max) }
public static func random(between x: CGFloat, and y: CGFloat) -> CGFloat {
let (start, end) = x < y ? (x, y) : (y, x)
return start + CGFloat.random * (end - start)
}
}
public extension CGRect {
public var randomPoint: CGPoint {
var point = CGPoint()
point.x = CGFloat.random(between: origin.x, and: origin.x + width)
point.y = CGFloat.random(between: origin.y, and: origin.y + height)
return point
}
}
You can paste the following into a playground:
import XCPlayground
import SpriteKit
let view = SKView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
XCPShowView("game", view)
let scene = SKScene(size: view.frame.size)
view.presentScene(scene)
let wait = SKAction.waitForDuration(0.5)
let popIn = SKAction.scaleTo(1, duration: 0.25)
let popOut = SKAction.scaleTo(0, duration: 0.25)
let remove = SKAction.removeFromParent()
let popInAndOut = SKAction.sequence([popIn, wait, popOut, remove])
let addBall = SKAction.runBlock { [unowned scene] in
let ballRadius: CGFloat = 25
let ball = SKShapeNode(circleOfRadius: ballRadius)
var popInArea = scene.frame
popInArea.inset(dx: ballRadius, dy: ballRadius)
ball.position = popInArea.randomPoint
ball.xScale = 0
ball.yScale = 0
ball.runAction(popInAndOut)
scene.addChild(ball)
}
scene.runAction(SKAction.repeatActionForever(SKAction.sequence([addBall, wait])))
(Just make sure to also paste in the random generators, too, or to copy them to the playground's Sources, as well as to open the assistant editor so you can see the animation.)