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

2

8

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

Coulombe answered 2/9, 2015 at 12:15 Comment(2)
Just set the velocity, and when it reaches a new point give it a new velocity aimed at the next point.Spoon
SKAction followPath.Fiche
M
6

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.

Modular answered 2/9, 2015 at 17:54 Comment(21)
Sorry, I've not been on the forum for a while. Thanks, you are a lifesaver. I tried to change some points when the body gets into contact with another body but the behaviour wasn't consistent. From my needs, the path element pathIndex - 2 should be changed after bodies contact. I put a println to check pathIndex just after and the values weren't consistent. Any advice on this?Coulombe
@Coulombe I'm having trouble following. It looks like what you are trying to do is change the path after some collision/contact. In this case you would set the path to the new path of points you want the node to travel, then make sure to set the pathIndex to 0 gain (or the index of the point you want it to start on). If you still have a problem try putting an nslog or println inside the didReachPoint so you can see every time the node reaches a point. This code should work if everything is set correctly. Sorry if this wasn't what you were asking.Modular
let me try to explain further. Basically two points are needed to get the direction. I changed a point after contact/collision using path[pathIndex-2] = CGPoint(x: 140, y: 90). This gives me the result I want sometimes. Other times when running the same code without any changes path[pathIndex-2] is a different point. The log shows pathIndex changing between 3 and 4. That's my current issue. I don't when whether the way it's updated is giving the inconsistent valuesCoulombe
@Coulombe You shouldn't use path[pathIndex-2] because pathIndex changes depending on which point the node is moving too. For example when the node is moving to the first point then pathIndex = 0, when it gets there we increment pathIndex by 1 which forces the node to travel to the second point and so forth. You should change the point explicitly. For example if you want to change the second point then do path[1]. Or if you want to change the first point do path[0]. Currently what you are doing is setting the point the node traveled to 2 positions ago, which is why you are noticing variance.Modular
@EpicByte Nice solution. I realised that if there is a contact/collision between circle body and another body the circle body moves erratically forcing to make it's way to it's intended point in the path. I am trying to move the circle body to the next point in ballPath when its in contact/collision with another body. The idea here is to caluculate/use the bodies contact point as some temporary point to get it to next point in ballPath. I'm not too sure how to go about this.Percutaneous
@Percutaneous To fix the erratic movement, 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.Modular
@EpicByte thanks, but what about the later part of my question. For example, if a body collides with the circle body when it's moving to ballPath[x] I would like to move the body to ballPath[x+1] without it ever reaching ballPath[x]. A bit like the contact point of the circle body with it's colliding body taking the place of ballPath[x] temporarily.Percutaneous
So when the body collides with the circle you want the node to stop traveling to the current point (path[x]) and instead travel to the next point path[x+1]? So can't you just set the pathIndex property equal to pathIndex+1? This will cause the node to begin moving to the next point in the path. You need to be careful though when doing this because you could possibly set the node index out of bounds. You may also want to disable the path repeating code in the didReachPoint function because you seem to be doing your own control of the pathIndex.Modular
In fact, you may want to set the pathIndex to something like this: pathIndex = (pathIndex+N)%path.count where N is how much further ahead or behind you would like to go. For example if you would like to go to the previous point then do pathIndex = (pathIndex-1)%path.count This will keep your increments and decrements within the bounds of the path.Modular
@EpicByte: your solution has saved me hours of pain. I implemented the contact solution you gave @Percutaneous but I am trying to change the circle body to a rectangular body when the circle body is in contact with some other body. I added the code in the next comment to didBeginContact when contact was established. The code is too a bit too long for this comment.Bedard
@EpicByte continued, let img = SKTexture(imageNamed: "rectangular"); (firstBody.node! as? SKSpriteNode)?.size = img.size(); firstBody.node!.physicsBody = SKPhysicsBody(texture: img, size: img.size()); firstBody.node!.physicsBody?.allowsRotation = false; changeCircleAction = SKAction.setTexture(img); firstBody.node!.runAction(changeCircleAction);Bedard
@EpicByte continued, After this contact condition is met, all other contact conditions involving the changed circle to rectangle are not satisfied. I suspect the pointRadius to be the issue. Initially the pointRadius was the circle width. But for a rectangle, I tried changing the pointRadius to the width or height but the result didn't change. The rectangle is larger than the circle'Bedard
@Bedard What do you mean when you say "After this contact condition is met, all other contact conditions involving the changed circle to rectangle are not satisfied" Are you talking about Sprite Kits contact detection system I.e. didBeginContact? If so, then the pointRadius should have no effect. The pointRadius is simply how far from the destination the node must be to be considered at the point. The reason for this is because we can't check if a node is exactly at a point because it may never be directly at this point, even though visually it looks to be at that point.Modular
Unless maybe you are using didReachPoint as your "contact" detection system, in which case my comment above doesn't apply.Modular
@EpicByte Sorry, I meant after I change the circle to a rectangle all other contact conditions (that I set in the didBeginContact) between other bodies and the rectangle are not satisfied. Yes, I am using didReachPoint as you used it in the answerBedard
@EpicByte I have been trying to adjust values for the didReachPoint, but I keep getting the same problem - after circle changes to rectangle all contact conditions between other bodies and the rectangle are not recognized.Bedard
@EpicByte sorry again, I meant I have been trying to adjust values for the radiusPoint. I have a few bodies located at some of the ballPath positions. I realized that at times the ball reaches these points but didBeginContact does not recognize any contact between the ball and the body at the ballPath position. I believe this is the bigger picture of my problem. Most of my conditions are set on contact, but contact doesn't occur though it looks like it has. Is there a way this can be solved?Bedard
@Bedard Hmm this sounds less of an issue with the code provided and more of an issue with the way you are handling contacts. I really can't do much without seeing the code. You should post a new question explaining what's not working and showing the code. Then myself and others can help you fix the issue.Modular
@EpicByte I just posted a question #32892330Bedard
@EpicByte I am using your solution but my sprites are moving at considerably different speeds on different iOS devices. How can I make them move at similar speeds on all devices.Besom
@EpicByte Can we please have a chat concerning my issue chat.stackoverflow.com/rooms/128504/…. Would really appreciate itBesom
S
1

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

Slowdown answered 2/9, 2015 at 13:29 Comment(1)
from the post, I think @Coulombe wants to do this using physics propertiesPercutaneous

© 2022 - 2024 — McMap. All rights reserved.