Constant Speed Orbit Around Point with Sknode

Constant speed orbit around point with SKNode

The slowing down behavior is often to be expected when dealing with physics engines. They are not perfect, especially in more complex scenarios like constraints. You should not rely on Sprite Kit to provide perfect continuos motion simulation because we have no clue what the physic's engine is doing under-the-hood; there are so many factors involved.

In cases where you need continuous motion, (especially something like constant centripetal motion) it is always best to calculate and preserve these values yourself. There is simply no escaping writing calculations in your update method for behaviors like this.

So I wrote a quick example project that calculates the necessary velocity needed to orbit a particular point. You simply specify a period, orbit position and orbit radius. Note that because I am calculating everything, there is no need for any SKJoints, so that makes this implementation also more lightweight. I highly recommend you do your orbiting this way, as it gives you total control over how you want your nodes to orbit each other (i.e. you could have nodes orbit moving nodes, you could have oval orbiting paths, you could adjust the orbit during key moments in your game, etc.)

import SpriteKit

class GameScene: SKScene {
var node1: SKShapeNode!
var node2: SKShapeNode!
var node2AngularDistance: CGFloat = 0

override func didMoveToView(view: SKView) {
physicsWorld.gravity = CGVector(dx: 0, dy: 0)
node1 = SKShapeNode(circleOfRadius: 10)
node1.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
node1.physicsBody = SKPhysicsBody(circleOfRadius: 10)
node2 = SKShapeNode(circleOfRadius: 10)
node2.position = CGPoint(x: self.size.width/2.0+50, y: self.size.height/2.0)
node2.physicsBody = SKPhysicsBody(circleOfRadius: 10)
self.addChild(node1)
self.addChild(node2)
}
override func update(currentTime: NSTimeInterval) {
let dt: CGFloat = 1.0/60.0 //Delta Time
let period: CGFloat = 3 //Number of seconds it takes to complete 1 orbit.
let orbitPosition = node1.position //Point to orbit.
let orbitRadius = CGPoint(x: 50, y: 50) //Radius of orbit.

let normal = CGVector(dx:orbitPosition.x + CGFloat(cos(self.node2AngularDistance))*orbitRadius.x ,dy:orbitPosition.y + CGFloat(sin(self.node2AngularDistance))*orbitRadius.y);
self.node2AngularDistance += (CGFloat(M_PI)*2.0)/period*dt;
if (fabs(self.node2AngularDistance)>CGFloat(M_PI)*2)
{
self.node2AngularDistance = 0
}
node2.physicsBody!.velocity = CGVector(dx:(normal.dx-node2.position.x)/dt ,dy:(normal.dy-node2.position.y)/dt);
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
node1.position = (touches.first! as! UITouch).locationInNode(self)
}
}

Below is a gif showing the centripetal motion. Notice that because it is completely dynamic we can actually move the centripetal point as it is orbiting.


Sample Image



If however you really want to use your current SKJoints implementation for some reason then I have another solution for you. Simply keep updating your node's linear velocity so that it never slows down.

override func update(currentTime: NSTimeInterval) {
let magnitude = sqrt(secondNode.physicsBody!.velocity.dx*secondNode.physicsBody!.velocity.dx+secondNode.physicsBody!.velocity.dy*secondNode.physicsBody!.velocity.dy)
let angle = Float(atan2(secondNode.physicsBody!.velocity.dy, secondNode.physicsBody!.velocity.dx))
if magnitude < 500 {
secondNode.physicsBody!.velocity = CGVector(dx: CGFloat(cosf(angle)*500), dy: CGFloat(sinf(angle)*500))
}
}



Regardless of the solution you pick (and once again I highly recommend the first solution!), you can choose to apply the velocity over time rather than instantaneously for a more realistic effect. I talk more about this in my answer here

Hopefully I have provided you with enough information to resolve your issue. Let me know if you have any questions. And best of luck with your game!

SKSpriteNode - constant speed, and detect which SKSpriteNode does something

  1. Just manually update your sprite's position. Define a speed in points per second, use a timer in update() to see how much time has passed since it was last called and with a bi of maths, work out its new position. I've done this and it works.

  2. I'm not sure what the relevance of the touchesBegan() code is to this point (are you creating a sprite and setting it moving when you touch the screen?), but in general you could iterate over the array of child nodes in the scene using enumerateChildNodes(). When you have a node that matches your criteria (specific x position etc.), perform the action on the node.

having a node follow at a constant speed

Here you go:

import SpriteKit
import GameplayKit

struct physicsCatagory{
static let me : UInt32 = 0x1 << 1
static let enemy : UInt32 = 0x1 << 2
static let coin : UInt32 = 0x1 << 3
}

