peter chrisanthopoulos peter chrisanthopoulos -4 years ago 126
iOS Question

having a node follow at a constant speed

I'm trying to make a small mini game where you drag a ball around the screen and every 10 seconds a ball gets added in that follows you. so far the you can drag a ball around the screen and a ball follows you, but when another ball gets added in the balls group together. I think this is because the ball is following me depending on how fast I'm going. so is there a certain way in which I can have the balls follow me at a certain speed constantly, like 10 pixels a second or something, and that should prevent the balls from grouping together.

I am currently working on the score so it should soon go up every second you survive. and you die if you touch one of the balls.

below is the code and a short gif of my current code

!(https://gyazo.com/1d6a56527bfd0884e8a26cff730f4e03)

import SpriteKit
import GameplayKit

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

}

class GameScene: SKScene, SKPhysicsContactDelegate {


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!") }
}

private func moveFollowerToTarget(_ sprites: FollowerAndTarget) {
let action = SKAction.move(to: sprites.target.position, duration: 1)
sprites.follower.run(action)
}



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

let enemySpeed: CGFloat = 300
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
var died = Bool()




override func didMove(to view: SKView) {

createScene()
}



func createEnemy () {
if died == true{

}
else {
let enemy = SKSpriteNode(imageNamed: "enemy1")
enemy.name = makeEnemyName()
addEnemyToDict(enemy: enemy, target: me)
moveFollowerToTarget((follower: enemy, target: me))
enemy.size = CGSize(width: 60, height: 60)
enemy.position = CGPoint(x:667, y: 200)
enemy.physicsBody?.restitution = 0.5
enemy.physicsBody = SKPhysicsBody(circleOfRadius: 60)
enemy.physicsBody?.affectedByGravity = false
enemy.zPosition = 2
enemy.physicsBody?.linearDamping = 0
enemy.physicsBody?.isDynamic = true
enemy.physicsBody?.categoryBitMask = physicsCatagory.enemy
enemy.physicsBody?.collisionBitMask = physicsCatagory.me
enemy.physicsBody?.contactTestBitMask = physicsCatagory.me
addChild(enemy)
}
}


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()
}

}
var lose: SKLabelNode!


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

if let nextScene = GameScene(fileNamed: "menuScene"){
nextScene.scaleMode = self.scaleMode
let transition = SKTransition.fade(withDuration: 1)
view?.presentScene(nextScene, transition: transition)
}
}

func createScene(){
me = self.childNode(withName: "me") as! SKSpriteNode
me.physicsBody = SKPhysicsBody(circleOfRadius: 20)
me.physicsBody?.affectedByGravity = false
me.physicsBody?.categoryBitMask = physicsCatagory.me
me.physicsBody?.collisionBitMask = physicsCatagory.enemy
me.zPosition = 2

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: 4.0)])))
}
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()
}


}

Answer Source

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()
  }
}
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download