Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to set physics properties for a circle so it follows given path

The movement of a physics body circle is too erratic for what I want to achieve. I would like to restrict it so it follows a certain path touching specific points (or a range of points) as shown in the image below. How can I set the physics properties to traverse a similar path?

enter image description here

like image 836
NSSwift Avatar asked Sep 02 '15 12:09

NSSwift


People also ask

What is the force required to keep something moving in a circular path?

A centripetal force is a net force that acts on an object to keep it moving along a circular path.

What forces cause a car to follow a circular path?

As a car makes a turn, the force of friction acting upon the turned wheels of the car provides centripetal force required for circular motion.

How would you explain the motion of an object in a circular path?

In physics, circular motion is a movement of an object along the circumference of a circle or rotation along a circular path. It can be uniform, with constant angular rate of rotation and constant speed, or non-uniform with a changing rate of rotation.


2 Answers

how to set physics properties for a circle so it follows given path

So essentially you are looking to move a node to a particular point using real-time motion. I have an answer here showing how to do this, however given the number of up votes this question has received, I will provide a more detailed answer.

What the answer I linked to doesn't provide is traversing a path of points. So below I have provided a solution showing how this can be done below. It simply just moves to each point in the path, and each time the node reaches a point, we increment the index to move to the next point. I also added a few variables for travel speed, rate (to make the motion more smooth or static) and whether or not the node should repeat the path. You could further expand upon this solution to better meet the needs of your game. I would definitely consider subclassing a node and building this behavior into it so you can re-use this motion for multiple nodes.

One final note, you may notice the calculation for the impulse varies between my solution below and the answer I linked to above. This is because I am avoiding using angle calculation because they are very expensive. Instead I am calculating a normal so that the calculation is more computationally efficient.

One final note, my answer here explains the use of the rate factor to smooth the motion and leave room for motion distortions.

import SpriteKit

class GameScene: SKScene {
    var node: SKShapeNode! //The node.
    let path: [CGPoint] = [CGPoint(x: 100, y: 100),CGPoint(x: 100, y: 300),CGPoint(x: 300, y: 300),CGPoint(x: 300, y: 100)] //The path of points to travel.
    let repeats: Bool = true //Whether to repeat the path.
    var pathIndex = 0 //The index of the current point to travel.
    let pointRadius: CGFloat = 10 //How close the node must be to reach the destination point.
    let travelSpeed: CGFloat = 200 //Speed the node will travel at.
    let rate: CGFloat = 0.5 //Motion smoothing.

    override func didMoveToView(view: SKView) {
        node = SKShapeNode(circleOfRadius: 10)
        node.physicsBody = SKPhysicsBody(circleOfRadius: 10)
        node.physicsBody!.affectedByGravity = false
        self.addChild(node)
    }

    final func didReachPoint() {
        //We reached a point!
        pathIndex++

        if pathIndex >= path.count && repeats {
            pathIndex = 0
        }
    }

    override func update(currentTime: NSTimeInterval) {
        if pathIndex >= 0 && pathIndex < path.count {
            let destination = path[pathIndex]
            let displacement = CGVector(dx: destination.x-node.position.x, dy: destination.y-node.position.y)
            let radius = sqrt(displacement.dx*displacement.dx+displacement.dy*displacement.dy)
            let normal = CGVector(dx: displacement.dx/radius, dy: displacement.dy/radius)
            let impulse = CGVector(dx: normal.dx*travelSpeed, dy: normal.dy*travelSpeed)
            let relativeVelocity = CGVector(dx:impulse.dx-node.physicsBody!.velocity.dx, dy:impulse.dy-node.physicsBody!.velocity.dy);
            node.physicsBody!.velocity=CGVectorMake(node.physicsBody!.velocity.dx+relativeVelocity.dx*rate, node.physicsBody!.velocity.dy+relativeVelocity.dy*rate);
            if radius < pointRadius {
                didReachPoint()
            }
        }
    }
}

I did this pretty quickly so I apologize if there is a mistake. I don't have time now but I will add a gif showing the solution later.


A note about collisions

To fix the erratic movement during a collision, after the 2 bodies collide set the "rate" property to 0 or preferably a very low number to reduce the travel velocity impulse which will give you more room for motion distortion. Then at some point in the future (maybe some time after the collision occurs or preferably when the body is moving slow again) set the rate back to its initial value. If you really want a nice effect, you can actually ramp up the rate value over time from 0 to the initial value to give yourself a smooth and gradual acceleration.

like image 172
Epic Byte Avatar answered Nov 13 '22 19:11

Epic Byte


Quick implementation using followPath:duration:

override func didMoveToView(view: SKView) {
    self.backgroundColor = UIColor.blackColor()
    let xWidth: CGFloat = CGRectGetMaxX(self.frame)
    let yHeight: CGFloat = CGRectGetMaxY(self.frame)

    let ball = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake(50.0, 50.0))
    let offset : CGFloat = ball.size.width / 2

    // Moving path
    let path = CGPathCreateMutable()
    CGPathMoveToPoint(path, nil, offset, yHeight / 2)
    CGPathAddLineToPoint(path, nil, xWidth * 2 / 3, yHeight - offset)
    CGPathAddLineToPoint(path, nil, xWidth - offset, yHeight * 2 / 3)
    CGPathAddLineToPoint(path, nil, xWidth / 2, offset)
    CGPathAddLineToPoint(path, nil, offset, yHeight / 2)

    // Movement
    let moveByPath = SKAction.followPath(path, asOffset: false, orientToPath: false, duration: 4.0)
    let moveForever = SKAction.repeatActionForever(moveByPath)
    ball.runAction(moveForever)

    self.addChild(ball)
}

In GameViewController.swift, I changed the default GameScene.unarchiveFromFile method to let scene = GameScene(size: view.bounds.size) for creating the scene's size.

Preview:

enter image description here

like image 32
WangYudong Avatar answered Nov 13 '22 18:11

WangYudong