class GameScene: SKScene, SKPhysicsContactDelegate {

var lose: SKLabelNode!
var me = SKSpriteNode()
// Tuple to keep track of enemy objects:
typealias FollowerAndTarget = (follower: SKSpriteNode, target: SKSpriteNode)
// [followerName: (followerSprite, targetSprite):
var spriteDictionary: [String: FollowerAndTarget] = [:]
// Give each enemy a unique name for the dictionary:
var enemyCounter = 0
let enemySpeed: CGFloat = 3
var died = Bool()
var timer = SKLabelNode()
var timerValue: Int = 0 {
didSet {
timer.text = "\(timerValue)"
}
}

private func makeEnemyName() -> String {
enemyCounter += 1
return "enemy\(enemyCounter)"
}

private func addEnemyToDict(enemy: SKSpriteNode, target: SKSpriteNode) {
if let name = enemy.name { spriteDictionary[name] = (enemy, target) }
else { print("enemy not found") }
}

private func removeEnemyFromDict(enemy: SKSpriteNode) {
if let name = enemy.name { spriteDictionary[name] = nil }
else { print("enemy not removed from dictionary!") }
}

// dont change anything outside of this, this is what makes the enemy follow you, so i have to have the enemy follow me at a constant speed
private func moveFollowerToTarget(_ sprites: FollowerAndTarget) {
let location = me.position

// Aim
let dx = location.x - sprites.follower.position.x
let dy = location.y - sprites.follower.position.y
let angle = atan2(dy, dx)

sprites.follower.zRotation = angle

// Seek
let vx = cos(angle) * enemySpeed
let vy = sin(angle) * enemySpeed

sprites.follower.position.x += vx
sprites.follower.position.y += vy
}


private func allEnemiesMoveToTarget() {
for sprites in spriteDictionary.values {

moveFollowerToTarget(sprites)
}
}

private func keepEnemiesSeparated() {

for sprites in spriteDictionary.values {

let iterator = sprites.follower
iterator.constraints = []

// get every other follower:
var otherFollowers: [SKSpriteNode] = []
for sprites in spriteDictionary.values {
if sprites.follower == iterator { continue }
else { otherFollowers.append(sprites.follower) }
}

// Assign constrain
for follower in otherFollowers {
let distanceBetween = CGFloat(60)
let constraint = SKConstraint.distance(SKRange(lowerLimit: distanceBetween), to: follower)
iterator.constraints!.append(constraint)
}
}
}

func createEnemy () {
if died { return }

let enemy = SKSpriteNode(color: .green, size: CGSize(width: 60, height: 60))
enemy.size = CGSize(width: 60, height: 60)
enemy.zPosition = 2
enemy.position.y -= size.height / 2
enemy.physicsBody = {
let pb = SKPhysicsBody(circleOfRadius: 30)
pb.restitution = 0.5
pb.affectedByGravity = false
pb.linearDamping = 0
pb.isDynamic = true
pb.categoryBitMask = physicsCatagory.enemy
pb.collisionBitMask = physicsCatagory.me
pb.contactTestBitMask = physicsCatagory.me
return pb
}()

enemy.name = makeEnemyName()
addEnemyToDict(enemy: enemy, target: me)
moveFollowerToTarget((follower: enemy, target: me))
keepEnemiesSeparated()

addChild(enemy)
}

func createCoin () {
let coin = SKSpriteNode(color: .yellow, size: CGSize(width: 20, height: 20))
let height = self.view!.frame.height
let width = self.view!.frame.width

let randomPosition = CGPoint( x:CGFloat( arc4random_uniform( UInt32( floor( width ) ) ) ),
y:CGFloat( arc4random_uniform( UInt32( floor( height ) ) ) )
)

coin.position = randomPosition
addChild(coin)
}

func restartScene(){
self.removeAllChildren()
self.removeAllActions()
died = false

let nextScene = GameScene(size: self.size)
nextScene.scaleMode = self.scaleMode
let transition = SKTransition.fade(withDuration: 1)
view?.presentScene(nextScene, transition: transition)
}

func createScene(){
me = SKSpriteNode(color: .blue, size: CGSize(width: 60, height: 60))
me.physicsBody = SKPhysicsBody(circleOfRadius: 30)
me.physicsBody?.affectedByGravity = false
me.physicsBody?.categoryBitMask = physicsCatagory.me
me.physicsBody?.collisionBitMask = physicsCatagory.enemy
me.zPosition = 2

timer = SKLabelNode(fontNamed: "Chalkduster")
timer.text = "\(timerValue)"
addChild(me)
addChild(timer)

let wait = SKAction.wait(forDuration: 1)

let block = SKAction.run({
[unowned self] in

if self.timerValue >= 0{
self.timerValue += 1
}else{
self.removeAction(forKey: "countdown")
}
})

let sequence = SKAction.sequence([wait,block])

run(SKAction.repeatForever(sequence), withKey: "countdown")

self.physicsWorld.contactDelegate = self

let border = SKPhysicsBody (edgeLoopFrom: self.frame)
border.friction = 0
self.physicsBody = border

run(SKAction.repeatForever(SKAction.sequence([SKAction.run(createEnemy), SKAction.wait(forDuration: 2.0)])))

run(SKAction.repeatForever(SKAction.sequence([SKAction.run(createCoin), SKAction.wait(forDuration: TimeInterval(arc4random_uniform(11) + 5))])))

}


override func didMove(to view: SKView) {
scene?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
createScene()
}

func didBegin(_ contact: SKPhysicsContact) {

let firstBody = contact.bodyA
let secondBody = contact.bodyB

if firstBody.categoryBitMask == physicsCatagory.me && secondBody.categoryBitMask == physicsCatagory.enemy
|| firstBody.categoryBitMask == physicsCatagory.enemy && secondBody.categoryBitMask == physicsCatagory.me {
died = true
restartScene()
}
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

for touch in touches{

let location = touch.location(in: self)
me.run(SKAction.moveTo(x: location.x, duration: 0))
me.run(SKAction.moveTo(y: location.y, duration: 0))
allEnemiesMoveToTarget()
}
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {

for touch in touches{

let location = touch.location(in: self)
me.run(SKAction.moveTo(x: location.x, duration: 0))
me.run(SKAction.moveTo(y: location.y, duration: 0))
allEnemiesMoveToTarget()
}
}

override func update(_ currentTime: TimeInterval) {

// Will iterate through dictonary and then call moveFollowerToTarget()
// thus giving each enemy a new movement action to follow.
allEnemiesMoveToTarget()
}
}

Rotate sprite by touch with a limited rotation speed

I have a turret which ... should target the finger position, but not
immediately, by taking time to turn.

You won't be able to get away with SKActions for something like this. You can try but it will be really messy and inefficient. You need real-time motion control for something like this because the angular velocity of your turret needs to change constantly depending on the touch position.

So I wrote you a quick example project showing how to calculate the angular velocity. The project handles all special cases as well, such as preventing the angle from jumping over your target rotation.

import SpriteKit

class GameScene: SKScene {
let turret = SKSpriteNode(imageNamed: "Spaceship")
let rotationSpeed: CGFloat = CGFloat(M_PI) //Speed turret rotates.
let rotationOffset: CGFloat = -CGFloat(M_PI/2.0) //Controls which side of the sprite faces the touch point. I offset the angle by -90 degrees so that the top of my image faces the touch point.

private var touchPosition: CGFloat = 0
private var targetZRotation: CGFloat = 0

override func didMoveToView(view: SKView) {
turret.physicsBody = SKPhysicsBody(rectangleOfSize: turret.size)
turret.physicsBody!.affectedByGravity = false
turret.position = CGPoint(x: self.size.width/2.0, y: self.size.height/2.0)
self.addChild(turret)
}

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
calculateAngleToTouch(touches.anyObject() as UITouch)
}

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
calculateAngleToTouch(touches.anyObject() as UITouch)
}

func calculateAngleToTouch(touch: UITouch) {
let position = touch.locationInNode(self)
let angle = atan2(position.y-turret.position.y, position.x-turret.position.x)

targetZRotation = angle + rotationOffset
}

override func update(currentTime: NSTimeInterval) {
var angularDisplacement = targetZRotation - turret.zRotation
if angularDisplacement > CGFloat(M_PI) {
angularDisplacement = (angularDisplacement - CGFloat(M_PI)*2)
} else if angularDisplacement < -CGFloat(M_PI) {
angularDisplacement = (angularDisplacement + CGFloat(M_PI)*2)
}

if abs(angularDisplacement) > rotationSpeed*(1.0/60.0) {
let angularVelocity = angularDisplacement < 0 ? -rotationSpeed : rotationSpeed
turret.physicsBody!.angularVelocity = angularVelocity
} else {
turret.physicsBody!.angularVelocity = 0
turret.zRotation = targetZRotation
}

}


}

Sample Image

SKSpriteNode follow CGPath plus adding SKAction

You can't move the same node with 2 move actions. What you can do is place the node in a container node. Then move the container node left while its child moves in a circle.

Another option is to not use SKActions and use something more dynamic and real-time by computing the centripetal velocity manually and shifting the centripetal point overtime. You can see an example of this in my answer here.



Related Topics



Leave a reply



Submit